From 4d2db6895a167fcb3f38972852a92bcceab9d930 Mon Sep 17 00:00:00 2001 From: ProsperousPotato Date: Sat, 14 Jun 2025 16:41:22 +0100 Subject: rewrite from original --- FAQ | 253 ++ LEGACY | 17 + README | 34 + TODO | 28 + graphics.c | 3812 +++++++++++++++++++++ graphics.h | 107 + graphics.o | Bin 0 -> 82328 bytes icat-mini.sh | 801 +++++ khash.h | 627 ++++ kvec.h | 90 + patches/alpha.diff | 129 + patches/changealpha.diff | 80 + patches/disable_bold.diff | 70 + patches/kitty-graphics.diff | 7324 ++++++++++++++++++++++++++++++++++++++++ patches/st-scrollback.diff | 351 ++ rowcolumn_diacritics_helpers.c | 391 +++ rowcolumn_diacritics_helpers.o | Bin 0 -> 16264 bytes 17 files changed, 14114 insertions(+) create mode 100644 FAQ create mode 100644 LEGACY create mode 100644 README create mode 100644 TODO create mode 100644 graphics.c create mode 100644 graphics.h create mode 100644 graphics.o create mode 100755 icat-mini.sh create mode 100644 khash.h create mode 100644 kvec.h create mode 100644 patches/alpha.diff create mode 100644 patches/changealpha.diff create mode 100644 patches/disable_bold.diff create mode 100644 patches/kitty-graphics.diff create mode 100644 patches/st-scrollback.diff create mode 100644 rowcolumn_diacritics_helpers.c create mode 100644 rowcolumn_diacritics_helpers.o diff --git a/FAQ b/FAQ new file mode 100644 index 0000000..6287a27 --- /dev/null +++ b/FAQ @@ -0,0 +1,253 @@ +## Why does st not handle utmp entries? + +Use the excellent tool of [utmp](https://git.suckless.org/utmp/) for this task. + + +## Some _random program_ complains that st is unknown/not recognised/unsupported/whatever! + +It means that st doesn’t have any terminfo entry on your system. Chances are +you did not `make install`. If you just want to test it without installing it, +you can manually run `tic -sx st.info`. + + +## Nothing works, and nothing is said about an unknown terminal! + +* Some programs just assume they’re running in xterm i.e. they don’t rely on + terminfo. What you see is the current state of the “xterm compliance”. +* Some programs don’t complain about the lacking st description and default to + another terminal. In that case see the question about terminfo. + + +## How do I scroll back up? + +* Using a terminal multiplexer. + * `st -e tmux` using C-b [ + * `st -e screen` using C-a ESC +* Using the excellent tool of [scroll](https://git.suckless.org/scroll/). +* Using the scrollback [patch](https://st.suckless.org/patches/scrollback/). + + +## I would like to have utmp and/or scroll functionality by default + +You can add the absolute path of both programs in your config.h file. You only +have to modify the value of utmp and scroll variables. + + +## Why doesn't the Del key work in some programs? + +Taken from the terminfo manpage: + + If the terminal has a keypad that transmits codes when the keys + are pressed, this information can be given. Note that it is not + possible to handle terminals where the keypad only works in + local (this applies, for example, to the unshifted HP 2621 keys). + If the keypad can be set to transmit or not transmit, give these + codes as smkx and rmkx. Otherwise the keypad is assumed to + always transmit. + +In the st case smkx=E[?1hE= and rmkx=E[?1lE>, so it is mandatory that +applications which want to test against keypad keys send these +sequences. + +But buggy applications (like bash and irssi, for example) don't do this. A fast +solution for them is to use the following command: + + $ printf '\033[?1h\033=' >/dev/tty + +or + $ tput smkx + +In the case of bash, readline is used. Readline has a different note in its +manpage about this issue: + + enable-keypad (Off) + When set to On, readline will try to enable the + application keypad when it is called. Some systems + need this to enable arrow keys. + +Adding this option to your .inputrc will fix the keypad problem for all +applications using readline. + +If you are using zsh, then read the zsh FAQ +: + + It should be noted that the O / [ confusion can occur with other keys + such as Home and End. Some systems let you query the key sequences + sent by these keys from the system's terminal database, terminfo. + Unfortunately, the key sequences given there typically apply to the + mode that is not the one zsh uses by default (it's the "application" + mode rather than the "raw" mode). Explaining the use of terminfo is + outside of the scope of this FAQ, but if you wish to use the key + sequences given there you can tell the line editor to turn on + "application" mode when it starts and turn it off when it stops: + + function zle-line-init () { echoti smkx } + function zle-line-finish () { echoti rmkx } + zle -N zle-line-init + zle -N zle-line-finish + +Putting these lines into your .zshrc will fix the problems. + + +## How can I use meta in 8bit mode? + +St supports meta in 8bit mode, but the default terminfo entry doesn't +use this capability. If you want it, you have to use the 'st-meta' value +in TERM. + + +## I cannot compile st in OpenBSD + +OpenBSD lacks librt, despite it being mandatory in POSIX +. +If you want to compile st for OpenBSD you have to remove -lrt from config.mk, and +st will compile without any loss of functionality, because all the functions are +included in libc on this platform. + + +## The Backspace Case + +St is emulating the Linux way of handling backspace being delete and delete being +backspace. + +This is an issue that was discussed in suckless mailing list +. Here is why some old grumpy +terminal users wants its backspace to be how he feels it: + + Well, I am going to comment why I want to change the behaviour + of this key. When ASCII was defined in 1968, communication + with computers was done using punched cards, or hardcopy + terminals (basically a typewriter machine connected with the + computer using a serial port). ASCII defines DELETE as 7F, + because, in punched-card terms, it means all the holes of the + card punched; it is thus a kind of 'physical delete'. In the + same way, the BACKSPACE key was a non-destructive backspace, + as on a typewriter. So, if you wanted to delete a character, + you had to BACKSPACE and then DELETE. Another use of BACKSPACE + was to type accented characters, for example 'a BACKSPACE `'. + The VT100 had no BACKSPACE key; it was generated using the + CONTROL key as another control character (CONTROL key sets to + 0 b7 b6 b5, so it converts H (code 0x48) into BACKSPACE (code + 0x08)), but it had a DELETE key in a similar position where + the BACKSPACE key is located today on common PC keyboards. + All the terminal emulators emulated the difference between + these keys correctly: the backspace key generated a BACKSPACE + (^H) and delete key generated a DELETE (^?). + + But a problem arose when Linus Torvalds wrote Linux. Unlike + earlier terminals, the Linux virtual terminal (the terminal + emulator integrated in the kernel) returned a DELETE when + backspace was pressed, due to the VT100 having a DELETE key in + the same position. This created a lot of problems (see [1] + and [2]). Since Linux has become the king, a lot of terminal + emulators today generate a DELETE when the backspace key is + pressed in order to avoid problems with Linux. The result is + that the only way of generating a BACKSPACE on these systems + is by using CONTROL + H. (I also think that emacs had an + important point here because the CONTROL + H prefix is used + in emacs in some commands (help commands).) + + From point of view of the kernel, you can change the key + for deleting a previous character with stty erase. When you + connect a real terminal into a machine you describe the type + of terminal, so getty configures the correct value of stty + erase for this terminal. In the case of terminal emulators, + however, you don't have any getty that can set the correct + value of stty erase, so you always get the default value. + For this reason, it is necessary to add 'stty erase ^H' to your + profile if you have changed the value of the backspace key. + Of course, another solution is for st itself to modify the + value of stty erase. I usually have the inverse problem: + when I connect to non-Unix machines, I have to press CONTROL + + h to get a BACKSPACE. The inverse problem occurs when a user + connects to my Unix machines from a different system with a + correct backspace key. + + [1] http://www.ibb.net/~anne/keyboard.html + [2] http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-5.html + + +## But I really want the old grumpy behaviour of my terminal + +Apply [1]. + +[1] https://st.suckless.org/patches/delkey + + +## Why do images not work in st using the w3m image hack? + +w3mimg uses a hack that draws an image on top of the terminal emulator Drawable +window. The hack relies on the terminal to use a single buffer to draw its +contents directly. + +st uses double-buffered drawing so the image is quickly replaced and may show a +short flicker effect. + +Below is a patch example to change st double-buffering to a single Drawable +buffer. + +diff --git a/x.c b/x.c +--- a/x.c ++++ b/x.c +@@ -732,10 +732,6 @@ xresize(int col, int row) + win.tw = col * win.cw; + win.th = row * win.ch; + +- XFreePixmap(xw.dpy, xw.buf); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); +- XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ +@@ -1148,8 +1144,7 @@ xinit(int cols, int rows) + gcvalues.graphics_exposures = False; + dc.gc = XCreateGC(xw.dpy, parent, GCGraphicsExposures, + &gcvalues); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.buf = xw.win; + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + +@@ -1632,8 +1627,6 @@ xdrawline(Line line, int x1, int y1, int x2) + void + xfinishdraw(void) + { +- XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, +- win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); + + +## BadLength X error in Xft when trying to render emoji + +Xft makes st crash when rendering color emojis with the following error: + +"X Error of failed request: BadLength (poly request too large or internal Xlib length error)" + Major opcode of failed request: 139 (RENDER) + Minor opcode of failed request: 20 (RenderAddGlyphs) + Serial number of failed request: 1595 + Current serial number in output stream: 1818" + +This is a known bug in Xft (not st) which happens on some platforms and +combination of particular fonts and fontconfig settings. + +See also: +https://gitlab.freedesktop.org/xorg/lib/libxft/issues/6 +https://bugs.freedesktop.org/show_bug.cgi?id=107534 +https://bugzilla.redhat.com/show_bug.cgi?id=1498269 + +The solution is to remove color emoji fonts or disable this in the fontconfig +XML configuration. As an ugly workaround (which may work only on newer +fontconfig versions (FC_COLOR)), the following code can be used to mask color +fonts: + + FcPatternAddBool(fcpattern, FC_COLOR, FcFalse); + +Please don't bother reporting this bug to st, but notify the upstream Xft +developers about fixing this bug. + +As of 2022-09-05 this now seems to be finally fixed in libXft 2.3.5: +https://gitlab.freedesktop.org/xorg/lib/libxft/-/blob/libXft-2.3.5/NEWS diff --git a/LEGACY b/LEGACY new file mode 100644 index 0000000..bf28b1e --- /dev/null +++ b/LEGACY @@ -0,0 +1,17 @@ +A STATEMENT ON LEGACY SUPPORT + +In the terminal world there is much cruft that comes from old and unsup‐ +ported terminals that inherit incompatible modes and escape sequences +which noone is able to know, except when he/she comes from that time and +developed a graphical vt100 emulator at that time. + +One goal of st is to only support what is really needed. When you en‐ +counter a sequence which you really need, implement it. But while you +are at it, do not add the other cruft you might encounter while sneek‐ +ing at other terminal emulators. History has bloated them and there is +no real evidence that most of the sequences are used today. + + +Christoph Lohmann <20h@r-36.net> +2012-09-13T07:00:36.081271045+02:00 + diff --git a/README b/README new file mode 100644 index 0000000..6a846ed --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +st - simple terminal +-------------------- +st is a simple terminal emulator for X which sucks less. + + +Requirements +------------ +In order to build st you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (st is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install st (if +necessary as root): + + make clean install + + +Running st +---------- +If you did not install st with make clean install, you must compile +the st terminfo entry with the following command: + + tic -sx st.info + +See the man page for additional details. + +Credits +------- +Based on Aurélien APTEL bt source code. + diff --git a/TODO b/TODO new file mode 100644 index 0000000..5f74cd5 --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +vt emulation +------------ + +* double-height support + +code & interface +---------------- + +* add a simple way to do multiplexing + +drawing +------- +* add diacritics support to xdraws() + * switch to a suckless font drawing library +* make the font cache simpler +* add better support for brightening of the upper colors + +bugs +---- + +* fix shift up/down (shift selection in emacs) +* remove DEC test sequence when appropriate + +misc +---- + + $ grep -nE 'XXX|TODO' st.c + diff --git a/graphics.c b/graphics.c new file mode 100644 index 0000000..64e6fe0 --- /dev/null +++ b/graphics.c @@ -0,0 +1,3812 @@ +/* The MIT License + + Copyright (c) 2021-2024 Sergei Grechanik + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// This file implements a subset of the kitty graphics protocol. +// +//////////////////////////////////////////////////////////////////////////////// + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "graphics.h" +#include "khash.h" +#include "kvec.h" + +extern char **environ; + +#define MAX_FILENAME_SIZE 256 +#define MAX_INFO_LEN 256 +#define MAX_IMAGE_RECTS 20 + +/// The type used in this file to represent time. Used both for time differences +/// and absolute times (as milliseconds since an arbitrary point in time, see +/// `initialization_time`). +typedef int64_t Milliseconds; + +enum ScaleMode { + SCALE_MODE_UNSET = 0, + /// Stretch or shrink the image to fill the box, ignoring aspect ratio. + SCALE_MODE_FILL = 1, + /// Preserve aspect ratio and fit to width or to height so that the + /// whole image is visible. + SCALE_MODE_CONTAIN = 2, + /// Do not scale. The image may be cropped if the box is too small. + SCALE_MODE_NONE = 3, + /// Do not scale, unless the box is too small, in which case the image + /// will be shrunk like with `SCALE_MODE_CONTAIN`. + SCALE_MODE_NONE_OR_CONTAIN = 4, +}; + +enum AnimationState { + ANIMATION_STATE_UNSET = 0, + /// The animation is stopped. Display the current frame, but don't + /// advance to the next one. + ANIMATION_STATE_STOPPED = 1, + /// Run the animation to then end, then wait for the next frame. + ANIMATION_STATE_LOADING = 2, + /// Run the animation in a loop. + ANIMATION_STATE_LOOPING = 3, +}; + +/// The status of an image. Each image uploaded to the terminal is cached on +/// disk, then it is loaded to ram when needed. +enum ImageStatus { + STATUS_UNINITIALIZED = 0, + STATUS_UPLOADING = 1, + STATUS_UPLOADING_ERROR = 2, + STATUS_UPLOADING_SUCCESS = 3, + STATUS_RAM_LOADING_ERROR = 4, + STATUS_RAM_LOADING_IN_PROGRESS = 5, + STATUS_RAM_LOADING_SUCCESS = 6, +}; + +const char *image_status_strings[6] = { + "STATUS_UNINITIALIZED", + "STATUS_UPLOADING", + "STATUS_UPLOADING_ERROR", + "STATUS_UPLOADING_SUCCESS", + "STATUS_RAM_LOADING_ERROR", + "STATUS_RAM_LOADING_SUCCESS", +}; + +enum ImageUploadingFailure { + ERROR_OVER_SIZE_LIMIT = 1, + ERROR_CANNOT_OPEN_CACHED_FILE = 2, + ERROR_UNEXPECTED_SIZE = 3, + ERROR_CANNOT_COPY_FILE = 4, +}; + +const char *image_uploading_failure_strings[5] = { + "NO_ERROR", + "ERROR_OVER_SIZE_LIMIT", + "ERROR_CANNOT_OPEN_CACHED_FILE", + "ERROR_UNEXPECTED_SIZE", + "ERROR_CANNOT_COPY_FILE", +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// We use the following structures to represent images and placements: +// +// - Image: this is the main structure representing an image, usually created +// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id, +// specified by 'i='). An image may have multiple frames (ImageFrame) and +// placements (ImagePlacement). +// +// - ImageFrame: represents a single frame of an image, usually created by +// the action 'a=f' (and the first frame is created with the image itself). +// Each frame has an index and also: +// - a file containing the frame data (considered to be "on disk", although +// it's probably in tmpfs), +// - an imlib object containing the fully composed frame (i.e. the frame +// data from the file composed onto the background frame or color). It is +// not ready for display yet, because it needs to be scaled and uploaded +// to the X server. +// +// - ImagePlacement: represents a placement of an image, created by 'a=p' and +// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also +// each placement has an array of pixmaps: one for each frame of the image. +// Each pixmap is a scaled and uploaded image ready to be displayed. +// +// Images are store in the `images` hash table, mapping image ids to Image +// objects (allocated on the heap). +// +// Placements are stored in the `placements` hash table of each Image object, +// mapping placement ids to ImagePlacement objects (also allocated on the heap). +// +// ImageFrames are stored in the `first_frame` field and in the +// `frames_beyond_the_first` array of each Image object. They are stored by +// value, so ImageFrame pointer may be invalidated when frames are +// added/deleted, be careful. +// +//////////////////////////////////////////////////////////////////////////////// + +struct Image; +struct ImageFrame; +struct ImagePlacement; + +KHASH_MAP_INIT_INT(id2image, struct Image *) +KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *) + +typedef struct ImageFrame { + /// The image this frame belongs to. + struct Image *image; + /// The 1-based index of the frame. Zero if the frame isn't initialized. + int index; + /// The last time when the frame was displayed or otherwise touched. + Milliseconds atime; + /// The background color of the frame in the 0xRRGGBBAA format. + uint32_t background_color; + /// The index of the background frame. Zero to use the color instead. + int background_frame_index; + /// The duration of the frame in milliseconds. + int gap; + /// The expected size of the frame image file (specified with 'S='), + /// used to check if uploading succeeded. + unsigned expected_size; + /// Format specification (see the `f=` key). + int format; + /// Pixel width and height of the non-composed (on-disk) frame data. May + /// differ from the image (i.e. first frame) dimensions. + int data_pix_width, data_pix_height; + /// The offset of the frame relative to the first frame. + int x, y; + /// Compression mode (see the `o=` key). + char compression; + /// The status (see `ImageStatus`). + char status; + /// The reason of uploading failure (see `ImageUploadingFailure`). + char uploading_failure; + /// Whether failures and successes should be reported ('q='). + char quiet; + /// Whether to blend the frame with the background or replace it. + char blend; + /// The file corresponding to the on-disk cache, used when uploading. + FILE *open_file; + /// The size of the corresponding file cached on disk. + unsigned disk_size; + /// The imlib object containing the fully composed frame. It's not + /// scaled for screen display yet. + Imlib_Image imlib_object; +} ImageFrame; + +typedef struct Image { + /// The client id (the one specified with 'i='). Must be nonzero. + uint32_t image_id; + /// The client id specified in the query command (`a=q`). This one must + /// be used to create the response if it's non-zero. + uint32_t query_id; + /// The number specified in the transmission command (`I=`). If + /// non-zero, it may be used to identify the image instead of the + /// image_id, and it also should be mentioned in responses. + uint32_t image_number; + /// The last time when the image was displayed or otherwise touched. + Milliseconds atime; + /// The total duration of the animation in milliseconds. + int total_duration; + /// The total size of cached image files for all frames. + int total_disk_size; + /// The global index of the creation command. Used to decide which image + /// is newer if they have the same image number. + uint64_t global_command_index; + /// The 1-based index of the currently displayed frame. + int current_frame; + /// The state of the animation, see `AnimationState`. + char animation_state; + /// The absolute time that is assumed to be the start of the current + /// frame (in ms since initialization). + Milliseconds current_frame_time; + /// The absolute time of the last redraw (in ms since initialization). + /// Used to check whether it's the first time we draw the image in the + /// current redraw cycle. + Milliseconds last_redraw; + /// The absolute time of the next redraw (in ms since initialization). + /// 0 means no redraw is scheduled. + Milliseconds next_redraw; + /// The unscaled pixel width and height of the image. Usually inherited + /// from the first frame. + int pix_width, pix_height; + /// The first frame. + ImageFrame first_frame; + /// The array of frames beyond the first one. + kvec_t(ImageFrame) frames_beyond_the_first; + /// Image placements. + khash_t(id2placement) *placements; + /// The default placement. + uint32_t default_placement; + /// The initial placement id, specified with the transmission command, + /// used to report success or failure. + uint32_t initial_placement_id; +} Image; + +typedef struct ImagePlacement { + /// The image this placement belongs to. + Image *image; + /// The id of the placement. Must be nonzero. + uint32_t placement_id; + /// The last time when the placement was displayed or otherwise touched. + Milliseconds atime; + /// The 1-based index of the protected pixmap. We protect a pixmap in + /// gr_load_pixmap to avoid unloading it right after it was loaded. + int protected_frame; + /// Whether the placement is used only for Unicode placeholders. + char virtual; + /// The scaling mode (see `ScaleMode`). + char scale_mode; + /// Height and width in cells. + uint16_t rows, cols; + /// Top-left corner of the source rectangle ('x=' and 'y='). + int src_pix_x, src_pix_y; + /// Height and width of the source rectangle (zero if full image). + int src_pix_width, src_pix_height; + /// The image appropriately scaled and uploaded to the X server. This + /// pixmap is premultiplied by alpha. + Pixmap first_pixmap; + /// The array of pixmaps beyond the first one. + kvec_t(Pixmap) pixmaps_beyond_the_first; + /// The dimensions of the cell used to scale the image. If cell + /// dimensions are changed (font change), the image will be rescaled. + uint16_t scaled_cw, scaled_ch; + /// If true, do not move the cursor when displaying this placement + /// (non-virtual placements only). + char do_not_move_cursor; +} ImagePlacement; + +/// A rectangular piece of an image to be drawn. +typedef struct { + uint32_t image_id; + uint32_t placement_id; + /// The position of the rectangle in pixels. + int screen_x_pix, screen_y_pix; + /// The starting row on the screen. + int screen_y_row; + /// The part of the whole image to be drawn, in cells. Starts are + /// zero-based, ends are exclusive. + int img_start_col, img_end_col, img_start_row, img_end_row; + /// The current cell width and height in pixels. + int cw, ch; + /// Whether colors should be inverted. + int reverse; +} ImageRect; + +/// Executes `code` for each frame of an image. Example: +/// +/// foreach_frame(image, frame, { +/// printf("Frame %d\n", frame->index); +/// }); +/// +#define foreach_frame(image, framevar, code) { size_t __i; \ + for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \ + ImageFrame *framevar = \ + __i == 0 ? &(image).first_frame \ + : &kv_A((image).frames_beyond_the_first, __i - 1); \ + code; \ + } } + +/// Executes `code` for each pixmap of a placement. Example: +/// +/// foreach_pixmap(placement, pixmap, { +/// ... +/// }); +/// +#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \ + for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \ + Pixmap pixmapvar = \ + __i == 0 ? (placement).first_pixmap \ + : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \ + code; \ + } } + + +static Image *gr_find_image(uint32_t image_id); +static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len); +static void gr_delete_image(Image *img); +static void gr_check_limits(); +static char *gr_base64dec(const char *src, size_t *size); +static void sanitize_str(char *str, size_t max_len); +static const char *sanitized_filename(const char *str); + +/// The array of image rectangles to draw. It is reset each frame. +static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}}; +/// The known images (including the ones being uploaded). +static khash_t(id2image) *images = NULL; +/// The total number of placements in all images. +static unsigned total_placement_count = 0; +/// The total size of all image files stored in the on-disk cache. +static int64_t images_disk_size = 0; +/// The total size of all images and placements loaded into ram. +static int64_t images_ram_size = 0; +/// The id of the last loaded image. +static uint32_t last_image_id = 0; +/// Current cell width and heigh in pixels. +static int current_cw = 0, current_ch = 0; +/// The id of the currently uploaded image (when using direct uploading). +static uint32_t current_upload_image_id = 0; +/// The index of the frame currently being uploaded. +static int current_upload_frame_index = 0; +/// The time when the graphics module was initialized. +static struct timespec initialization_time = {0}; +/// The time when the current frame drawing started, used for debugging fps and +/// to calculate the current frame for animations. +static Milliseconds drawing_start_time; +/// The global index of the current command. +static uint64_t global_command_counter = 0; +/// The next redraw times for each row of the terminal. Used for animations. +/// 0 means no redraw is scheduled. +static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL}; +/// The number of files loaded in the current redraw cycle. +static int this_redraw_cycle_loaded_files = 0; +/// The number of pixmaps loaded in the current redraw cycle. +static int this_redraw_cycle_loaded_pixmaps = 0; + +/// The directory where the cache files are stored. +static char cache_dir[MAX_FILENAME_SIZE - 16]; + +/// The table used for color inversion. +static unsigned char reverse_table[256]; + +// Declared in the header. +GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE; +char graphics_display_images = 1; +GraphicsCommandResult graphics_command_result = {0}; +int graphics_next_redraw_delay = INT_MAX; + +// Defined in config.h +extern const char graphics_cache_dir_template[]; +extern unsigned graphics_max_single_image_file_size; +extern unsigned graphics_total_file_cache_size; +extern unsigned graphics_max_single_image_ram_size; +extern unsigned graphics_max_total_ram_size; +extern unsigned graphics_max_total_placements; +extern double graphics_excess_tolerance_ratio; +extern unsigned graphics_animation_min_delay; + + +//////////////////////////////////////////////////////////////////////////////// +// Basic helpers. +//////////////////////////////////////////////////////////////////////////////// + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) < (b) ? (b) : (a)) + +/// Returns the difference between `end` and `start` in milliseconds. +static int64_t gr_timediff_ms(const struct timespec *end, + const struct timespec *start) { + return (end->tv_sec - start->tv_sec) * 1000 + + (end->tv_nsec - start->tv_nsec) / 1000000; +} + +/// Returns the current time in milliseconds since the initialization. +static Milliseconds gr_now_ms() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return gr_timediff_ms(&now, &initialization_time); +} + +//////////////////////////////////////////////////////////////////////////////// +// Logging. +//////////////////////////////////////////////////////////////////////////////// + +#define GR_LOG(...) \ + do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0) + +//////////////////////////////////////////////////////////////////////////////// +// Basic image management functions (create, delete, find, etc). +//////////////////////////////////////////////////////////////////////////////// + +/// Returns the 1-based index of the last frame. Note that you may want to use +/// `gr_last_uploaded_frame_index` instead since the last frame may be not +/// fully uploaded yet. +static inline int gr_last_frame_index(Image *img) { + return kv_size(img->frames_beyond_the_first) + 1; +} + +/// Returns the frame with the given index. Returns NULL if the index is out of +/// bounds. The index is 1-based. +static ImageFrame *gr_get_frame(Image *img, int index) { + if (!img) + return NULL; + if (index == 1) + return &img->first_frame; + if (2 <= index && index <= gr_last_frame_index(img)) + return &kv_A(img->frames_beyond_the_first, index - 2); + return NULL; +} + +/// Returns the last frame of the image. Returns NULL if `img` is NULL. +static ImageFrame *gr_get_last_frame(Image *img) { + if (!img) + return NULL; + return gr_get_frame(img, gr_last_frame_index(img)); +} + +/// Returns the 1-based index of the last frame or the second-to-last frame if +/// the last frame is not fully uploaded yet. +static inline int gr_last_uploaded_frame_index(Image *img) { + int last_index = gr_last_frame_index(img); + if (last_index > 1 && + gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS) + return last_index - 1; + return last_index; +} + +/// Returns the pixmap for the frame with the given index. Returns 0 if the +/// index is out of bounds. The index is 1-based. +static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) { + if (index == 1) + return placement->first_pixmap; + if (2 <= index && + index <= kv_size(placement->pixmaps_beyond_the_first) + 1) + return kv_A(placement->pixmaps_beyond_the_first, index - 2); + return 0; +} + +/// Sets the pixmap for the frame with the given index. The index is 1-based. +/// The array of pixmaps is resized if needed. +static void gr_set_frame_pixmap(ImagePlacement *placement, int index, + Pixmap pixmap) { + if (index == 1) { + placement->first_pixmap = pixmap; + return; + } + // Resize the array if needed. + size_t old_size = kv_size(placement->pixmaps_beyond_the_first); + if (old_size < index - 1) { + kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2); + for (size_t i = old_size; i < index - 1; i++) + kv_A(placement->pixmaps_beyond_the_first, i) = 0; + } + kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap; +} + +/// Finds the image corresponding to the client id. Returns NULL if cannot find. +static Image *gr_find_image(uint32_t image_id) { + khiter_t k = kh_get(id2image, images, image_id); + if (k == kh_end(images)) + return NULL; + Image *res = kh_value(images, k); + return res; +} + +/// Finds the newest image corresponding to the image number. Returns NULL if +/// cannot find. +static Image *gr_find_image_by_number(uint32_t image_number) { + if (image_number == 0) + return NULL; + Image *newest_img = NULL; + Image *img = NULL; + kh_foreach_value(images, img, { + if (img->image_number == image_number && + (!newest_img || newest_img->global_command_index < + img->global_command_index)) + newest_img = img; + }); + if (!newest_img) + GR_LOG("Image number %u not found\n", image_number); + else + GR_LOG("Found image number %u, its id is %u\n", image_number, + img->image_id); + return newest_img; +} + +/// Finds the placement corresponding to the id. If the placement id is 0, +/// returns some default placement. +static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) { + if (!img) + return NULL; + if (placement_id == 0) { + // Try to get the default placement. + ImagePlacement *dflt = NULL; + if (img->default_placement != 0) + dflt = gr_find_placement(img, img->default_placement); + if (dflt) + return dflt; + // If there is no default placement, return the first one and + // set it as the default. + kh_foreach_value(img->placements, dflt, { + img->default_placement = dflt->placement_id; + return dflt; + }); + // If there are no placements, return NULL. + return NULL; + } + khiter_t k = kh_get(id2placement, img->placements, placement_id); + if (k == kh_end(img->placements)) + return NULL; + ImagePlacement *res = kh_value(img->placements, k); + return res; +} + +/// Finds the placement by image id and placement id. +static ImagePlacement *gr_find_image_and_placement(uint32_t image_id, + uint32_t placement_id) { + return gr_find_placement(gr_find_image(image_id), placement_id); +} + +/// Writes the name of the on-disk cache file to `out`. `max_len` should be the +/// size of `out`. The name will be something like +/// "/tmp/st-images-xxx/img-ID-FRAME". +static void gr_get_frame_filename(ImageFrame *frame, char *out, + size_t max_len) { + snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir, + frame->image->image_id, frame->index); +} + +/// Returns the (estimation) of the RAM size used by the frame right now. +static unsigned gr_frame_current_ram_size(ImageFrame *frame) { + if (!frame->imlib_object) + return 0; + return (unsigned)frame->image->pix_width * frame->image->pix_height * 4; +} + +/// Returns the (estimation) of the RAM size used by a single frame pixmap. +static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) { + return (unsigned)placement->rows * placement->cols * + placement->scaled_ch * placement->scaled_cw * 4; +} + +/// Returns the (estimation) of the RAM size used by the placemenet right now. +static unsigned gr_placement_current_ram_size(ImagePlacement *placement) { + unsigned single_frame_size = + gr_placement_single_frame_ram_size(placement); + unsigned result = 0; + foreach_pixmap(*placement, pixmap, { + if (pixmap) + result += single_frame_size; + }); + return result; +} + +/// Unload the frame from RAM (i.e. delete the corresponding imlib object). +/// If the on-disk file of the frame is preserved, it can be reloaded later. +static void gr_unload_frame(ImageFrame *frame) { + if (!frame->imlib_object) + return; + + unsigned frame_ram_size = gr_frame_current_ram_size(frame); + images_ram_size -= frame_ram_size; + + imlib_context_set_image(frame->imlib_object); + imlib_free_image_and_decache(); + frame->imlib_object = NULL; + + GR_LOG("After unloading image %u frame %u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + frame->image->image_id, frame->index, + drawing_start_time - frame->atime, images_ram_size / 1024, + frame_ram_size / 1024); +} + +/// Unload all frames of the image. +static void gr_unload_all_frames(Image *img) { + foreach_frame(*img, frame, { + gr_unload_frame(frame); + }); +} + +/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps). +/// If the on-disk files or imlib objects of the corresponding image are +/// preserved, the placement can be reloaded later. +static void gr_unload_placement(ImagePlacement *placement) { + unsigned placement_ram_size = gr_placement_current_ram_size(placement); + images_ram_size -= placement_ram_size; + + Display *disp = imlib_context_get_display(); + foreach_pixmap(*placement, pixmap, { + if (pixmap) + XFreePixmap(disp, pixmap); + }); + + placement->first_pixmap = 0; + placement->pixmaps_beyond_the_first.n = 0; + placement->scaled_ch = placement->scaled_cw = 0; + + GR_LOG("After unloading placement %u/%u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + placement->image->image_id, placement->placement_id, + drawing_start_time - placement->atime, images_ram_size / 1024, + placement_ram_size / 1024); +} + +/// Unload a single pixmap of the placement from RAM. +static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) { + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); + if (!pixmap) + return; + + Display *disp = imlib_context_get_display(); + XFreePixmap(disp, pixmap); + gr_set_frame_pixmap(placement, frameidx, 0); + images_ram_size -= gr_placement_single_frame_ram_size(placement); + + GR_LOG("After unloading pixmap %ld of " + "placement %u/%u (atime %ld ms ago) " + "frame %u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + pixmap, placement->image->image_id, placement->placement_id, + drawing_start_time - placement->atime, frameidx, + drawing_start_time - + gr_get_frame(placement->image, frameidx)->atime, + images_ram_size / 1024, + gr_placement_single_frame_ram_size(placement) / 1024); +} + +/// Deletes the on-disk cache file corresponding to the frame. The in-ram image +/// object (if it exists) is not deleted, placements are not unloaded either. +static void gr_delete_imagefile(ImageFrame *frame) { + // It may still be being loaded. Close the file in this case. + if (frame->open_file) { + fclose(frame->open_file); + frame->open_file = NULL; + } + + if (frame->disk_size == 0) + return; + + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + remove(filename); + + unsigned disk_size = frame->disk_size; + images_disk_size -= disk_size; + frame->image->total_disk_size -= disk_size; + frame->disk_size = 0; + + GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) " + "disk: %ld KiB (- %u KiB)\n", + frame->image->image_id, frame->index, + drawing_start_time - frame->atime, images_disk_size / 1024, + disk_size / 1024); +} + +/// Deletes all on-disk cache files of the image (for each frame). +static void gr_delete_imagefiles(Image *img) { + foreach_frame(*img, frame, { + gr_delete_imagefile(frame); + }); +} + +/// Deletes the given placement: unloads, frees the object, but doesn't change +/// the `placements` hash table. +static void gr_delete_placement_keep_id(ImagePlacement *placement) { + if (!placement) + return; + GR_LOG("Deleting placement %u/%u\n", placement->image->image_id, + placement->placement_id); + gr_unload_placement(placement); + kv_destroy(placement->pixmaps_beyond_the_first); + free(placement); + total_placement_count--; +} + +/// Deletes all placements of `img`. +static void gr_delete_all_placements(Image *img) { + ImagePlacement *placement = NULL; + kh_foreach_value(img->placements, placement, { + gr_delete_placement_keep_id(placement); + }); + kh_clear(id2placement, img->placements); +} + +/// Deletes the given image: unloads, deletes the file, frees the Image object, +/// but doesn't change the `images` hash table. +static void gr_delete_image_keep_id(Image *img) { + if (!img) + return; + GR_LOG("Deleting image %u\n", img->image_id); + foreach_frame(*img, frame, { + gr_delete_imagefile(frame); + gr_unload_frame(frame); + }); + kv_destroy(img->frames_beyond_the_first); + gr_delete_all_placements(img); + kh_destroy(id2placement, img->placements); + free(img); +} + +/// Deletes the given image: unloads, deletes the file, frees the Image object, +/// and also removes it from `images`. +static void gr_delete_image(Image *img) { + if (!img) + return; + uint32_t id = img->image_id; + gr_delete_image_keep_id(img); + khiter_t k = kh_get(id2image, images, id); + kh_del(id2image, images, k); +} + +/// Deletes the given placement: unloads, frees the object, and also removes it +/// from `placements`. +static void gr_delete_placement(ImagePlacement *placement) { + if (!placement) + return; + uint32_t id = placement->placement_id; + Image *img = placement->image; + gr_delete_placement_keep_id(placement); + khiter_t k = kh_get(id2placement, img->placements, id); + kh_del(id2placement, img->placements, k); +} + +/// Deletes all images and clears `images`. +static void gr_delete_all_images() { + Image *img = NULL; + kh_foreach_value(images, img, { + gr_delete_image_keep_id(img); + }); + kh_clear(id2image, images); +} + +/// Update the atime of the image. +static void gr_touch_image(Image *img) { + img->atime = gr_now_ms(); +} + +/// Update the atime of the frame. +static void gr_touch_frame(ImageFrame *frame) { + frame->image->atime = frame->atime = gr_now_ms(); +} + +/// Update the atime of the placement. Touches the images too. +static void gr_touch_placement(ImagePlacement *placement) { + placement->image->atime = placement->atime = gr_now_ms(); +} + +/// Creates a new image with the given id. If an image with that id already +/// exists, it is deleted first. If the provided id is 0, generates a +/// random id. +static Image *gr_new_image(uint32_t id) { + if (id == 0) { + do { + id = rand(); + // Avoid IDs that don't need full 32 bits. + } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 || + gr_find_image(id)); + GR_LOG("Generated random image id %u\n", id); + } + Image *img = gr_find_image(id); + gr_delete_image_keep_id(img); + GR_LOG("Creating image %u\n", id); + img = malloc(sizeof(Image)); + memset(img, 0, sizeof(Image)); + img->placements = kh_init(id2placement); + int ret; + khiter_t k = kh_put(id2image, images, id, &ret); + kh_value(images, k) = img; + img->image_id = id; + gr_touch_image(img); + img->global_command_index = global_command_counter; + return img; +} + +/// Creates a new frame at the end of the frame array. It may be the first frame +/// if there are no frames yet. +static ImageFrame *gr_append_new_frame(Image *img) { + ImageFrame *frame = NULL; + if (img->first_frame.index == 0 && + kv_size(img->frames_beyond_the_first) == 0) { + frame = &img->first_frame; + frame->index = 1; + } else { + frame = kv_pushp(ImageFrame, img->frames_beyond_the_first); + memset(frame, 0, sizeof(ImageFrame)); + frame->index = kv_size(img->frames_beyond_the_first) + 1; + } + frame->image = img; + gr_touch_frame(frame); + GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id); + return frame; +} + +/// Creates a new placement with the given id. If a placement with that id +/// already exists, it is deleted first. If the provided id is 0, generates a +/// random id. +static ImagePlacement *gr_new_placement(Image *img, uint32_t id) { + if (id == 0) { + do { + // Currently we support only 24-bit IDs. + id = rand() & 0xFFFFFF; + // Avoid IDs that need only one byte. + } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id)); + } + ImagePlacement *placement = gr_find_placement(img, id); + gr_delete_placement_keep_id(placement); + GR_LOG("Creating placement %u/%u\n", img->image_id, id); + placement = malloc(sizeof(ImagePlacement)); + memset(placement, 0, sizeof(ImagePlacement)); + total_placement_count++; + int ret; + khiter_t k = kh_put(id2placement, img->placements, id, &ret); + kh_value(img->placements, k) = placement; + placement->image = img; + placement->placement_id = id; + gr_touch_placement(placement); + if (img->default_placement == 0) + img->default_placement = id; + return placement; +} + +static int64_t ceil_div(int64_t a, int64_t b) { + return (a + b - 1) / b; +} + +/// Computes the best number of rows and columns for a placement if it's not +/// specified, and also adjusts the source rectangle size. +static void gr_infer_placement_size_maybe(ImagePlacement *placement) { + // The size of the image. + int image_pix_width = placement->image->pix_width; + int image_pix_height = placement->image->pix_height; + // Negative values are not allowed. Quietly set them to 0. + if (placement->src_pix_x < 0) + placement->src_pix_x = 0; + if (placement->src_pix_y < 0) + placement->src_pix_y = 0; + if (placement->src_pix_width < 0) + placement->src_pix_width = 0; + if (placement->src_pix_height < 0) + placement->src_pix_height = 0; + // If the source rectangle is outside the image, truncate it. + if (placement->src_pix_x > image_pix_width) + placement->src_pix_x = image_pix_width; + if (placement->src_pix_y > image_pix_height) + placement->src_pix_y = image_pix_height; + // If the source rectangle is not specified, use the whole image. If + // it's partially outside the image, truncate it. + if (placement->src_pix_width == 0 || + placement->src_pix_x + placement->src_pix_width > image_pix_width) + placement->src_pix_width = + image_pix_width - placement->src_pix_x; + if (placement->src_pix_height == 0 || + placement->src_pix_y + placement->src_pix_height > image_pix_height) + placement->src_pix_height = + image_pix_height - placement->src_pix_y; + + if (placement->cols != 0 && placement->rows != 0) + return; + if (placement->src_pix_width == 0 || placement->src_pix_height == 0) + return; + if (current_cw == 0 || current_ch == 0) + return; + + // If no size is specified, use the image size. + if (placement->cols == 0 && placement->rows == 0) { + placement->cols = + ceil_div(placement->src_pix_width, current_cw); + placement->rows = + ceil_div(placement->src_pix_height, current_ch); + return; + } + + // Some applications specify only one of the dimensions. + if (placement->scale_mode == SCALE_MODE_CONTAIN) { + // If we preserve aspect ratio and fit to width/height, the most + // logical thing is to find the minimum size of the + // non-specified dimension that allows the image to fit the + // specified dimension. + if (placement->cols == 0) { + placement->cols = ceil_div( + placement->src_pix_width * placement->rows * + current_ch, + placement->src_pix_height * current_cw); + return; + } + if (placement->rows == 0) { + placement->rows = + ceil_div(placement->src_pix_height * + placement->cols * current_cw, + placement->src_pix_width * current_ch); + return; + } + } else { + // Otherwise we stretch the image or preserve the original size. + // In both cases we compute the best number of columns from the + // pixel size and cell size. + // TODO: In the case of stretching it's not the most logical + // thing to do, may need to revisit in the future. + // Currently we switch to SCALE_MODE_CONTAIN when only one + // of the dimensions is specified, so this case shouldn't + // happen in practice. + if (!placement->cols) + placement->cols = + ceil_div(placement->src_pix_width, current_cw); + if (!placement->rows) + placement->rows = + ceil_div(placement->src_pix_height, current_ch); + } +} + +/// Adjusts the current frame index if enough time has passed since the display +/// of the current frame. Also computes the time of the next redraw of this +/// image (`img->next_redraw`). The current time is passed as an argument so +/// that all animations are in sync. +static void gr_update_frame_index(Image *img, Milliseconds now) { + if (img->current_frame == 0) { + img->current_frame_time = now; + img->current_frame = 1; + img->next_redraw = now + MAX(1, img->first_frame.gap); + return; + } + // If the animation is stopped, show the current frame. + if (!img->animation_state || + img->animation_state == ANIMATION_STATE_STOPPED || + img->animation_state == ANIMATION_STATE_UNSET) { + // The next redraw is never (unless the state is changed). + img->next_redraw = 0; + return; + } + int last_uploaded_frame_index = gr_last_uploaded_frame_index(img); + // If we are loading and we reached the last frame, show the last frame. + if (img->animation_state == ANIMATION_STATE_LOADING && + img->current_frame == last_uploaded_frame_index) { + // The next redraw is never (unless the state is changed or + // frames are added). + img->next_redraw = 0; + return; + } + + // Check how many milliseconds passed since the current frame was shown. + int passed_ms = now - img->current_frame_time; + // If the animation is looping and too much time has passes, we can + // make a shortcut. + if (img->animation_state == ANIMATION_STATE_LOOPING && + img->total_duration > 0 && passed_ms >= img->total_duration) { + passed_ms %= img->total_duration; + img->current_frame_time = now - passed_ms; + } + // Find the next frame. + int original_frame_index = img->current_frame; + while (1) { + ImageFrame *frame = gr_get_frame(img, img->current_frame); + if (!frame) { + // The frame doesn't exist, go to the first frame. + img->current_frame = 1; + img->current_frame_time = now; + img->next_redraw = now + MAX(1, img->first_frame.gap); + return; + } + if (frame->gap >= 0 && passed_ms < frame->gap) { + // Not enough time has passed, we are still in the same + // frame, and it's not a gapless frame. + img->next_redraw = + img->current_frame_time + MAX(1, frame->gap); + return; + } + // Otherwise go to the next frame. + passed_ms -= MAX(0, frame->gap); + if (img->current_frame >= last_uploaded_frame_index) { + // It's the last frame, if the animation is loading, + // remain on it. + if (img->animation_state == ANIMATION_STATE_LOADING) { + img->next_redraw = 0; + return; + } + // Otherwise the animation is looping. + img->current_frame = 1; + // TODO: Support finite number of loops. + } else { + img->current_frame++; + } + // Make sure we don't get stuck in an infinite loop. + if (img->current_frame == original_frame_index) { + // We looped through all frames, but haven't reached the + // next frame yet. This may happen if too much time has + // passed since the last redraw or all the frames are + // gapless. Just move on to the next frame. + img->current_frame++; + if (img->current_frame > + last_uploaded_frame_index) + img->current_frame = 1; + img->current_frame_time = now; + img->next_redraw = now + MAX( + 1, gr_get_frame(img, img->current_frame)->gap); + return; + } + // Adjust the start time of the frame. The next redraw time will + // be set in the next iteration. + img->current_frame_time += MAX(0, frame->gap); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Unloading and deleting images to save resources. +//////////////////////////////////////////////////////////////////////////////// + +/// A helper to compare frames by atime for qsort. +static int gr_cmp_frames_by_atime(const void *a, const void *b) { + ImageFrame *frame_a = *(ImageFrame *const *)a; + ImageFrame *frame_b = *(ImageFrame *const *)b; + if (frame_a->atime == frame_b->atime) + return frame_a->image->global_command_index - + frame_b->image->global_command_index; + return frame_a->atime - frame_b->atime; +} + +/// A helper to compare images by atime for qsort. +static int gr_cmp_images_by_atime(const void *a, const void *b) { + Image *img_a = *(Image *const *)a; + Image *img_b = *(Image *const *)b; + if (img_a->atime == img_b->atime) + return img_a->global_command_index - + img_b->global_command_index; + return img_a->atime - img_b->atime; +} + +/// A helper to compare placements by atime for qsort. +static int gr_cmp_placements_by_atime(const void *a, const void *b) { + ImagePlacement *p_a = *(ImagePlacement **)a; + ImagePlacement *p_b = *(ImagePlacement **)b; + if (p_a->atime == p_b->atime) + return p_a->image->global_command_index - + p_b->image->global_command_index; + return p_a->atime - p_b->atime; +} + +typedef kvec_t(Image *) ImageVec; +typedef kvec_t(ImagePlacement *) ImagePlacementVec; +typedef kvec_t(ImageFrame *) ImageFrameVec; + +/// Returns an array of pointers to all images sorted by atime. +static ImageVec gr_get_images_sorted_by_atime() { + ImageVec vec; + kv_init(vec); + if (kh_size(images) == 0) + return vec; + kv_resize(Image *, vec, kh_size(images)); + Image *img = NULL; + kh_foreach_value(images, img, { kv_push(Image *, vec, img); }); + qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime); + return vec; +} + +/// Returns an array of pointers to all placements sorted by atime. +static ImagePlacementVec gr_get_placements_sorted_by_atime() { + ImagePlacementVec vec; + kv_init(vec); + if (total_placement_count == 0) + return vec; + kv_resize(ImagePlacement *, vec, total_placement_count); + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + kh_foreach_value(img->placements, placement, { + kv_push(ImagePlacement *, vec, placement); + }); + }); + qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *), + gr_cmp_placements_by_atime); + return vec; +} + +/// Returns an array of pointers to all frames sorted by atime. +static ImageFrameVec gr_get_frames_sorted_by_atime() { + ImageFrameVec frames; + kv_init(frames); + Image *img = NULL; + kh_foreach_value(images, img, { + foreach_frame(*img, frame, { + kv_push(ImageFrame *, frames, frame); + }); + }); + qsort(frames.a, kv_size(frames), sizeof(ImageFrame *), + gr_cmp_frames_by_atime); + return frames; +} + +/// An object that can be unloaded from RAM. +typedef struct { + /// Some score, probably based on access time. The lower the score, the + /// more likely that the object should be unloaded. + int64_t score; + union { + ImagePlacement *placement; + ImageFrame *frame; + }; + /// If zero, the object is the imlib object of `frame`, if non-zero, + /// the object is a pixmap of `frameidx`-th frame of `placement`. + int frameidx; +} UnloadableObject; + +typedef kvec_t(UnloadableObject) UnloadableObjectVec; + +/// A helper to compare unloadable objects by score for qsort. +static int gr_cmp_unloadable_objects(const void *a, const void *b) { + UnloadableObject *obj_a = (UnloadableObject *)a; + UnloadableObject *obj_b = (UnloadableObject *)b; + return obj_a->score - obj_b->score; +} + +/// Unloads an unloadable object from RAM. +static void gr_unload_object(UnloadableObject *obj) { + if (obj->frameidx) { + if (obj->placement->protected_frame == obj->frameidx) + return; + gr_unload_pixmap(obj->placement, obj->frameidx); + } else { + gr_unload_frame(obj->frame); + } +} + +/// Returns the recency threshold for an image. Frames that were accessed within +/// this threshold from now are considered recent and may be handled +/// differently because we may need them again very soon. +static Milliseconds gr_recency_threshold(Image *img) { + return img->total_duration * 2 + 1000; +} + +/// Creates an unloadable object for the imlib object of a frame. +static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now, + ImageFrame *frame) { + UnloadableObject obj = {0}; + obj.frameidx = 0; + obj.frame = frame; + Milliseconds atime = frame->atime; + obj.score = atime; + if (atime >= now - gr_recency_threshold(frame->image)) { + // This is a recent frame, probably from an active animation. + // Score it above `now` to prefer unloading non-active frames. + // Randomize the score because it's not very clear in which + // order we want to unload them: reloading a frame may require + // reloading other frames. + obj.score = now + 1000 + rand() % 1000; + } + return obj; +} + +/// Creates an unloadable object for a pixmap. +static UnloadableObject +gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame, + ImagePlacement *placement) { + UnloadableObject obj = {0}; + obj.frameidx = frame->index; + obj.placement = placement; + obj.score = placement->atime; + // Since we don't store pixmap atimes, use the + // oldest atime of the frame and the placement. + Milliseconds atime = MIN(placement->atime, frame->atime); + obj.score = atime; + if (atime >= now - gr_recency_threshold(frame->image)) { + // This is a recent pixmap, probably from an active animation. + // Score it above `now` to prefer unloading non-active frames. + // Also assign higher scores to frames that are closer to the + // current frame (more likely to be used soon). + int num_frames = gr_last_frame_index(frame->image); + int dist = frame->index - frame->image->current_frame; + if (dist < 0) + dist += num_frames; + obj.score = + now + 1000 + (num_frames - dist) * 1000 / num_frames; + // If the pixmap is much larger than the imlib image, prefer to + // unload the pixmap by adding up to -1000 to the score. If the + // imlib image is larger, add up to +1000. + float imlib_size = gr_frame_current_ram_size(frame); + float pixmap_size = + gr_placement_single_frame_ram_size(placement); + obj.score += + 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5); + } + return obj; +} + +/// Returns an array of unloadable objects sorted by score. +static UnloadableObjectVec +gr_get_unloadable_objects_sorted_by_score(Milliseconds now) { + UnloadableObjectVec objects; + kv_init(objects); + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + foreach_frame(*img, frame, { + if (!frame->imlib_object) + continue; + kv_push(UnloadableObject, objects, + gr_unloadable_object_for_frame(now, frame)); + int frameidx = frame->index; + kh_foreach_value(img->placements, placement, { + if (!gr_get_frame_pixmap(placement, frameidx)) + continue; + kv_push(UnloadableObject, objects, + gr_unloadable_object_for_pixmap( + now, frame, placement)); + }); + }); + }); + qsort(objects.a, kv_size(objects), sizeof(UnloadableObject), + gr_cmp_unloadable_objects); + return objects; +} + +/// Returns the limit adjusted by the excess tolerance ratio. +static inline unsigned apply_tolerance(unsigned limit) { + return limit + (unsigned)(limit * graphics_excess_tolerance_ratio); +} + +/// Checks RAM and disk cache limits and deletes/unloads some images. +static void gr_check_limits() { + Milliseconds now = gr_now_ms(); + ImageVec images_sorted = {0}; + ImagePlacementVec placements_sorted = {0}; + ImageFrameVec frames_sorted = {0}; + UnloadableObjectVec objects_sorted = {0}; + int images_begin = 0; + int placements_begin = 0; + char changed = 0; + // First reduce the number of images if there are too many. + if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) { + GR_LOG("Too many images: %d\n", kh_size(images)); + changed = 1; + images_sorted = gr_get_images_sorted_by_atime(); + int to_delete = kv_size(images_sorted) - + graphics_max_total_placements; + for (; images_begin < to_delete; images_begin++) + gr_delete_image(images_sorted.a[images_begin]); + } + // Then reduce the number of placements if there are too many. + if (total_placement_count > + apply_tolerance(graphics_max_total_placements)) { + GR_LOG("Too many placements: %d\n", total_placement_count); + changed = 1; + placements_sorted = gr_get_placements_sorted_by_atime(); + int to_delete = kv_size(placements_sorted) - + graphics_max_total_placements; + for (; placements_begin < to_delete; placements_begin++) { + ImagePlacement *placement = + placements_sorted.a[placements_begin]; + if (placement->protected_frame) + break; + gr_delete_placement(placement); + } + } + // Then reduce the size of the image file cache. The files correspond to + // image frames. + if (images_disk_size > + apply_tolerance(graphics_total_file_cache_size)) { + GR_LOG("Too big disk cache: %ld KiB\n", + images_disk_size / 1024); + changed = 1; + frames_sorted = gr_get_frames_sorted_by_atime(); + for (int i = 0; i < kv_size(frames_sorted); i++) { + if (images_disk_size <= graphics_total_file_cache_size) + break; + gr_delete_imagefile(kv_A(frames_sorted, i)); + } + } + // Then unload images from RAM. + if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) { + changed = 1; + int frames_begin = 0; + GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024); + objects_sorted = gr_get_unloadable_objects_sorted_by_score(now); + for (int i = 0; i < kv_size(objects_sorted); i++) { + if (images_ram_size <= graphics_max_total_ram_size) + break; + gr_unload_object(&kv_A(objects_sorted, i)); + } + } + if (changed) { + GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " + "img count: %d placement count: %d\n", + images_ram_size / 1024, images_disk_size / 1024, + kh_size(images), total_placement_count); + } + kv_destroy(images_sorted); + kv_destroy(placements_sorted); + kv_destroy(frames_sorted); + kv_destroy(objects_sorted); +} + +/// Unloads all images by user request. +void gr_unload_images_to_reduce_ram() { + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + kh_foreach_value(img->placements, placement, { + if (placement->protected_frame) + continue; + gr_unload_placement(placement); + }); + gr_unload_all_frames(img); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Image loading. +//////////////////////////////////////////////////////////////////////////////// + +/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2 +/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted +/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout +/// on little-endian architectures). +static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format, + size_t num_pixels) { + size_t pixel_size = format == 24 ? 3 : 4; + if (format == 32) { + for (unsigned i = 0; i < num_pixels; ++i) { + unsigned byte_i = i * pixel_size; + to[i] = ((DATA32)from[byte_i + 2]) | + ((DATA32)from[byte_i + 1]) << 8 | + ((DATA32)from[byte_i]) << 16 | + ((DATA32)from[byte_i + 3]) << 24; + } + } else { + for (unsigned i = 0; i < num_pixels; ++i) { + unsigned byte_i = i * pixel_size; + to[i] = ((DATA32)from[byte_i + 2]) | + ((DATA32)from[byte_i + 1]) << 8 | + ((DATA32)from[byte_i]) << 16 | 0xFF000000; + } + } +} + +/// Loads uncompressed RGB or RGBA image data from a file. +static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file, + int format, + size_t total_pixels) { + unsigned char chunk[BUFSIZ]; + size_t pixel_size = format == 24 ? 3 : 4; + size_t chunk_size_pix = BUFSIZ / 4; + size_t chunk_size_bytes = chunk_size_pix * pixel_size; + size_t bytes = total_pixels * pixel_size; + for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels; + chunk_start_pix += chunk_size_pix) { + size_t read_size = fread(chunk, 1, chunk_size_bytes, file); + size_t read_pixels = read_size / pixel_size; + if (chunk_start_pix + read_pixels > total_pixels) + read_pixels = total_pixels - chunk_start_pix; + gr_copy_pixels(data + chunk_start_pix, chunk, format, + read_pixels); + } +} + +#define COMPRESSED_CHUNK_SIZE BUFSIZ +#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4) + +/// Loads compressed RGB or RGBA image data from a file. +static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file, + int format, size_t total_pixels) { + size_t pixel_size = format == 24 ? 3 : 4; + unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE]; + unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE]; + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_out = decompressed_chunk; + strm.avail_out = DECOMPRESSED_CHUNK_SIZE; + strm.avail_in = 0; + strm.next_in = Z_NULL; + int ret = inflateInit(&strm); + if (ret != Z_OK) + return 1; + + int error = 0; + int progress = 0; + size_t total_copied_pixels = 0; + while (1) { + // If we don't have enough data in the input buffer, try to read + // from the file. + if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) { + // Move the existing data to the beginning. + memmove(compressed_chunk, strm.next_in, strm.avail_in); + strm.next_in = compressed_chunk; + // Read more data. + size_t bytes_read = fread( + compressed_chunk + strm.avail_in, 1, + COMPRESSED_CHUNK_SIZE - strm.avail_in, file); + strm.avail_in += bytes_read; + if (bytes_read != 0) + progress = 1; + } + + // Try to inflate the data. + int ret = inflate(&strm, Z_SYNC_FLUSH); + if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) { + error = 1; + fprintf(stderr, + "error: could not decompress the image, error " + "%s\n", + ret == Z_MEM_ERROR ? "Z_MEM_ERROR" + : "Z_DATA_ERROR"); + break; + } + + // Copy the data from the output buffer to the image. + size_t full_pixels = + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size; + // Make sure we don't overflow the image. + if (full_pixels > total_pixels - total_copied_pixels) + full_pixels = total_pixels - total_copied_pixels; + if (full_pixels > 0) { + // Copy pixels. + gr_copy_pixels(data, decompressed_chunk, format, + full_pixels); + data += full_pixels; + total_copied_pixels += full_pixels; + if (total_copied_pixels >= total_pixels) { + // We filled the whole image, there may be some + // data left, but we just truncate it. + break; + } + // Move the remaining data to the beginning. + size_t copied_bytes = full_pixels * pixel_size; + size_t leftover = + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) - + copied_bytes; + memmove(decompressed_chunk, + decompressed_chunk + copied_bytes, leftover); + strm.next_out -= copied_bytes; + strm.avail_out += copied_bytes; + progress = 1; + } + + // If we haven't made any progress, then we have reached the end + // of both the file and the inflated data. + if (!progress) + break; + progress = 0; + } + + inflateEnd(&strm); + return error; +} + +#undef COMPRESSED_CHUNK_SIZE +#undef DECOMPRESSED_CHUNK_SIZE + +/// Load the image from a file containing raw pixel data (RGB or RGBA), the data +/// may be compressed. +static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame, + const char *filename) { + size_t total_pixels = frame->data_pix_width * frame->data_pix_height; + if (total_pixels * 4 > graphics_max_single_image_ram_size) { + fprintf(stderr, + "error: image %u frame %u is too big too load: %zu > %u\n", + frame->image->image_id, frame->index, total_pixels * 4, + graphics_max_single_image_ram_size); + return NULL; + } + + FILE* file = fopen(filename, "rb"); + if (!file) { + fprintf(stderr, + "error: could not open image file: %s\n", + sanitized_filename(filename)); + return NULL; + } + + Imlib_Image image = imlib_create_image(frame->data_pix_width, + frame->data_pix_height); + if (!image) { + fprintf(stderr, + "error: could not create an image of size %d x %d\n", + frame->data_pix_width, frame->data_pix_height); + fclose(file); + return NULL; + } + + imlib_context_set_image(image); + imlib_image_set_has_alpha(1); + DATA32* data = imlib_image_get_data(); + + // The default format is 32. + int format = frame->format ? frame->format : 32; + + if (frame->compression == 0) { + gr_load_raw_pixel_data_uncompressed(data, file, format, + total_pixels); + } else { + int ret = gr_load_raw_pixel_data_compressed(data, file, format, + total_pixels); + if (ret != 0) { + imlib_image_put_back_data(data); + imlib_free_image(); + fclose(file); + return NULL; + } + } + + fclose(file); + imlib_image_put_back_data(data); + return image; +} + +/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object +/// is fully composed on top of the background frame. If the frame is already +/// loaded, does nothing. Loading may fail, in which case the status of the +/// frame will be set to STATUS_RAM_LOADING_ERROR. +static void gr_load_imlib_object(ImageFrame *frame) { + if (frame->imlib_object) + return; + + // If the image is uninitialized or uploading has failed, or the file + // has been deleted, we cannot load the image. + if (frame->status < STATUS_UPLOADING_SUCCESS) + return; + if (frame->disk_size == 0) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: cached image was deleted: %u frame %u\n", + frame->image->image_id, frame->index); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + // Prevent recursive dependences between frames. + if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) { + fprintf(stderr, + "error: recursive loading of image %u frame %u\n", + frame->image->image_id, frame->index); + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + frame->status = STATUS_RAM_LOADING_IN_PROGRESS; + + // Load the background frame if needed. Hopefully it's not recursive. + ImageFrame *bg_frame = NULL; + if (frame->background_frame_index) { + bg_frame = gr_get_frame(frame->image, + frame->background_frame_index); + if (!bg_frame) { + fprintf(stderr, + "error: could not find background " + "frame %d for image %u frame %d\n", + frame->background_frame_index, + frame->image->image_id, frame->index); + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + gr_load_imlib_object(bg_frame); + if (!bg_frame->imlib_object) { + fprintf(stderr, + "error: could not load background frame %d for " + "image %u frame %d\n", + frame->background_frame_index, + frame->image->image_id, frame->index); + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + } + + // Load the frame data image. + Imlib_Image frame_data_image = NULL; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + GR_LOG("Loading image: %s\n", sanitized_filename(filename)); + if (frame->format == 100 || frame->format == 0) + frame_data_image = imlib_load_image(filename); + if (frame->format == 32 || frame->format == 24 || + (!frame_data_image && frame->format == 0)) + frame_data_image = gr_load_raw_pixel_data(frame, filename); + this_redraw_cycle_loaded_files++; + + if (!frame_data_image) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, "error: could not load image: %s\n", + sanitized_filename(filename)); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + imlib_context_set_image(frame_data_image); + int frame_data_width = imlib_image_get_width(); + int frame_data_height = imlib_image_get_height(); + GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width, + frame_data_height); + // If imlib loading succeeded, and it is the first frame, set the + // information about the original image size, unless it's already set. + if (frame->index == 1 && frame->image->pix_width == 0 && + frame->image->pix_height == 0) { + frame->image->pix_width = frame_data_width; + frame->image->pix_height = frame_data_height; + } + + int image_width = frame->image->pix_width; + int image_height = frame->image->pix_height; + + // Compose the image with the background color or frame. + if (frame->background_color != 0 || bg_frame || + image_width != frame_data_width || + image_height != frame_data_height) { + GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n", + frame->background_color, frame->background_frame_index); + Imlib_Image composed_image = imlib_create_image( + image_width, image_height); + imlib_context_set_image(composed_image); + imlib_image_set_has_alpha(1); + imlib_context_set_anti_alias(0); + + // Start with the background frame or color. + imlib_context_set_blend(0); + if (bg_frame && bg_frame->imlib_object) { + imlib_blend_image_onto_image( + bg_frame->imlib_object, 1, 0, 0, + image_width, image_height, 0, 0, + image_width, image_height); + } else { + int r = (frame->background_color >> 24) & 0xFF; + int g = (frame->background_color >> 16) & 0xFF; + int b = (frame->background_color >> 8) & 0xFF; + int a = frame->background_color & 0xFF; + imlib_context_set_color(r, g, b, a); + imlib_image_fill_rectangle(0, 0, image_width, + image_height); + } + + // Blend the frame data image onto the background. + imlib_context_set_blend(1); + imlib_blend_image_onto_image( + frame_data_image, 1, 0, 0, frame->data_pix_width, + frame->data_pix_height, frame->x, frame->y, + frame->data_pix_width, frame->data_pix_height); + + // Free the frame data image. + imlib_context_set_image(frame_data_image); + imlib_free_image(); + + frame_data_image = composed_image; + } + + frame->imlib_object = frame_data_image; + + images_ram_size += gr_frame_current_ram_size(frame); + frame->status = STATUS_RAM_LOADING_SUCCESS; + + GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n", + frame->image->image_id, frame->index, + images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024); +} + +/// Premultiplies the alpha channel of the image data. The data is an array of +/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB. +static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) { + for (size_t i = 0; i < num_pixels; ++i) { + DATA32 pixel = data[i]; + unsigned char a = pixel >> 24; + if (a == 0) { + data[i] = 0; + } else if (a != 255) { + unsigned char b = (pixel & 0xFF) * a / 255; + unsigned char g = ((pixel >> 8) & 0xFF) * a / 255; + unsigned char r = ((pixel >> 16) & 0xFF) * a / 255; + data[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + } +} + +/// Creates a pixmap for the frame of an image placement. The pixmap contain the +/// image data correctly scaled and fit to the box defined by the number of +/// rows/columns of the image placement and the provided cell dimensions in +/// pixels. If the placement is already loaded, it will be reloaded only if the +/// cell dimensions have changed. +Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) { + Image *img = placement->image; + ImageFrame *frame = gr_get_frame(img, frameidx); + + // Update the atime uncoditionally. + gr_touch_placement(placement); + if (frame) + gr_touch_frame(frame); + + // If cw or ch are different, unload all the pixmaps. + if (placement->scaled_cw != cw || placement->scaled_ch != ch) { + gr_unload_placement(placement); + placement->scaled_cw = cw; + placement->scaled_ch = ch; + } + + // If it's already loaded, do nothing. + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); + if (pixmap) + return pixmap; + + GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id, + placement->placement_id, frameidx); + + // Load the imlib object for the frame. + if (!frame) { + fprintf(stderr, + "error: could not find frame %u for image %u\n", + frameidx, img->image_id); + return 0; + } + gr_load_imlib_object(frame); + if (!frame->imlib_object) + return 0; + + // Infer the placement size if needed. + gr_infer_placement_size_maybe(placement); + + // Create the scaled image. This is temporary, we will scale it + // appropriately, upload to the X server, and then delete immediately. + int scaled_w = (int)placement->cols * cw; + int scaled_h = (int)placement->rows * ch; + if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) { + fprintf(stderr, + "error: placement %u/%u would be too big to load: %d x " + "%d x 4 > %u\n", + img->image_id, placement->placement_id, scaled_w, + scaled_h, graphics_max_single_image_ram_size); + return 0; + } + Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h); + if (!scaled_image) { + fprintf(stderr, + "error: imlib_create_image(%d, %d) returned " + "null\n", + scaled_w, scaled_h); + return 0; + } + imlib_context_set_image(scaled_image); + imlib_image_set_has_alpha(1); + + // First fill the scaled image with the transparent color. + imlib_context_set_blend(0); + imlib_context_set_color(0, 0, 0, 0); + imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + + // The source rectangle. + int src_x = placement->src_pix_x; + int src_y = placement->src_pix_y; + int src_w = placement->src_pix_width; + int src_h = placement->src_pix_height; + // Whether the box is too small to use the true size of the image. + char box_too_small = scaled_w < src_w || scaled_h < src_h; + char mode = placement->scale_mode; + + // Then blend the original image onto the transparent background. + if (src_w <= 0 || src_h <= 0) { + fprintf(stderr, "warning: image of zero size\n"); + } else if (mode == SCALE_MODE_FILL) { + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, + src_y, src_w, src_h, 0, 0, + scaled_w, scaled_h); + } else if (mode == SCALE_MODE_NONE || + (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, + src_y, src_w, src_h, 0, 0, src_w, + src_h); + } else { + if (mode != SCALE_MODE_CONTAIN && + mode != SCALE_MODE_NONE_OR_CONTAIN) { + fprintf(stderr, + "warning: unknown scale mode %u, using " + "'contain' instead\n", + mode); + } + int dest_x, dest_y; + int dest_w, dest_h; + if (scaled_w * src_h > src_w * scaled_h) { + // If the box is wider than the original image, fit to + // height. + dest_h = scaled_h; + dest_y = 0; + dest_w = src_w * scaled_h / src_h; + dest_x = (scaled_w - dest_w) / 2; + } else { + // Otherwise, fit to width. + dest_w = scaled_w; + dest_x = 0; + dest_h = src_h * scaled_w / src_w; + dest_y = (scaled_h - dest_h) / 2; + } + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, + src_y, src_w, src_h, dest_x, + dest_y, dest_w, dest_h); + } + + // XRender needs the alpha channel premultiplied. + DATA32 *data = imlib_image_get_data(); + gr_premultiply_alpha(data, scaled_w * scaled_h); + + // Upload the image to the X server. + Display *disp = imlib_context_get_display(); + Visual *vis = imlib_context_get_visual(); + Colormap cmap = imlib_context_get_colormap(); + Drawable drawable = imlib_context_get_drawable(); + if (!drawable) + drawable = DefaultRootWindow(disp); + pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32); + XVisualInfo visinfo; + XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo); + XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, + (char *)data, scaled_w, scaled_h, 32, 0); + GC gc = XCreateGC(disp, pixmap, 0, NULL); + XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w, + scaled_h); + XFreeGC(disp, gc); + // XDestroyImage will free the data as well, but it is managed by imlib, + // so set it to NULL. + ximage->data = NULL; + XDestroyImage(ximage); + imlib_image_put_back_data(data); + imlib_free_image(); + + // Assign the pixmap to the frame and increase the ram size. + gr_set_frame_pixmap(placement, frameidx, pixmap); + images_ram_size += gr_placement_single_frame_ram_size(placement); + this_redraw_cycle_loaded_pixmaps++; + + GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " + "KiB)\n", + frame->image->image_id, placement->placement_id, frame->index, + images_ram_size / 1024, + gr_placement_single_frame_ram_size(placement) / 1024); + + // Free up ram if needed, but keep the pixmap we've loaded no matter + // what. + placement->protected_frame = frameidx; + gr_check_limits(); + placement->protected_frame = 0; + + return pixmap; +} + +//////////////////////////////////////////////////////////////////////////////// +// Initialization and deinitialization. +//////////////////////////////////////////////////////////////////////////////// + +/// Creates a temporary directory. +static int gr_create_cache_dir() { + strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); + if (!mkdtemp(cache_dir)) { + fprintf(stderr, + "error: could not create temporary dir from template " + "%s\n", + sanitized_filename(cache_dir)); + return 0; + } + fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); + return 1; +} + +/// Checks whether `tmp_dir` exists and recreates it if it doesn't. +static void gr_make_sure_tmpdir_exists() { + struct stat st; + if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) + return; + fprintf(stderr, + "error: %s is not a directory, will need to create a new " + "graphics cache directory\n", + sanitized_filename(cache_dir)); + gr_create_cache_dir(); +} + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm) { + // Set the initialization time. + clock_gettime(CLOCK_MONOTONIC, &initialization_time); + + // Create the temporary dir. + if (!gr_create_cache_dir()) + abort(); + + // Initialize imlib. + imlib_context_set_display(disp); + imlib_context_set_visual(vis); + imlib_context_set_colormap(cm); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + // Imlib2 checks only the file name when caching, which is not enough + // for us since we reuse file names. Disable caching. + imlib_set_cache_size(0); + + // Prepare for color inversion. + for (size_t i = 0; i < 256; ++i) + reverse_table[i] = 255 - i; + + // Create data structures. + images = kh_init(id2image); + kv_init(next_redraw_times); + + atexit(gr_deinit); +} + +/// Deinitialize the graphics module. +void gr_deinit() { + // Remove the cache dir. + remove(cache_dir); + kv_destroy(next_redraw_times); + if (images) { + // Delete all images. + gr_delete_all_images(); + // Destroy the data structures. + kh_destroy(id2image, images); + images = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Dumping, debugging, and image preview. +//////////////////////////////////////////////////////////////////////////////// + +/// Returns a string containing a time difference in a human-readable format. +/// Uses a static buffer, so be careful. +static const char *gr_ago(Milliseconds diff) { + static char result[32]; + double seconds = (double)diff / 1000.0; + if (seconds < 1) + snprintf(result, sizeof(result), "%.2f sec ago", seconds); + else if (seconds < 60) + snprintf(result, sizeof(result), "%d sec ago", (int)seconds); + else if (seconds < 3600) + snprintf(result, sizeof(result), "%d min %d sec ago", + (int)(seconds / 60), (int)(seconds) % 60); + else { + snprintf(result, sizeof(result), "%d hr %d min %d sec ago", + (int)(seconds / 3600), (int)(seconds) % 3600 / 60, + (int)(seconds) % 60); + } + return result; +} + +/// Prints to `file` with an indentation of `ind` spaces. +static void fprintf_ind(FILE *file, int ind, const char *format, ...) { + fprintf(file, "%*s", ind, ""); + va_list args; + va_start(args, format); + vfprintf(file, format, args); + va_end(args); +} + +/// Dumps the image info to `file` with an indentation of `ind` spaces. +static void gr_dump_image_info(FILE *file, Image *img, int ind) { + if (!img) { + fprintf_ind(file, ind, "Image is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Image %u\n", img->image_id); + ind += 4; + fprintf_ind(file, ind, "number: %u\n", img->image_number); + fprintf_ind(file, ind, "global command index: %lu\n", + img->global_command_index); + fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, + gr_ago(now - img->atime)); + fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, + img->pix_height); + fprintf_ind(file, ind, "cur frame start time: %ld %s\n", + img->current_frame_time, + gr_ago(now - img->current_frame_time)); + if (img->next_redraw) + fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", + img->next_redraw, img->next_redraw - now); + fprintf_ind(file, ind, "total disk size: %u KiB\n", + img->total_disk_size / 1024); + fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); + fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); + fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); + fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); + fprintf_ind(file, ind, "default_placement: %u\n", + img->default_placement); +} + +/// Dumps the frame info to `file` with an indentation of `ind` spaces. +static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { + if (!frame) { + fprintf_ind(file, ind, "Frame is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Frame %d\n", frame->index); + ind += 4; + if (frame->index == 0) { + fprintf_ind(file, ind, "NOT INITIALIZED\n"); + return; + } + if (frame->uploading_failure) + fprintf_ind(file, ind, "uploading failure: %s\n", + image_uploading_failure_strings + [frame->uploading_failure]); + fprintf_ind(file, ind, "gap: %d\n", frame->gap); + fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, + gr_ago(now - frame->atime)); + fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, + frame->data_pix_height); + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (access(filename, F_OK) != -1) + fprintf_ind(file, ind, "file: %s\n", + sanitized_filename(filename)); + else + fprintf_ind(file, ind, "not on disk\n"); + fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); + if (frame->imlib_object) { + unsigned ram_size = gr_frame_current_ram_size(frame); + fprintf_ind(file, ind, + "loaded into ram, size: %d " + "KiB\n", + ram_size / 1024); + } else { + fprintf_ind(file, ind, "not loaded into ram\n"); + } +} + +/// Dumps the placement info to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) { + fprintf_ind(file, ind, "Placement is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); + ind += 4; + fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, + gr_ago(now - placement->atime)); + fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); + fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, + placement->rows); + fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, + placement->scaled_ch); + fprintf_ind(file, ind, "ram per frame: %u KiB\n", + gr_placement_single_frame_ram_size(placement) / 1024); + unsigned ram_size = gr_placement_current_ram_size(placement); + fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); +} + +/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) + return; + int frameidx = 1; + foreach_pixmap(*placement, pixmap, { + fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, + pixmap); + ++frameidx; + }); +} + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state() { + FILE *file = stderr; + int ind = 0; + fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); + fprintf_ind(file, ind, + "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " + "sizeof(ImagePlacement) = %lu\n", + sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); + fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); + fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); + fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", + images_ram_size / 1024); + fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", + images_disk_size / 1024); + + Milliseconds now = gr_now_ms(); + + int64_t images_ram_size_computed = 0; + int64_t images_disk_size_computed = 0; + + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + fprintf_ind(file, ind, "----------------\n"); + gr_dump_image_info(file, img, 0); + int64_t total_disk_size_computed = 0; + int total_duration_computed = 0; + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + if (frame->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + total_duration_computed += frame->gap; + images_disk_size_computed += frame->disk_size; + total_disk_size_computed += frame->disk_size; + if (frame->imlib_object) + images_ram_size_computed += + gr_frame_current_ram_size(frame); + }); + if (img->total_disk_size != total_disk_size_computed) { + fprintf_ind(file, ind, + " ERROR: total_disk_size is %u, but " + "computed value is %ld\n", + img->total_disk_size, total_disk_size_computed); + } + if (img->total_duration != total_duration_computed) { + fprintf_ind(file, ind, + " ERROR: total_duration is %d, but computed " + "value is %d\n", + img->total_duration, total_duration_computed); + } + kh_foreach_value(img->placements, placement, { + gr_dump_placement_info(file, placement, 4); + if (placement->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + fprintf_ind(file, 8, + "Pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 12); + unsigned ram_size = + gr_placement_current_ram_size(placement); + images_ram_size_computed += ram_size; + }); + }); + if (images_ram_size != images_ram_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_ram_size is %ld, but computed value " + "is %ld\n", + images_ram_size, images_ram_size_computed); + } + if (images_disk_size != images_disk_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_disk_size is %ld, but computed value " + "is %ld\n", + images_disk_size, images_disk_size_computed); + } + fprintf_ind(file, ind, "===========================================\n"); +} + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +// TODO: Currently we do this for the first frame only. Not sure what to do with +// animations. +void gr_preview_image(uint32_t image_id, const char *exec) { + char command[256]; + size_t len; + Image *img = gr_find_image(image_id); + if (img) { + ImageFrame *frame = &img->first_frame; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (frame->disk_size == 0) { + len = snprintf(command, 255, + "xmessage 'Image with id=%u is not " + "fully copied to %s'", + image_id, sanitized_filename(filename)); + } else { + len = snprintf(command, 255, "%s %s &", exec, + sanitized_filename(filename)); + } + } else { + len = snprintf(command, 255, + "xmessage 'Cannot find image with id=%u'", + image_id); + } + if (len > 255) { + fprintf(stderr, "error: command too long: %s\n", command); + snprintf(command, 255, "xmessage 'error: command too long'"); + } + if (system(command) != 0) { + fprintf(stderr, "error: could not execute command %s\n", + command); + } +} + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable) { + char filename[MAX_FILENAME_SIZE]; + snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); + FILE *file = fopen(filename, "w"); + if (!file) { + perror("fopen"); + return; + } + // Basic information about the cell. + fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); + fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); + fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); + fprintf(file, "classic/unicode placeholder = %s\n", + is_classic_placeholder ? "classic" : "unicode"); + fprintf(file, "original diacritic count = %d\n", diacritic_count); + // Information about the image and the placement. + Image *img = gr_find_image(image_id); + ImagePlacement *placement = gr_find_placement(img, placement_id); + gr_dump_image_info(file, img, 0); + gr_dump_placement_info(file, placement, 0); + if (img) { + fprintf(file, "Frames:\n"); + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + }); + } + if (placement) { + fprintf(file, "Placement pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 4); + } + fclose(file); + char *argv[] = {st_executable, "-e", "less", filename, NULL}; + if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) { + perror("posix_spawnp"); + return; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Appending and displaying image rectangles. +//////////////////////////////////////////////////////////////////////////////// + +/// Displays debug information in the rectangle using colors col1 and col2. +static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2, + const char *message) { + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + char info[MAX_INFO_LEN]; + if (rect->placement_id) + snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message, + rect->image_id, rect->placement_id, + rect->img_start_col, rect->img_end_col, + rect->img_start_row, rect->img_end_row); + else + snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message, + rect->image_id, rect->img_start_col, rect->img_end_col, + rect->img_start_row, rect->img_end_row); + XSetForeground(disp, gc, col1); + XDrawString(disp, buf, gc, rect->screen_x_pix + 4, + rect->screen_y_pix + h_pix - 3, info, strlen(info)); + XSetForeground(disp, gc, col2); + XDrawString(disp, buf, gc, rect->screen_x_pix + 2, + rect->screen_y_pix + h_pix - 5, info, strlen(info)); + XFreeGC(disp, gc); +} + +/// Draws a rectangle (bounding box) for debugging. +static void gr_showrect(Drawable buf, ImageRect *rect) { + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + XSetForeground(disp, gc, 0xFF00FF00); + XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix, + w_pix - 1, h_pix - 1); + XSetForeground(disp, gc, 0xFFFF0000); + XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1, + rect->screen_y_pix + 1, w_pix - 3, h_pix - 3); + XFreeGC(disp, gc); +} + +/// Updates the next redraw time for the given row. Resizes the +/// next_redraw_times array if needed. +static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) { + if (next_redraw == 0) + return; + if (row >= kv_size(next_redraw_times)) { + size_t old_size = kv_size(next_redraw_times); + kv_a(Milliseconds, next_redraw_times, row); + for (size_t i = old_size; i <= row; ++i) + kv_A(next_redraw_times, i) = 0; + } + Milliseconds old_value = kv_A(next_redraw_times, row); + if (old_value == 0 || old_value > next_redraw) + kv_A(next_redraw_times, row) = next_redraw; +} + +/// Draws the given part of an image. +static void gr_drawimagerect(Drawable buf, ImageRect *rect) { + ImagePlacement *placement = + gr_find_image_and_placement(rect->image_id, rect->placement_id); + // If the image does not exist or image display is switched off, draw + // the bounding box. + if (!placement || !graphics_display_images) { + gr_showrect(buf, rect); + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + return; + } + + Image *img = placement->image; + + if (img->last_redraw < drawing_start_time) { + // This is the first time we draw this image in this redraw + // cycle. Update the frame index we are going to display. Note + // that currently all image placements are synchronized. + int old_frame = img->current_frame; + gr_update_frame_index(img, drawing_start_time); + img->last_redraw = drawing_start_time; + } + + // Adjust next redraw times for the rows of this image rect. + if (img->next_redraw) { + for (int row = rect->screen_y_row; + row <= rect->screen_y_row + rect->img_end_row - + rect->img_start_row - 1; ++row) { + gr_update_next_redraw_time( + row, img->next_redraw); + } + } + + // Load the frame. + Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw, + rect->ch); + + // If the image couldn't be loaded, display the bounding box. + if (!pixmap) { + gr_showrect(buf, rect); + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + return; + } + + int src_x = rect->img_start_col * rect->cw; + int src_y = rect->img_start_row * rect->ch; + int width = (rect->img_end_col - rect->img_start_col) * rect->cw; + int height = (rect->img_end_row - rect->img_start_row) * rect->ch; + int dst_x = rect->screen_x_pix; + int dst_y = rect->screen_y_pix; + + // Display the image. + Display *disp = imlib_context_get_display(); + Visual *vis = imlib_context_get_visual(); + + // Create an xrender picture for the window. + XRenderPictFormat *win_format = + XRenderFindVisualFormat(disp, vis); + Picture window_pic = + XRenderCreatePicture(disp, buf, win_format, 0, NULL); + + // If needed, invert the image pixmap. Note that this naive approach of + // inverting the pixmap is not entirely correct, because the pixmap is + // premultiplied. But the result is good enough to visually indicate + // selection. + if (rect->reverse) { + unsigned pixmap_w = + (unsigned)placement->cols * placement->scaled_cw; + unsigned pixmap_h = + (unsigned)placement->rows * placement->scaled_ch; + Pixmap invpixmap = + XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32); + XGCValues gcv = {.function = GXcopyInverted}; + GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv); + XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w, + pixmap_h, 0, 0); + XFreeGC(disp, gc); + pixmap = invpixmap; + } + + // Create a picture for the image pixmap. + XRenderPictFormat *pic_format = + XRenderFindStandardFormat(disp, PictStandardARGB32); + Picture pixmap_pic = + XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL); + + // Composite the image onto the window. In the reverse mode we ignore + // the alpha channel of the image because the naive inversion above + // seems to invert the alpha channel as well. + int pictop = rect->reverse ? PictOpSrc : PictOpOver; + XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, + src_x, src_y, src_x, src_y, dst_x, dst_y, width, + height); + + // Free resources + XRenderFreePicture(disp, pixmap_pic); + XRenderFreePicture(disp, window_pic); + if (rect->reverse) + XFreePixmap(disp, pixmap); + + // In debug mode always draw bounding boxes and print info. + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) { + gr_showrect(buf, rect); + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + } +} + +/// Removes the given image rectangle. +static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); } + +/// Returns the bottom coordinate of the rect. +static int gr_getrectbottom(ImageRect *rect) { + return rect->screen_y_pix + + (rect->img_end_row - rect->img_start_row) * rect->ch; +} + +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch) { + current_cw = cw; + current_ch = ch; + this_redraw_cycle_loaded_files = 0; + this_redraw_cycle_loaded_pixmaps = 0; + drawing_start_time = gr_now_ms(); + imlib_context_set_drawable(buf); +} + +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf) { + // Draw and then delete all known image rectangles. + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (!rect->image_id) + continue; + gr_drawimagerect(buf, rect); + gr_freerect(rect); + } + + // Compute the delay until the next redraw as the minimum of the next + // redraw delays for all rows. + Milliseconds drawing_end_time = gr_now_ms(); + graphics_next_redraw_delay = INT_MAX; + for (int row = 0; row < kv_size(next_redraw_times); ++row) { + Milliseconds row_next_redraw = kv_A(next_redraw_times, row); + if (row_next_redraw > 0) { + int delay = MAX(graphics_animation_min_delay, + row_next_redraw - drawing_end_time); + graphics_next_redraw_delay = + MIN(graphics_next_redraw_delay, delay); + } + } + + // In debug mode display additional info. + if (graphics_debug_mode) { + int milliseconds = drawing_end_time - drawing_start_time; + + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + const char *debug_mode_str = + graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES + ? "(boxes shown) " + : ""; + int redraw_delay = graphics_next_redraw_delay == INT_MAX + ? -1 + : graphics_next_redraw_delay; + char info[MAX_INFO_LEN]; + snprintf(info, MAX_INFO_LEN, + "%sRender time: %d ms ram %ld K disk %ld K count " + "%d cell %dx%d delay %d", + debug_mode_str, milliseconds, images_ram_size / 1024, + images_disk_size / 1024, kh_size(images), current_cw, + current_ch, redraw_delay); + XSetForeground(disp, gc, 0xFF000000); + XFillRectangle(disp, buf, gc, 0, 0, 600, 16); + XSetForeground(disp, gc, 0xFFFFFFFF); + XDrawString(disp, buf, gc, 0, 14, info, strlen(info)); + XFreeGC(disp, gc); + + if (milliseconds > 0) { + fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n", + info, this_redraw_cycle_loaded_files, + this_redraw_cycle_loaded_pixmaps); + } + } + + // Check the limits in case we have used too much ram for placements. + gr_check_limits(); +} + +// Add an image rectangle to the list of rectangles to draw. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse) { + current_cw = cw; + current_ch = ch; + + ImageRect new_rect; + new_rect.image_id = image_id; + new_rect.placement_id = placement_id; + new_rect.img_start_col = img_start_col; + new_rect.img_end_col = img_end_col; + new_rect.img_start_row = img_start_row; + new_rect.img_end_row = img_end_row; + new_rect.screen_y_row = y_row; + new_rect.screen_x_pix = x_pix; + new_rect.screen_y_pix = y_pix; + new_rect.ch = ch; + new_rect.cw = cw; + new_rect.reverse = reverse; + + // Display some red text in debug mode. + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? "); + + // If it's the empty image (image_id=0) or an empty rectangle, do + // nothing. + if (image_id == 0 || img_end_col - img_start_col <= 0 || + img_end_row - img_start_row <= 0) + return; + // Try to find a rect to merge with. + ImageRect *free_rect = NULL; + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (rect->image_id == 0) { + if (!free_rect) + free_rect = rect; + continue; + } + if (rect->image_id != image_id || + rect->placement_id != placement_id || rect->cw != cw || + rect->ch != ch || rect->reverse != reverse) + continue; + // We only support the case when the new stripe is added to the + // bottom of an existing rectangle and they are perfectly + // aligned. + if (rect->img_end_row == img_start_row && + gr_getrectbottom(rect) == y_pix) { + if (rect->img_start_col == img_start_col && + rect->img_end_col == img_end_col && + rect->screen_x_pix == x_pix) { + rect->img_end_row = img_end_row; + return; + } + } + } + // If we haven't merged the new rect with any existing rect, and there + // is no free rect, we have to render one of the existing rects. + if (!free_rect) { + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (!free_rect || gr_getrectbottom(free_rect) > + gr_getrectbottom(rect)) + free_rect = rect; + } + gr_drawimagerect(buf, free_rect); + gr_freerect(free_rect); + } + // Start a new rectangle in `free_rect`. + *free_rect = new_rect; +} + +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows) { + if (rows < kv_size(next_redraw_times)) + kv_size(next_redraw_times) = rows; + if (rows * 2 < kv_max(next_redraw_times)) + kv_resize(Milliseconds, next_redraw_times, rows); + for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) { + if (dirty[i]) { + kv_A(next_redraw_times, i) = 0; + continue; + } + Milliseconds next_update = kv_A(next_redraw_times, i); + if (next_update > 0 && next_update <= drawing_start_time) { + dirty[i] = 1; + kv_A(next_redraw_times, i) = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Command parsing and handling. +//////////////////////////////////////////////////////////////////////////////// + +/// A parsed kitty graphics protocol command. +typedef struct { + /// The command itself, without the 'G'. + char *command; + /// The payload (after ';'). + char *payload; + /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'. + char action; + /// 'q=', 1 to suppress OK response, 2 to suppress errors too. + int quiet; + /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with + /// imlib2. If 'f=0', will try to load with imlib2, then fallback to + /// 32-bit pixel data. + int format; + /// 'o=', may be 'z' for RFC 1950 ZLIB. + int compression; + /// 't=', may be 'f', 't' or 'd'. + char transmission_medium; + /// 'd=' + char delete_specifier; + /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'. + /// When 'a=f', this is the size of the frame rectangle when composed on + /// top of another frame. + int frame_pix_width, frame_pix_height; + /// 'x=', 'y=' - top-left corner of the source rectangle. + int src_pix_x, src_pix_y; + /// 'w=', 'h=' - width and height of the source rectangle. + int src_pix_width, src_pix_height; + /// 'r=', 'c=' + int rows, columns; + /// 'i=' + uint32_t image_id; + /// 'I=' + uint32_t image_number; + /// 'p=' + uint32_t placement_id; + /// 'm=', may be 0 or 1. + int more; + /// True if either 'm=0' or 'm=1' is specified. + char is_data_transmission; + /// True if turns out that this command is a continuation of a data + /// transmission and not the first one for this image. Populated by + /// `gr_handle_transmit_command`. + char is_direct_transmission_continuation; + /// 'S=', used to check the size of uploaded data. + int size; + /// 'U=', whether it's a virtual placement for Unicode placeholders. + int virtual; + /// 'C=', if true, do not move the cursor when displaying this placement + /// (non-virtual placements only). + char do_not_move_cursor; + // --------------------------------------------------------------------- + // Animation-related fields. Their keys often overlap with keys of other + // commands, so these make sense only if the action is 'a=f' (frame + // transmission) or 'a=a' (animation control). + // + // 'x=' and 'y=', the relative position of the frame image when it's + // composed on top of another frame. + int frame_dst_pix_x, frame_dst_pix_y; + /// 'X=', 'X=1' to replace colors instead of alpha blending on top of + /// the background color or frame. + char replace_instead_of_blending; + /// 'Y=', the background color in the 0xRRGGBBAA format (still + /// transmitted as a decimal number). + uint32_t background_color; + /// (Only for 'a=f'). 'c=', the 1-based index of the background frame. + int background_frame; + /// (Only for 'a=a'). 'c=', sets the index of the current frame. + int current_frame; + /// 'r=', the 1-based index of the frame to edit. + int edit_frame; + /// 'z=', the duration of the frame. Zero if not specified, negative if + /// the frame is gapless (i.e. skipped). + int gap; + /// (Only for 'a=a'). 's=', if non-zero, sets the state of the + /// animation, 1 to stop, 2 to run in loading mode, 3 to loop. + int animation_state; + /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the + /// animation will loop. 1 to loop infinitely, N to loop N-1 times. + int loops; +} GraphicsCommand; + +/// Replaces all non-printed characters in `str` with '?' and truncates the +/// string to `max_size`, maybe inserting ellipsis at the end. +static void sanitize_str(char *str, size_t max_size) { + assert(max_size >= 4); + for (size_t i = 0; i < max_size; ++i) { + unsigned c = str[i]; + if (c == '\0') + return; + if (c >= 128 || !isprint(c)) + str[i] = '?'; + } + str[max_size - 1] = '\0'; + str[max_size - 2] = '.'; + str[max_size - 3] = '.'; + str[max_size - 4] = '.'; +} + +/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be +/// careful. +static const char *sanitized_filename(const char *str) { + static char buf[MAX_FILENAME_SIZE]; + strncpy(buf, str, sizeof(buf)); + sanitize_str(buf, sizeof(buf)); + return buf; +} + +/// Creates a response to the current command in `graphics_command_result`. +static void gr_createresponse(uint32_t image_id, uint32_t image_number, + uint32_t placement_id, const char *msg) { + if (!image_id && !image_number && !placement_id) { + // Nobody expects the response in this case, so just print it to + // stderr. + fprintf(stderr, + "error: No image id or image number or placement_id, " + "but still there is a response: %s\n", + msg); + return; + } + char *buf = graphics_command_result.response; + size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN; + size_t written; + written = snprintf(buf, maxlen, "\033_G"); + buf += written; + maxlen -= written; + if (image_id) { + written = snprintf(buf, maxlen, "i=%u,", image_id); + buf += written; + maxlen -= written; + } + if (image_number) { + written = snprintf(buf, maxlen, "I=%u,", image_number); + buf += written; + maxlen -= written; + } + if (placement_id) { + written = snprintf(buf, maxlen, "p=%u,", placement_id); + buf += written; + maxlen -= written; + } + buf[-1] = ';'; + written = snprintf(buf, maxlen, "%s\033\\", msg); + buf += written; + maxlen -= written; + buf[-2] = '\033'; + buf[-1] = '\\'; +} + +/// Creates the 'OK' response to the current command, unless suppressed or a +/// non-final data transmission. +static void gr_reportsuccess_cmd(GraphicsCommand *cmd) { + if (cmd->quiet < 1 && !cmd->more) + gr_createresponse(cmd->image_id, cmd->image_number, + cmd->placement_id, "OK"); +} + +/// Creates the 'OK' response to the current command (unless suppressed). +static void gr_reportsuccess_frame(ImageFrame *frame) { + uint32_t id = frame->image->query_id ? frame->image->query_id + : frame->image->image_id; + if (frame->quiet < 1) + gr_createresponse(id, frame->image->image_number, + frame->image->initial_placement_id, "OK"); +} + +/// Creates an error response to the current command (unless suppressed). +static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) { + char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; + graphics_command_result.error = 1; + va_list args; + va_start(args, format); + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); + va_end(args); + + fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command); + if (cmd->quiet < 2) + gr_createresponse(cmd->image_id, cmd->image_number, + cmd->placement_id, errmsg); +} + +/// Creates an error response to the current command (unless suppressed). +static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) { + char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; + graphics_command_result.error = 1; + va_list args; + va_start(args, format); + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); + va_end(args); + + if (!frame) { + fprintf(stderr, "%s\n", errmsg); + gr_createresponse(0, 0, 0, errmsg); + } else { + uint32_t id = frame->image->query_id ? frame->image->query_id + : frame->image->image_id; + fprintf(stderr, "%s id=%u\n", errmsg, id); + if (frame->quiet < 2) + gr_createresponse(id, frame->image->image_number, + frame->image->initial_placement_id, + errmsg); + } +} + +/// Loads an image and creates a success/failure response. Returns `frame`, or +/// NULL if it's a query action and the image was deleted. +static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) { + gr_load_imlib_object(frame); + if (!frame->imlib_object) { + gr_reporterror_frame(frame, "EBADF: could not load image"); + } else { + gr_reportsuccess_frame(frame); + } + // If it was a query action, discard the image. + if (frame->image->query_id) { + gr_delete_image(frame->image); + return NULL; + } + return frame; +} + +/// Creates an appropriate uploading failure response to the current command. +static void gr_reportuploaderror(ImageFrame *frame) { + switch (frame->uploading_failure) { + case 0: + return; + case ERROR_CANNOT_OPEN_CACHED_FILE: + gr_reporterror_frame(frame, + "EIO: could not create a file for image"); + break; + case ERROR_OVER_SIZE_LIMIT: + gr_reporterror_frame( + frame, + "EFBIG: the size of the uploaded image exceeded " + "the image size limit %u", + graphics_max_single_image_file_size); + break; + case ERROR_UNEXPECTED_SIZE: + gr_reporterror_frame(frame, + "EINVAL: the size of the uploaded image %u " + "doesn't match the expected size %u", + frame->disk_size, frame->expected_size); + break; + }; +} + +/// Displays a non-virtual placement. This functions records the information in +/// `graphics_command_result`, the placeholder itself is created by the terminal +/// after handling the current command in the graphics module. +static void gr_display_nonvirtual_placement(ImagePlacement *placement) { + if (placement->virtual) + return; + if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS) + return; + // Infer the placement size if needed. + gr_infer_placement_size_maybe(placement); + // Populate the information about the placeholder which will be created + // by the terminal. + graphics_command_result.create_placeholder = 1; + graphics_command_result.placeholder.image_id = placement->image->image_id; + graphics_command_result.placeholder.placement_id = placement->placement_id; + graphics_command_result.placeholder.columns = placement->cols; + graphics_command_result.placeholder.rows = placement->rows; + graphics_command_result.placeholder.do_not_move_cursor = + placement->do_not_move_cursor; + GR_LOG("Creating a placeholder for %u/%u %d x %d\n", + placement->image->image_id, placement->placement_id, + placement->cols, placement->rows); +} + +/// Marks the rows that are occupied by the image as dirty. +static void gr_schedule_image_redraw(Image *img) { + if (!img) + return; + gr_schedule_image_redraw_by_id(img->image_id); +} + +/// Appends data from `payload` to the frame `frame` when using direct +/// transmission. Note that we report errors only for the final command +/// (`!more`) to avoid spamming the client. If the frame is not specified, use +/// the image id and frame index we are currently uploading. +static void gr_append_data(ImageFrame *frame, const char *payload, int more) { + if (!frame) { + Image *img = gr_find_image(current_upload_image_id); + frame = gr_get_frame(img, current_upload_frame_index); + GR_LOG("Appending data to image %u frame %d\n", + current_upload_image_id, current_upload_frame_index); + if (!img) + GR_LOG("ERROR: this image doesn't exist\n"); + if (!frame) + GR_LOG("ERROR: this frame doesn't exist\n"); + } + if (!more) { + current_upload_image_id = 0; + current_upload_frame_index = 0; + } + if (!frame) { + if (!more) + gr_reporterror_frame(NULL, "ENOENT: could not find the " + "image to append data to"); + return; + } + if (frame->status != STATUS_UPLOADING) { + if (!more) + gr_reportuploaderror(frame); + return; + } + + // Decode the data. + size_t data_size = 0; + char *data = gr_base64dec(payload, &data_size); + + GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size, + frame->disk_size + data_size); + + // Do not append this data if the image exceeds the size limit. + if (frame->disk_size + data_size > + graphics_max_single_image_file_size || + frame->expected_size > graphics_max_single_image_file_size) { + free(data); + gr_delete_imagefile(frame); + frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; + if (!more) + gr_reportuploaderror(frame); + return; + } + + // If there is no open file corresponding to the image, create it. + if (!frame->open_file) { + gr_make_sure_tmpdir_exists(); + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + FILE *file = fopen(filename, frame->disk_size ? "a" : "w"); + if (!file) { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; + if (!more) + gr_reportuploaderror(frame); + return; + } + frame->open_file = file; + } + + // Write data to the file and update disk size variables. + fwrite(data, 1, data_size, frame->open_file); + free(data); + frame->disk_size += data_size; + frame->image->total_disk_size += data_size; + images_disk_size += data_size; + gr_touch_frame(frame); + + if (more) { + current_upload_image_id = frame->image->image_id; + current_upload_frame_index = frame->index; + } else { + current_upload_image_id = 0; + current_upload_frame_index = 0; + // Close the file. + if (frame->open_file) { + fclose(frame->open_file); + frame->open_file = NULL; + } + frame->status = STATUS_UPLOADING_SUCCESS; + uint32_t placement_id = frame->image->default_placement; + if (frame->expected_size && + frame->expected_size != frame->disk_size) { + // Report failure if the uploaded image size doesn't + // match the expected size. + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + gr_reportuploaderror(frame); + } else { + // Make sure to redraw all existing image instances. + gr_schedule_image_redraw(frame->image); + // Try to load the image into ram and report the result. + frame = gr_loadimage_and_report(frame); + // If there is a non-virtual image placement, we may + // need to display it. + if (frame && frame->index == 1) { + Image *img = frame->image; + ImagePlacement *placement = NULL; + kh_foreach_value(img->placements, placement, { + gr_display_nonvirtual_placement(placement); + }); + } + } + } + + // Check whether we need to delete old images. + gr_check_limits(); +} + +/// Finds the image either by id or by number specified in the command and sets +/// the image_id of `cmd` if the image was found. +static Image *gr_find_image_for_command(GraphicsCommand *cmd) { + if (cmd->image_id) + return gr_find_image(cmd->image_id); + Image *img = NULL; + // If the image number is not specified, we can't find the image, unless + // it's a put command, in which case we will try the last image. + if (cmd->image_number == 0 && cmd->action == 'p') + img = gr_find_image(last_image_id); + else + img = gr_find_image_by_number(cmd->image_number); + if (img) + cmd->image_id = img->image_id; + return img; +} + +/// Creates a new image or a new frame in an existing image (depending on the +/// command's action) and initializes its parameters from the command. +static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) { + if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 && + cmd->compression != 0) { + gr_reporterror_cmd(cmd, "EINVAL: compression is supported only " + "for raw pixel data (f=32 or f=24)"); + // Even though we report an error, we still create an image. + } + + Image *img = NULL; + if (cmd->action == 'f') { + // If it's a frame transmission action, there must be an + // existing image. + img = gr_find_image_for_command(cmd); + if (!img) { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return NULL; + } + } else { + // Otherwise create a new image object. If the action is `q`, + // we'll use random id instead of the one specified in the + // command. + uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id; + img = gr_new_image(image_id); + if (!img) + return NULL; + if (cmd->action == 'q') + img->query_id = cmd->image_id; + else if (!cmd->image_id) + cmd->image_id = img->image_id; + // Set the image number. + img->image_number = cmd->image_number; + } + + ImageFrame *frame = gr_append_new_frame(img); + // Initialize the frame. + frame->expected_size = cmd->size; + frame->format = cmd->format; + frame->compression = cmd->compression; + frame->background_color = cmd->background_color; + frame->background_frame_index = cmd->background_frame; + frame->gap = cmd->gap; + img->total_duration += frame->gap; + frame->blend = !cmd->replace_instead_of_blending; + frame->data_pix_width = cmd->frame_pix_width; + frame->data_pix_height = cmd->frame_pix_height; + if (cmd->action == 'f') { + frame->x = cmd->frame_dst_pix_x; + frame->y = cmd->frame_dst_pix_y; + } + // We save the quietness information in the frame because for direct + // transmission subsequent transmission command won't contain this info. + frame->quiet = cmd->quiet; + return frame; +} + +/// Removes a file if it actually looks like a temporary file. +static void gr_delete_tmp_file(const char *filename) { + if (strstr(filename, "tty-graphics-protocol") == NULL) + return; + if (strstr(filename, "/tmp/") != filename) { + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir || !tmpdir[0] || + strstr(filename, tmpdir) != filename) + return; + } + unlink(filename); +} + +/// Handles a data transmission command. +static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) { + // The default is direct transmission. + if (!cmd->transmission_medium) + cmd->transmission_medium = 'd'; + + // If neither id, nor image number is specified, and the transmission + // medium is 'd' (or unspecified), and there is an active direct upload, + // this is a continuation of the upload. + if (current_upload_image_id != 0 && cmd->image_id == 0 && + cmd->image_number == 0 && cmd->transmission_medium == 'd') { + cmd->image_id = current_upload_image_id; + GR_LOG("No images id is specified, continuing uploading %u\n", + cmd->image_id); + } + + ImageFrame *frame = NULL; + if (cmd->transmission_medium == 'f' || + cmd->transmission_medium == 't') { + // File transmission. + // Create a new image or a new frame of an existing image. + frame = gr_new_image_or_frame_from_command(cmd); + if (!frame) + return NULL; + last_image_id = frame->image->image_id; + // Decode the filename. + char *original_filename = gr_base64dec(cmd->payload, NULL); + GR_LOG("Copying image %s\n", + sanitized_filename(original_filename)); + // Stat the file and check that it's a regular file and not too + // big. + struct stat st; + int stat_res = stat(original_filename, &st); + const char *stat_error = NULL; + if (stat_res) + stat_error = strerror(errno); + else if (!S_ISREG(st.st_mode)) + stat_error = "Not a regular file"; + else if (st.st_size == 0) + stat_error = "The size of the file is zero"; + else if (st.st_size > graphics_max_single_image_file_size) + stat_error = "The file is too large"; + if (stat_error) { + gr_reporterror_cmd(cmd, + "EBADF: %s", stat_error); + fprintf(stderr, "Could not load the file %s\n", + sanitized_filename(original_filename)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + } else { + gr_make_sure_tmpdir_exists(); + // Build the filename for the cached copy of the file. + char cache_filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, cache_filename, + MAX_FILENAME_SIZE); + // We will create a symlink to the original file, and + // then copy the file to the temporary cache dir. We do + // this symlink trick mostly to be able to use cp for + // copying, and avoid escaping file name characters when + // calling system at the same time. + char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0}; + strcat(tmp_filename_symlink, cache_filename); + strcat(tmp_filename_symlink, ".sym"); + char command[MAX_FILENAME_SIZE + 256]; + size_t len = + snprintf(command, MAX_FILENAME_SIZE + 255, + "cp '%s' '%s'", tmp_filename_symlink, + cache_filename); + if (len > MAX_FILENAME_SIZE + 255 || + symlink(original_filename, tmp_filename_symlink) || + system(command) != 0) { + gr_reporterror_cmd(cmd, + "EBADF: could not copy the " + "image to the cache dir"); + fprintf(stderr, + "Could not copy the image " + "%s (symlink %s) to %s", + sanitized_filename(original_filename), + tmp_filename_symlink, cache_filename); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + } else { + // Get the file size of the copied file. + frame->status = STATUS_UPLOADING_SUCCESS; + frame->disk_size = st.st_size; + frame->image->total_disk_size += st.st_size; + images_disk_size += frame->disk_size; + if (frame->expected_size && + frame->expected_size != frame->disk_size) { + // The file has unexpected size. + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = + ERROR_UNEXPECTED_SIZE; + gr_reportuploaderror(frame); + } else { + // Everything seems fine, try to load + // and redraw existing instances. + gr_schedule_image_redraw(frame->image); + frame = gr_loadimage_and_report(frame); + } + } + // Delete the symlink. + unlink(tmp_filename_symlink); + // Delete the original file if it's temporary. + if (cmd->transmission_medium == 't') + gr_delete_tmp_file(original_filename); + } + free(original_filename); + gr_check_limits(); + } else if (cmd->transmission_medium == 'd') { + // Direct transmission (default if 't' is not specified). + frame = gr_get_last_frame(gr_find_image_for_command(cmd)); + if (frame && frame->status == STATUS_UPLOADING) { + // This is a continuation of the previous transmission. + cmd->is_direct_transmission_continuation = 1; + gr_append_data(frame, cmd->payload, cmd->more); + return frame; + } + // If no action is specified, it's not the first transmission + // command. If we couldn't find the image, something went wrong + // and we should just drop this command. + if (cmd->action == 0) + return NULL; + // Otherwise create a new image or frame structure. + frame = gr_new_image_or_frame_from_command(cmd); + if (!frame) + return NULL; + last_image_id = frame->image->image_id; + frame->status = STATUS_UPLOADING; + // Start appending data. + gr_append_data(frame, cmd->payload, cmd->more); + } else { + gr_reporterror_cmd( + cmd, + "EINVAL: transmission medium '%c' is not supported", + cmd->transmission_medium); + return NULL; + } + + return frame; +} + +/// Handles the 'put' command by creating a placement. +static void gr_handle_put_command(GraphicsCommand *cmd) { + if (cmd->image_id == 0 && cmd->image_number == 0) { + gr_reporterror_cmd(cmd, + "EINVAL: neither image id nor image number " + "are specified or both are zero"); + return; + } + + // Find the image with the id or number. + Image *img = gr_find_image_for_command(cmd); + if (!img) { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return; + } + + // Create a placement. If a placement with the same id already exists, + // it will be deleted. If the id is zero, a random id will be generated. + ImagePlacement *placement = gr_new_placement(img, cmd->placement_id); + placement->virtual = cmd->virtual; + placement->src_pix_x = cmd->src_pix_x; + placement->src_pix_y = cmd->src_pix_y; + placement->src_pix_width = cmd->src_pix_width; + placement->src_pix_height = cmd->src_pix_height; + placement->cols = cmd->columns; + placement->rows = cmd->rows; + placement->do_not_move_cursor = cmd->do_not_move_cursor; + + if (placement->virtual) { + placement->scale_mode = SCALE_MODE_CONTAIN; + } else if (placement->cols && placement->rows) { + // For classic placements the default is to stretch the image if + // both cols and rows are specified. + placement->scale_mode = SCALE_MODE_FILL; + } else if (placement->cols || placement->rows) { + // But if only one of them is specified, the default is to + // contain. + placement->scale_mode = SCALE_MODE_CONTAIN; + } else { + // If none of them are specified, the default is to use the + // original size. + placement->scale_mode = SCALE_MODE_NONE; + } + + // Display the placement unless it's virtual. + gr_display_nonvirtual_placement(placement); + + // Report success. + gr_reportsuccess_cmd(cmd); +} + +/// Information about what to delete. +typedef struct DeletionData { + uint32_t image_id; + uint32_t placement_id; + /// If true, delete the image object if there are no more placements. + char delete_image_if_no_ref; +} DeletionData; + +/// The callback called for each cell to perform deletion. +static int gr_deletion_callback(void *data, uint32_t image_id, + uint32_t placement_id, int col, + int row, char is_classic) { + DeletionData *del_data = data; + // Leave unicode placeholders alone. + if (!is_classic) + return 0; + if (del_data->image_id && del_data->image_id != image_id) + return 0; + if (del_data->placement_id && del_data->placement_id != placement_id) + return 0; + Image *img = gr_find_image(image_id); + // If the image is already deleted, just erase the placeholder. + if (!img) + return 1; + // Delete the placement. + if (placement_id) + gr_delete_placement(gr_find_placement(img, placement_id)); + // Delete the image if image deletion is requested (uppercase delete + // specifier) and there are no more placements. + if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0) + gr_delete_image(img); + return 1; +} + +/// Handles the delete command. +static void gr_handle_delete_command(GraphicsCommand *cmd) { + DeletionData del_data = {0}; + del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0; + char d = tolower(cmd->delete_specifier); + + if (d == 'n') { + d = 'i'; + Image *img = gr_find_image_by_number(cmd->image_number); + if (!img) + return; + del_data.image_id = img->image_id; + } + + if (!d || d == 'a') { + // Delete all visible placements. + gr_for_each_image_cell(gr_deletion_callback, &del_data); + } else if (d == 'i') { + // Delete the specified image by image id and maybe placement + // id. + if (!del_data.image_id) + del_data.image_id = cmd->image_id; + if (!del_data.image_id) { + fprintf(stderr, + "ERROR: image id is not specified in the " + "delete command\n"); + return; + } + del_data.placement_id = cmd->placement_id; + // NOTE: It's not very clear whether we should delete the image + // even if there are no _visible_ placements to delete. We do + // this because otherwise there is no way to delete an image + // with virtual placements in one command. + if (!del_data.placement_id && del_data.delete_image_if_no_ref) + gr_delete_image(gr_find_image(cmd->image_id)); + gr_for_each_image_cell(gr_deletion_callback, &del_data); + } else { + fprintf(stderr, + "WARNING: unsupported value of the d key: '%c'. The " + "command is ignored.\n", + cmd->delete_specifier); + } +} + +static void gr_handle_animation_control_command(GraphicsCommand *cmd) { + if (cmd->image_id == 0 && cmd->image_number == 0) { + gr_reporterror_cmd(cmd, + "EINVAL: neither image id nor image number " + "are specified or both are zero"); + return; + } + + // Find the image with the id or number. + Image *img = gr_find_image_for_command(cmd); + if (!img) { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return; + } + + // Find the frame to edit, if requested. + ImageFrame *frame = NULL; + if (cmd->edit_frame) + frame = gr_get_frame(img, cmd->edit_frame); + if (cmd->edit_frame || cmd->gap) { + if (!frame) { + gr_reporterror_cmd(cmd, "ENOENT: frame %d not found", + cmd->edit_frame); + return; + } + if (cmd->gap) { + img->total_duration -= frame->gap; + frame->gap = cmd->gap; + img->total_duration += frame->gap; + } + } + + // Set animation-related parameters of the image. + if (cmd->current_frame) + img->current_frame = cmd->current_frame; + if (cmd->animation_state) { + if (cmd->animation_state == 1) { + img->animation_state = ANIMATION_STATE_STOPPED; + } else if (cmd->animation_state == 2) { + img->animation_state = ANIMATION_STATE_LOADING; + } else if (cmd->animation_state == 3) { + img->animation_state = ANIMATION_STATE_LOOPING; + } else { + gr_reporterror_cmd( + cmd, "EINVAL: invalid animation state: %d", + cmd->animation_state); + } + } + // TODO: Set the number of loops to cmd->loops + + // Make sure we redraw all instances of the image. + gr_schedule_image_redraw(img); +} + +/// Handles a command. +static void gr_handle_command(GraphicsCommand *cmd) { + if (!cmd->image_id && !cmd->image_number) { + // If there is no image id or image number, nobody expects a + // response, so set quiet to 2. + cmd->quiet = 2; + } + ImageFrame *frame = NULL; + switch (cmd->action) { + case 0: + // If no action is specified, it may be a data transmission + // command if 'm=' is specified. + if (cmd->is_data_transmission) { + gr_handle_transmit_command(cmd); + break; + } + gr_reporterror_cmd(cmd, "EINVAL: no action specified"); + break; + case 't': + case 'q': + case 'f': + // Transmit data. 'q' means query, which is basically the same + // as transmit, but the image is discarded, and the id is fake. + // 'f' appends a frame to an existing image. + gr_handle_transmit_command(cmd); + break; + case 'p': + // Display (put) the image. + gr_handle_put_command(cmd); + break; + case 'T': + // Transmit and display. + frame = gr_handle_transmit_command(cmd); + if (frame && !cmd->is_direct_transmission_continuation) { + gr_handle_put_command(cmd); + if (cmd->placement_id) + frame->image->initial_placement_id = + cmd->placement_id; + } + break; + case 'd': + gr_handle_delete_command(cmd); + break; + case 'a': + gr_handle_animation_control_command(cmd); + break; + default: + gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c", + cmd->action); + return; + } +} + +/// A partially parsed key-value pair. +typedef struct KeyAndValue { + char *key_start; + char *val_start; + unsigned key_len, val_len; +} KeyAndValue; + +/// Parses the value of a key and assigns it to the appropriate field of `cmd`. +static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) { + char *key_start = kv->key_start; + char *key_end = key_start + kv->key_len; + char *value_start = kv->val_start; + char *value_end = value_start + kv->val_len; + // Currently all keys are one-character. + if (key_end - key_start != 1) { + gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s", + key_end - key_start, key_start); + return; + } + long num = 0; + if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' || + *key_start == 'o') { + // Some keys have one-character values. + if (value_end - value_start != 1) { + gr_reporterror_cmd( + cmd, + "EINVAL: value of 'a', 't' or 'd' must be a " + "single char: %s", + key_start); + return; + } + } else { + // All the other keys have integer values. + char *num_end = NULL; + num = strtol(value_start, &num_end, 10); + if (num_end != value_end) { + gr_reporterror_cmd( + cmd, "EINVAL: could not parse number value: %s", + key_start); + return; + } + } + switch (*key_start) { + case 'a': + cmd->action = *value_start; + break; + case 't': + cmd->transmission_medium = *value_start; + break; + case 'd': + cmd->delete_specifier = *value_start; + break; + case 'q': + cmd->quiet = num; + break; + case 'f': + cmd->format = num; + if (num != 0 && num != 24 && num != 32 && num != 100) { + gr_reporterror_cmd( + cmd, + "EINVAL: unsupported format specification: %s", + key_start); + } + break; + case 'o': + cmd->compression = *value_start; + if (cmd->compression != 'z') { + gr_reporterror_cmd(cmd, + "EINVAL: unsupported compression " + "specification: %s", + key_start); + } + break; + case 's': + if (cmd->action == 'a') + cmd->animation_state = num; + else + cmd->frame_pix_width = num; + break; + case 'v': + if (cmd->action == 'a') + cmd->loops = num; + else + cmd->frame_pix_height = num; + break; + case 'i': + cmd->image_id = num; + break; + case 'I': + cmd->image_number = num; + break; + case 'p': + cmd->placement_id = num; + break; + case 'x': + cmd->src_pix_x = num; + cmd->frame_dst_pix_x = num; + break; + case 'y': + if (cmd->action == 'f') + cmd->frame_dst_pix_y = num; + else + cmd->src_pix_y = num; + break; + case 'w': + cmd->src_pix_width = num; + break; + case 'h': + cmd->src_pix_height = num; + break; + case 'c': + if (cmd->action == 'f') + cmd->background_frame = num; + else if (cmd->action == 'a') + cmd->current_frame = num; + else + cmd->columns = num; + break; + case 'r': + if (cmd->action == 'f' || cmd->action == 'a') + cmd->edit_frame = num; + else + cmd->rows = num; + break; + case 'm': + cmd->is_data_transmission = 1; + cmd->more = num; + break; + case 'S': + cmd->size = num; + break; + case 'U': + cmd->virtual = num; + break; + case 'X': + if (cmd->action == 'f') + cmd->replace_instead_of_blending = num; + else + break; /*ignore*/ + break; + case 'Y': + if (cmd->action == 'f') + cmd->background_color = num; + else + break; /*ignore*/ + break; + case 'z': + if (cmd->action == 'f' || cmd->action == 'a') + cmd->gap = num; + else + break; /*ignore*/ + break; + case 'C': + cmd->do_not_move_cursor = num; + break; + default: + gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s", + key_start); + return; + } +} + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters. Returns 1 on success. +int gr_parse_command(char *buf, size_t len) { + if (buf[0] != 'G') + return 0; + + memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); + + global_command_counter++; + GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); + + // Eat the 'G'. + ++buf; + --len; + + GraphicsCommand cmd = {.command = buf}; + // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to + // parse the payload. + char state = 'k'; + // An array of partially parsed key-value pairs. + KeyAndValue key_vals[32]; + unsigned key_vals_count = 0; + char *key_start = buf; + char *key_end = NULL; + char *val_start = NULL; + char *val_end = NULL; + char *c = buf; + while (c - buf < len + 1) { + if (state == 'k') { + switch (*c) { + case ',': + case ';': + case '\0': + state = *c == ',' ? 'k' : 'p'; + key_end = c; + gr_reporterror_cmd( + &cmd, "EINVAL: key without value: %s ", + key_start); + break; + case '=': + key_end = c; + state = 'v'; + val_start = c + 1; + break; + default: + break; + } + } else if (state == 'v') { + switch (*c) { + case ',': + case ';': + case '\0': + state = *c == ',' ? 'k' : 'p'; + val_end = c; + if (key_vals_count >= + sizeof(key_vals) / sizeof(*key_vals)) { + gr_reporterror_cmd(&cmd, + "EINVAL: too many " + "key-value pairs"); + break; + } + key_vals[key_vals_count].key_start = key_start; + key_vals[key_vals_count].val_start = val_start; + key_vals[key_vals_count].key_len = + key_end - key_start; + key_vals[key_vals_count].val_len = + val_end - val_start; + ++key_vals_count; + key_start = c + 1; + break; + default: + break; + } + } else if (state == 'p') { + cmd.payload = c; + // break out of the loop, we don't check the payload + break; + } + ++c; + } + + // Set the action key ('a=') first because we need it to disambiguate + // some keys. Also set 'i=' and 'I=' for better error reporting. + for (unsigned i = 0; i < key_vals_count; ++i) { + if (key_vals[i].key_len == 1) { + char *start = key_vals[i].key_start; + if (*start == 'a' || *start == 'i' || *start == 'I') { + gr_set_keyvalue(&cmd, &key_vals[i]); + break; + } + } + } + // Set the rest of the keys. + for (unsigned i = 0; i < key_vals_count; ++i) + gr_set_keyvalue(&cmd, &key_vals[i]); + + if (!cmd.payload) + cmd.payload = buf + len; + + if (cmd.payload && cmd.payload[0]) + GR_LOG(" payload size: %ld\n", strlen(cmd.payload)); + + if (!graphics_command_result.error) + gr_handle_command(&cmd); + + if (graphics_debug_mode) { + fprintf(stderr, "Response: "); + for (const char *resp = graphics_command_result.response; + *resp != '\0'; ++resp) { + if (isprint(*resp)) + fprintf(stderr, "%c", *resp); + else + fprintf(stderr, "(0x%x)", *resp); + } + fprintf(stderr, "\n"); + } + + // Make sure that we suppress response if needed. Usually cmd.quiet is + // taken into account when creating the response, but it's not very + // reliable in the current implementation. + if (cmd.quiet) { + if (!graphics_command_result.error || cmd.quiet >= 2) + graphics_command_result.response[0] = '\0'; + } + + return 1; +} + +//////////////////////////////////////////////////////////////////////////////// +// base64 decoding part is basically copied from st.c +//////////////////////////////////////////////////////////////////////////////// + +static const char gr_base64_digits[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +static char gr_base64_getc(const char **src) { + while (**src && !isprint(**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char *gr_base64dec(const char *src, size_t *size) { + size_t in_len = strlen(src); + char *result, *dst; + + result = dst = malloc((in_len + 3) / 4 * 3 + 1); + while (*src) { + int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + + if (a == -1 || b == -1) + break; + + *dst++ = (a << 2) | ((b & 0x30) >> 4); + if (c == -1) + break; + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + if (d == -1) + break; + *dst++ = ((c & 0x03) << 6) | d; + } + *dst = '\0'; + if (size) { + *size = dst - result; + } + return result; +} diff --git a/graphics.h b/graphics.h new file mode 100644 index 0000000..2e75dea --- /dev/null +++ b/graphics.h @@ -0,0 +1,107 @@ + +#include +#include +#include + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm); +/// Deinitialize the graphics module. +void gr_deinit(); + +/// Add an image rectangle to a list if rectangles to draw. This function may +/// actually draw some rectangles, or it may wait till more rectangles are +/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. +/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the +/// part of the image to draw (row/col indices are zero-based, ends are +/// excluded). +/// - `x_col` and `y_row` are the coordinates of the top-left corner of the +/// image in the terminal grid. +/// - `x_pix` and `y_pix` are the same but in pixels. +/// - `reverse` indicates whether colors should be inverted. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse); +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch); +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf); +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows); + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters (including '\0'). Returns 1 on success. +/// Additional informations is returned through `graphics_command_result`. +int gr_parse_command(char *buf, size_t len); + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +void gr_preview_image(uint32_t image_id, const char *command); + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable); + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state(); + +/// Unloads images to reduce RAM usage. +void gr_unload_images_to_reduce_ram(); + +/// Executes `callback` for each image cell. `callback` may return 1 to erase +/// the cell or 0 to keep it. This function is implemented in `st.c`. +void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id, + uint32_t placement_id, int col, + int row, char is_classic), + void *data); + +/// Marks all the rows containing the image with `image_id` as dirty. +void gr_schedule_image_redraw_by_id(uint32_t image_id); + +typedef enum { + GRAPHICS_DEBUG_NONE = 0, + GRAPHICS_DEBUG_LOG = 1, + GRAPHICS_DEBUG_LOG_AND_BOXES = 2, +} GraphicsDebugMode; + +/// Print additional information, draw bounding bounding boxes, etc. +extern GraphicsDebugMode graphics_debug_mode; + +/// Whether to display images or just draw bounding boxes. +extern char graphics_display_images; + +/// The time in milliseconds until the next redraw to update animations. +/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`. +extern int graphics_next_redraw_delay; + +#define MAX_GRAPHICS_RESPONSE_LEN 256 + +/// A structure representing the result of a graphics command. +typedef struct { + /// Indicates if the terminal needs to be redrawn. + char redraw; + /// The response of the command that should be sent back to the client + /// (may be empty if the quiet flag is set). + char response[MAX_GRAPHICS_RESPONSE_LEN]; + /// Whether there was an error executing this command (not very useful, + /// the response must be sent back anyway). + char error; + /// Whether the terminal has to create a placeholder for a non-virtual + /// placement. + char create_placeholder; + /// The placeholder that needs to be created. + struct { + uint32_t rows, columns; + uint32_t image_id, placement_id; + char do_not_move_cursor; + } placeholder; +} GraphicsCommandResult; + +/// The result of a graphics command. +extern GraphicsCommandResult graphics_command_result; diff --git a/graphics.o b/graphics.o new file mode 100644 index 0000000..73551f9 Binary files /dev/null and b/graphics.o differ diff --git a/icat-mini.sh b/icat-mini.sh new file mode 100755 index 0000000..0a8ebab --- /dev/null +++ b/icat-mini.sh @@ -0,0 +1,801 @@ +#!/bin/sh + +# vim: shiftwidth=4 + +script_name="$(basename "$0")" + +short_help="Usage: $script_name [OPTIONS] + +This is a script to display images in the terminal using the kitty graphics +protocol with Unicode placeholders. It is very basic, please use something else +if you have alternatives. + +Options: + -h Show this help. + -s SCALE The scale of the image, may be floating point. + -c N, --cols N The number of columns. + -r N, --rows N The number of rows. + --max-cols N The maximum number of columns. + --max-rows N The maximum number of rows. + --cell-size WxH The cell size in pixels. + -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. + --speed SPEED The multiplier for the animation speed (float). +" + +# Exit the script on keyboard interrupt +trap "echo 'icat-mini was interrupted' >&2; exit 1" INT + +cols="" +rows="" +file="" +tty="/dev/tty" +uploading_method="auto" +cell_size="" +scale=1 +max_cols="" +max_rows="" +speed="" + +# Parse the command line. +while [ $# -gt 0 ]; do + case "$1" in + -c|--columns|--cols) + cols="$2" + shift 2 + ;; + -r|--rows|-l|--lines) + rows="$2" + shift 2 + ;; + -s|--scale) + scale="$2" + shift 2 + ;; + -h|--help) + echo "$short_help" + exit 0 + ;; + -m|--upload-method|--uploading-method) + uploading_method="$2" + shift 2 + ;; + --cell-size) + cell_size="$2" + shift 2 + ;; + --max-cols) + max_cols="$2" + shift 2 + ;; + --max-rows) + max_rows="$2" + shift 2 + ;; + --speed) + speed="$2" + shift 2 + ;; + --) + file="$2" + shift 2 + ;; + -*) + echo "Unknown option: $1" >&2 + exit 1 + ;; + *) + if [ -n "$file" ]; then + echo "Multiple image files are not supported: $file and $1" >&2 + exit 1 + fi + file="$1" + shift + ;; + esac +done + +file="$(realpath "$file")" + +##################################################################### +# Adjust the terminal state +##################################################################### + +stty_orig="$(stty -g < "$tty")" +stty -echo < "$tty" +# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some +# horrible issues otherwise. +stty susp undef < "$tty" +stty -icanon < "$tty" + +restore_echo() { + [ -n "$stty_orig" ] || return + stty $stty_orig < "$tty" +} + +trap restore_echo EXIT TERM + +##################################################################### +# Detect imagemagick +##################################################################### + +# If there is the 'magick' command, use it instead of separate 'convert' and +# 'identify' commands. +if command -v magick > /dev/null; then + identify="magick identify" + convert="magick" +else + identify="identify" + convert="convert" +fi + +##################################################################### +# Detect tmux +##################################################################### + +# Check if we are inside tmux. +inside_tmux="" +if [ -n "$TMUX" ]; then + case "$TERM" in + *tmux*|*screen*) + inside_tmux=1 + ;; + esac +fi + +##################################################################### +# Compute the number of rows and columns +##################################################################### + +is_pos_int() { + if [ -z "$1" ]; then + return 1 # false + fi + if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then + if [ "$1" -gt 0 ]; then + return 0 # true + fi + fi + return 1 # false +} + +if [ -n "$cols" ] || [ -n "$rows" ]; then + if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then + echo "You can't specify both max-cols/rows and cols/rows" >&2 + exit 1 + fi +fi + +# Get the max number of cols and rows. +[ -n "$max_cols" ] || max_cols="$(tput cols)" +[ -n "$max_rows" ] || max_rows="$(tput lines)" +if [ "$max_rows" -gt 255 ]; then + max_rows=255 +fi + +python_ioctl_command="import array, fcntl, termios +buf = array.array('H', [0, 0, 0, 0]) +fcntl.ioctl(0, termios.TIOCGWINSZ, buf) +print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" + +# Get the cell size in pixels if either cols or rows are not specified. +if [ -z "$cols" ] || [ -z "$rows" ]; then + cell_width="" + cell_height="" + # If the cell size is specified, use it. + if [ -n "$cell_size" ]; then + cell_width="${cell_size%x*}" + cell_height="${cell_size#*x}" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + echo "Invalid cell size: $cell_size" >&2 + exit 1 + fi + fi + # Otherwise try to use TIOCGWINSZ ioctl via python. + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then + cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)" + cell_width="${cell_size_ioctl% *}" + cell_height="${cell_size_ioctl#* }" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + cell_width="" + cell_height="" + fi + fi + # If it didn't work, try to use csi XTWINOPS. + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then + if [ -n "$inside_tmux" ]; then + printf '\ePtmux;\e\e[16t\e\\' >> "$tty" + else + printf '\e[16t' >> "$tty" + fi + # The expected response will look like ^[[6;;t + term_response="" + while true; do + char=$(dd bs=1 count=1 <"$tty" 2>/dev/null) + if [ "$char" = "t" ]; then + break + fi + term_response="$term_response$char" + done + cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" + cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + cell_width=8 + cell_height=16 + fi + fi +fi + +# Compute a formula with bc and round to the nearest integer. +bc_round() { + LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" +} + +# Compute the number of rows and columns of the image. +if [ -z "$cols" ] || [ -z "$rows" ]; then + # Get the size of the image and its resolution. If it's an animation, use + # the first frame. + format_output="$($identify -format '%w %h\n' "$file" | head -1)" + img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" + img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" + if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then + echo "Couldn't get image size from identify: $format_output" >&2 + echo >&2 + exit 1 + fi + opt_cols_expr="(${scale}*${img_width}/${cell_width})" + opt_rows_expr="(${scale}*${img_height}/${cell_height})" + if [ -z "$cols" ] && [ -z "$rows" ]; then + # If columns and rows are not specified, compute the optimal values + # using the information about rows and columns per inch. + cols="$(bc_round "$opt_cols_expr")" + rows="$(bc_round "$opt_rows_expr")" + # Make sure that automatically computed rows and columns are within some + # sane limits + if [ "$cols" -gt "$max_cols" ]; then + rows="$(bc_round "$rows * $max_cols / $cols")" + cols="$max_cols" + fi + if [ "$rows" -gt "$max_rows" ]; then + cols="$(bc_round "$cols * $max_rows / $rows")" + rows="$max_rows" + fi + elif [ -z "$cols" ]; then + # If only one dimension is specified, compute the other one to match the + # aspect ratio as close as possible. + cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" + elif [ -z "$rows" ]; then + rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" + fi + + if [ "$cols" -lt 1 ]; then + cols=1 + fi + if [ "$rows" -lt 1 ]; then + rows=1 + fi +fi + +##################################################################### +# Generate an image id +##################################################################### + +image_id="" +while [ -z "$image_id" ]; do + image_id="$(shuf -i 16777217-4294967295 -n 1)" + # Check that the id requires 24-bit fg colors. + if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then + image_id="" + fi +done + +##################################################################### +# Uploading the image +##################################################################### + +# Choose the uploading method +if [ "$uploading_method" = "auto" ]; then + if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then + uploading_method="direct" + else + uploading_method="file" + fi +fi + +# Functions to emit the start and the end of a graphics command. +if [ -n "$inside_tmux" ]; then + # If we are in tmux we have to wrap the command in Ptmux. + graphics_command_start='\ePtmux;\e\e_G' + graphics_command_end='\e\e\\\e\\' +else + graphics_command_start='\e_G' + graphics_command_end='\e\\' +fi + +start_gr_command() { + printf "$graphics_command_start" >> "$tty" +} +end_gr_command() { + printf "$graphics_command_end" >> "$tty" +} + +# Send a graphics command with the correct start and end +gr_command() { + start_gr_command + printf '%s' "$1" >> "$tty" + end_gr_command +} + +# Send an uploading command. Usage: gr_upload +# Where is a part of command that specifies the action, it will be +# repeated for every chunk (if the method is direct), and is the rest +# of the command that specifies the image parameters. and +# must not include the transmission method or ';'. +# Example: +# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" +gr_upload() { + arg_action="$1" + arg_command="$2" + arg_file="$3" + if [ "$uploading_method" = "file" ]; then + # base64-encode the filename + encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) + gr_command "${arg_action},${arg_command},t=f;${encoded_filename}" + fi + if [ "$uploading_method" = "direct" ]; then + # Create a temporary directory to store the chunked image. + chunkdir="$(mktemp -d)" + if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then + echo "Can't create a temp dir" >&2 + exit 1 + fi + # base64-encode the file and split it into chunks. The size of each + # graphics command shouldn't be more than 4096, so we set the size of an + # encoded chunk to be 3968, slightly less than that. + chunk_size=3968 + cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" + + # Issue a command indicating that we want to start data transmission for + # a new image. + gr_command "${arg_action},${arg_command},t=d,m=1" + + # Transmit chunks. + for chunk in "$chunkdir/chunk_"*; do + start_gr_command + printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty" + cat "$chunk" >> "$tty" + end_gr_command + rm "$chunk" + done + + # Tell the terminal that we are done. + gr_command "${arg_action},i=$image_id,m=0" + + # Remove the temporary directory. + rmdir "$chunkdir" + fi +} + +delayed_frame_dir_cleanup() { + arg_frame_dir="$1" + sleep 2 + if [ -n "$arg_frame_dir" ]; then + for frame in "$arg_frame_dir"/frame_*.png; do + rm "$frame" + done + rmdir "$arg_frame_dir" + fi +} + +upload_image_and_print_placeholder() { + # Check if the file is an animation. + frame_count=$($identify -format '%n\n' "$file" | head -n 1) + if [ "$frame_count" -gt 1 ]; then + # The file is an animation, decompose into frames and upload each frame. + frame_dir="$(mktemp -d)" + frame_dir="$HOME/temp/frames${frame_dir}" + mkdir -p "$frame_dir" + if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then + echo "Can't create a temp dir for frames" >&2 + exit 1 + fi + + # Decompose the animation into separate frames. + $convert "$file" -coalesce "$frame_dir/frame_%06d.png" + + # Get all frame delays at once, in centiseconds, as a space-separated + # string. + delays=$($identify -format "%T " "$file") + + frame_number=1 + for frame in "$frame_dir"/frame_*.png; do + # Read the delay for the current frame and convert it from + # centiseconds to milliseconds. + delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") + delay=$((delay * 10)) + # If the delay is 0, set it to 100ms. + if [ "$delay" -eq 0 ]; then + delay=100 + fi + + if [ -n "$speed" ]; then + delay=$(bc_round "$delay / $speed") + fi + + if [ "$frame_number" -eq 1 ]; then + # Upload the first frame with a=T + gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" + # Set the delay for the first frame and also play the animation + # in loading mode (s=2). + gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" + # Print the placeholder after the first frame to reduce the wait + # time. + print_placeholder + else + # Upload subsequent frames with a=f + gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" + fi + + frame_number=$((frame_number + 1)) + done + + # Play the animation in loop mode (s=3). + gr_command "a=a,v=1,s=3,i=${image_id}" + + # Remove the temporary directory, but do it in the background with a + # delay to avoid removing files before they are loaded by the terminal. + delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & + else + # The file is not an animation, upload it directly + gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" + # Print the placeholder + print_placeholder + fi +} + +##################################################################### +# Printing the image placeholder +##################################################################### + +print_placeholder() { + # Each line starts with the escape sequence to set the foreground color to + # the image id. + blue="$(expr "$image_id" % 256 )" + green="$(expr \( "$image_id" / 256 \) % 256 )" + red="$(expr \( "$image_id" / 65536 \) % 256 )" + line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" + line_end="$(printf "\e[39;m")" + + id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" + eval "id_diacritic=\$d${id4th}" + + # Reset the brush state, mostly to reset the underline color. + printf "\e[0m" + + # Fill the output with characters representing the image + for y in $(seq 0 "$(expr "$rows" - 1)"); do + eval "row_diacritic=\$d${y}" + printf '%s' "$line_start" + for x in $(seq 0 "$(expr "$cols" - 1)"); do + eval "col_diacritic=\$d${x}" + # Note that when $x is out of bounds, the column diacritic will + # be empty, meaning that the column should be guessed by the + # terminal. + if [ "$x" -ge "$num_diacritics" ]; then + printf '%s' "${placeholder}${row_diacritic}" + else + printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" + fi + done + printf '%s\n' "$line_end" + done + + printf "\e[0m" +} + +d0="̅" +d1="̍" +d2="̎" +d3="̐" +d4="̒" +d5="̽" +d6="̾" +d7="̿" +d8="͆" +d9="͊" +d10="͋" +d11="͌" +d12="͐" +d13="͑" +d14="͒" +d15="͗" +d16="͛" +d17="ͣ" +d18="ͤ" +d19="ͥ" +d20="ͦ" +d21="ͧ" +d22="ͨ" +d23="ͩ" +d24="ͪ" +d25="ͫ" +d26="ͬ" +d27="ͭ" +d28="ͮ" +d29="ͯ" +d30="҃" +d31="҄" +d32="҅" +d33="҆" +d34="҇" +d35="֒" +d36="֓" +d37="֔" +d38="֕" +d39="֗" +d40="֘" +d41="֙" +d42="֜" +d43="֝" +d44="֞" +d45="֟" +d46="֠" +d47="֡" +d48="֨" +d49="֩" +d50="֫" +d51="֬" +d52="֯" +d53="ׄ" +d54="ؐ" +d55="ؑ" +d56="ؒ" +d57="ؓ" +d58="ؔ" +d59="ؕ" +d60="ؖ" +d61="ؗ" +d62="ٗ" +d63="٘" +d64="ٙ" +d65="ٚ" +d66="ٛ" +d67="ٝ" +d68="ٞ" +d69="ۖ" +d70="ۗ" +d71="ۘ" +d72="ۙ" +d73="ۚ" +d74="ۛ" +d75="ۜ" +d76="۟" +d77="۠" +d78="ۡ" +d79="ۢ" +d80="ۤ" +d81="ۧ" +d82="ۨ" +d83="۫" +d84="۬" +d85="ܰ" +d86="ܲ" +d87="ܳ" +d88="ܵ" +d89="ܶ" +d90="ܺ" +d91="ܽ" +d92="ܿ" +d93="݀" +d94="݁" +d95="݃" +d96="݅" +d97="݇" +d98="݉" +d99="݊" +d100="߫" +d101="߬" +d102="߭" +d103="߮" +d104="߯" +d105="߰" +d106="߱" +d107="߳" +d108="ࠖ" +d109="ࠗ" +d110="࠘" +d111="࠙" +d112="ࠛ" +d113="ࠜ" +d114="ࠝ" +d115="ࠞ" +d116="ࠟ" +d117="ࠠ" +d118="ࠡ" +d119="ࠢ" +d120="ࠣ" +d121="ࠥ" +d122="ࠦ" +d123="ࠧ" +d124="ࠩ" +d125="ࠪ" +d126="ࠫ" +d127="ࠬ" +d128="࠭" +d129="॑" +d130="॓" +d131="॔" +d132="ྂ" +d133="ྃ" +d134="྆" +d135="྇" +d136="፝" +d137="፞" +d138="፟" +d139="៝" +d140="᤺" +d141="ᨗ" +d142="᩵" +d143="᩶" +d144="᩷" +d145="᩸" +d146="᩹" +d147="᩺" +d148="᩻" +d149="᩼" +d150="᭫" +d151="᭭" +d152="᭮" +d153="᭯" +d154="᭰" +d155="᭱" +d156="᭲" +d157="᭳" +d158="᳐" +d159="᳑" +d160="᳒" +d161="᳚" +d162="᳛" +d163="᳠" +d164="᷀" +d165="᷁" +d166="᷃" +d167="᷄" +d168="᷅" +d169="᷆" +d170="᷇" +d171="᷈" +d172="᷉" +d173="᷋" +d174="᷌" +d175="᷑" +d176="᷒" +d177="ᷓ" +d178="ᷔ" +d179="ᷕ" +d180="ᷖ" +d181="ᷗ" +d182="ᷘ" +d183="ᷙ" +d184="ᷚ" +d185="ᷛ" +d186="ᷜ" +d187="ᷝ" +d188="ᷞ" +d189="ᷟ" +d190="ᷠ" +d191="ᷡ" +d192="ᷢ" +d193="ᷣ" +d194="ᷤ" +d195="ᷥ" +d196="ᷦ" +d197="᷾" +d198="⃐" +d199="⃑" +d200="⃔" +d201="⃕" +d202="⃖" +d203="⃗" +d204="⃛" +d205="⃜" +d206="⃡" +d207="⃧" +d208="⃩" +d209="⃰" +d210="⳯" +d211="⳰" +d212="⳱" +d213="ⷠ" +d214="ⷡ" +d215="ⷢ" +d216="ⷣ" +d217="ⷤ" +d218="ⷥ" +d219="ⷦ" +d220="ⷧ" +d221="ⷨ" +d222="ⷩ" +d223="ⷪ" +d224="ⷫ" +d225="ⷬ" +d226="ⷭ" +d227="ⷮ" +d228="ⷯ" +d229="ⷰ" +d230="ⷱ" +d231="ⷲ" +d232="ⷳ" +d233="ⷴ" +d234="ⷵ" +d235="ⷶ" +d236="ⷷ" +d237="ⷸ" +d238="ⷹ" +d239="ⷺ" +d240="ⷻ" +d241="ⷼ" +d242="ⷽ" +d243="ⷾ" +d244="ⷿ" +d245="꙯" +d246="꙼" +d247="꙽" +d248="꛰" +d249="꛱" +d250="꣠" +d251="꣡" +d252="꣢" +d253="꣣" +d254="꣤" +d255="꣥" +d256="꣦" +d257="꣧" +d258="꣨" +d259="꣩" +d260="꣪" +d261="꣫" +d262="꣬" +d263="꣭" +d264="꣮" +d265="꣯" +d266="꣰" +d267="꣱" +d268="ꪰ" +d269="ꪲ" +d270="ꪳ" +d271="ꪷ" +d272="ꪸ" +d273="ꪾ" +d274="꪿" +d275="꫁" +d276="︠" +d277="︡" +d278="︢" +d279="︣" +d280="︤" +d281="︥" +d282="︦" +d283="𐨏" +d284="𐨸" +d285="𝆅" +d286="𝆆" +d287="𝆇" +d288="𝆈" +d289="𝆉" +d290="𝆪" +d291="𝆫" +d292="𝆬" +d293="𝆭" +d294="𝉂" +d295="𝉃" +d296="𝉄" + +num_diacritics="297" + +placeholder="􎻮" + +##################################################################### +# Upload the image and print the placeholder +##################################################################### + +upload_image_and_print_placeholder diff --git a/khash.h b/khash.h new file mode 100644 index 0000000..f75f347 --- /dev/null +++ b/khash.h @@ -0,0 +1,627 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include +#include +#include + +/* compiler specific configuration */ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash set containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ diff --git a/kvec.h b/kvec.h new file mode 100644 index 0000000..10f1c5b --- /dev/null +++ b/kvec.h @@ -0,0 +1,90 @@ +/* The MIT License + + Copyright (c) 2008, by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "kvec.h" +int main() { + kvec_t(int) array; + kv_init(array); + kv_push(int, array, 10); // append + kv_a(int, array, 20) = 5; // dynamic + kv_A(array, 20) = 4; // static + kv_destroy(array); + return 0; +} +*/ + +/* + 2008-09-22 (0.1.0): + + * The initial version. + +*/ + +#ifndef AC_KVEC_H +#define AC_KVEC_H + +#include + +#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) + +#define kvec_t(type) struct { size_t n, m; type *a; } +#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) +#define kv_destroy(v) free((v).a) +#define kv_A(v, i) ((v).a[(i)]) +#define kv_pop(v) ((v).a[--(v).n]) +#define kv_size(v) ((v).n) +#define kv_max(v) ((v).m) + +#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) + +#define kv_copy(type, v1, v0) do { \ + if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ + (v1).n = (v0).n; \ + memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ + } while (0) \ + +#define kv_push(type, v, x) do { \ + if ((v).n == (v).m) { \ + (v).m = (v).m? (v).m<<1 : 2; \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ + } \ + (v).a[(v).n++] = (x); \ + } while (0) + +#define kv_pushp(type, v) ((((v).n == (v).m)? \ + ((v).m = ((v).m? (v).m<<1 : 2), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : 0), ((v).a + ((v).n++))) + +#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ + ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ + : 0), (v).a[(i)]) + +#endif diff --git a/patches/alpha.diff b/patches/alpha.diff new file mode 100644 index 0000000..6913d19 --- /dev/null +++ b/patches/alpha.diff @@ -0,0 +1,129 @@ +diff --git a/config.def.h b/config.def.h +index 2cd740a..019a4e1 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -93,6 +93,9 @@ char *termname = "st-256color"; + */ + unsigned int tabspaces = 8; + ++/* bg opacity */ ++float alpha = 0.8; ++ + /* Terminal colors (16 first used in escape sequence) */ + static const char *colorname[] = { + /* 8 normal colors */ +diff --git a/x.c b/x.c +index d73152b..f32fd6c 100644 +--- a/x.c ++++ b/x.c +@@ -105,6 +105,7 @@ typedef struct { + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ ++ int depth; /* bit depth */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ + } XWindow; +@@ -752,7 +753,7 @@ xresize(int col, int row) + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.depth); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + +@@ -812,6 +813,10 @@ xloadcols(void) + else + die("could not allocate color %d\n", i); + } ++ ++ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); ++ dc.col[defaultbg].pixel &= 0x00FFFFFF; ++ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; + loaded = 1; + } + +@@ -842,6 +847,12 @@ xsetcolorname(int x, const char *name) + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + ++ if (x == defaultbg) { ++ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); ++ dc.col[defaultbg].pixel &= 0x00FFFFFF; ++ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; ++ } ++ + return 0; + } + +@@ -1134,11 +1145,25 @@ xinit(int cols, int rows) + Window parent, root; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; ++ XWindowAttributes attr; ++ XVisualInfo vis; + + if (!(xw.dpy = XOpenDisplay(NULL))) + die("can't open display\n"); + xw.scr = XDefaultScreen(xw.dpy); +- xw.vis = XDefaultVisual(xw.dpy, xw.scr); ++ ++ root = XRootWindow(xw.dpy, xw.scr); ++ if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) ++ parent = root; ++ ++ if (XMatchVisualInfo(xw.dpy, xw.scr, 32, TrueColor, &vis) != 0) { ++ xw.vis = vis.visual; ++ xw.depth = vis.depth; ++ } else { ++ XGetWindowAttributes(xw.dpy, parent, &attr); ++ xw.vis = attr.visual; ++ xw.depth = attr.depth; ++ } + + /* font */ + if (!FcInit()) +@@ -1148,7 +1173,7 @@ xinit(int cols, int rows) + xloadfonts(usedfont, 0); + + /* colors */ +- xw.cmap = XDefaultColormap(xw.dpy, xw.scr); ++ xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None); + xloadcols(); + + /* adjust fixed window geometry */ +@@ -1168,11 +1193,8 @@ xinit(int cols, int rows) + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + +- root = XRootWindow(xw.dpy, xw.scr); +- if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) +- parent = root; +- xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, +- win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, ++ xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t, ++ win.w, win.h, 0, xw.depth, InputOutput, + xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &xw.attrs); + if (parent != root) +@@ -1183,7 +1205,7 @@ xinit(int cols, int rows) + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.depth); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + +@@ -2047,6 +2069,10 @@ main(int argc, char *argv[]) + case 'a': + allowaltscreen = 0; + break; ++ case 'A': ++ alpha = strtof(EARGF(usage()), NULL); ++ LIMIT(alpha, 0.0, 1.0); ++ break; + case 'c': + opt_class = EARGF(usage()); + break; diff --git a/patches/changealpha.diff b/patches/changealpha.diff new file mode 100644 index 0000000..172969f --- /dev/null +++ b/patches/changealpha.diff @@ -0,0 +1,80 @@ +diff --git a/config.def.h b/config.def.h +index 91ab8ca..8a06176 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -93,6 +93,9 @@ char *termname = "st-256color"; + */ + unsigned int tabspaces = 8; + ++/* Background opacity */ ++float alpha_def; ++ + /* Terminal colors (16 first used in escape sequence) */ + static const char *colorname[] = { + /* 8 normal colors */ +@@ -201,6 +204,9 @@ static Shortcut shortcuts[] = { + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, ++ { MODKEY, XK_bracketleft, chgalpha, {.f = -1} }, /* Decrease opacity */ ++ { MODKEY|ShiftMask, XK_braceright, chgalpha, {.f = +1} }, /* Increase opacity */ ++ { MODKEY, XK_bracketright,chgalpha, {.f = 0} }, /* Reset opacity */ + }; + + /* +diff --git a/st.h b/st.h +index fd3b0d8..3bb587e 100644 +--- a/st.h ++++ b/st.h +@@ -124,3 +124,4 @@ extern unsigned int tabspaces; + extern unsigned int defaultfg; + extern unsigned int defaultbg; + extern unsigned int defaultcs; ++extern float alpha_def; +diff --git a/x.c b/x.c +index aa09997..f8c8c1a 100644 +--- a/x.c ++++ b/x.c +@@ -59,6 +59,7 @@ static void zoom(const Arg *); + static void zoomabs(const Arg *); + static void zoomreset(const Arg *); + static void ttysend(const Arg *); ++static void chgalpha(const Arg *); + + /* config.h for applying patches and the configuration. */ + #include "config.h" +@@ -1147,6 +1148,9 @@ xinit(int cols, int rows) + usedfont = (opt_font == NULL)? font : opt_font; + xloadfonts(usedfont, 0); + ++ /* Backup default alpha value */ ++ alpha_def = alpha; ++ + /* colors */ + xw.cmap = XDefaultColormap(xw.dpy, xw.scr); + xloadcols(); +@@ -1371,6 +1375,24 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + return numspecs; + } + ++void ++chgalpha(const Arg *arg) ++{ ++ if (arg->f == -1.0f && alpha >= 0.1f) ++ alpha -= 0.1f; ++ else if (arg->f == 1.0f && alpha < 1.0f) ++ alpha += 0.1f; ++ else if (arg->f == 0.0f) ++ alpha = alpha_def; ++ else ++ return; ++ ++ dc.col[defaultbg].color.alpha = (unsigned short)(0xFFFF * alpha); ++ /* Required to remove artifacting from borderpx */ ++ cresize(0, 0); ++ redraw(); ++} ++ + void + xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) + { diff --git a/patches/disable_bold.diff b/patches/disable_bold.diff new file mode 100644 index 0000000..385f1b4 --- /dev/null +++ b/patches/disable_bold.diff @@ -0,0 +1,70 @@ +From 0856fbfcdae3f8e48db791984591b0bb8a91de68 Mon Sep 17 00:00:00 2001 +From: Ryan Kes +Date: Fri, 29 Mar 2019 10:59:09 +0100 +Subject: [PATCH] st-disable-bold-italic-fonts-0.8.2 + +--- + config.def.h | 6 ++++++ + x.c | 10 +++++++++- + 2 files changed, 15 insertions(+), 1 deletion(-) + +diff --git a/config.def.h b/config.def.h +index 482901e..4f5aeac 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -6,6 +6,12 @@ + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ + static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; ++ ++/* disable bold, italic and roman fonts globally */ ++int disablebold = 0; ++int disableitalic = 0; ++int disableroman = 0; ++ + static int borderpx = 2; + + /* +diff --git a/x.c b/x.c +index 5828a3b..9663fa6 100644 +--- a/x.c ++++ b/x.c +@@ -233,6 +233,11 @@ static char *usedfont = NULL; + static double usedfontsize = 0; + static double defaultfontsize = 0; + ++/* declared in config.h */ ++extern int disablebold; ++extern int disableitalic; ++extern int disableroman; ++ + static char *opt_class = NULL; + static char **opt_cmd = NULL; + static char *opt_embed = NULL; +@@ -966,17 +971,20 @@ xloadfonts(char *fontstr, double fontsize) + win.ch = ceilf(dc.font.height * chscale); + + FcPatternDel(pattern, FC_SLANT); +- FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); ++ if (!disableitalic) ++ FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadfont(&dc.ifont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_WEIGHT); +- FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); ++ if (!disablebold) ++ FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadfont(&dc.ibfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_SLANT); +- FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); ++ if (!disableroman) ++ FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadfont(&dc.bfont, pattern)) + die("can't open font %s\n", fontstr); + +-- +2.39.2 + diff --git a/patches/kitty-graphics.diff b/patches/kitty-graphics.diff new file mode 100644 index 0000000..6049fe2 --- /dev/null +++ b/patches/kitty-graphics.diff @@ -0,0 +1,7324 @@ +From 25d9cca81ce48141de7f6a823b006dddaafd9de8 Mon Sep 17 00:00:00 2001 +From: Sergei Grechanik +Date: Sun, 22 Sep 2024 08:36:05 -0700 +Subject: [PATCH] Kitty graphics protocol support 7b717e3 2024-09-22 + +This patch implements the kitty graphics protocol in st. +See https://github.com/sergei-grechanik/st-graphics +Created by squashing the graphics branch, the most recent +commit is 7b717e38b1739e11356b34df9fdfdfa339960864 (2024-09-22). + +Squashed on top of a0274bc20e11d8672bb2953fdd1d3010c0e708c5 + +Note that the following files were excluded from the squash: + .clang-format + README.md + generate-rowcolumn-helpers.py + rowcolumn-diacritics.txt + rowcolumn_diacritics.sh +--- + Makefile | 4 +- + config.def.h | 46 +- + config.mk | 5 +- + graphics.c | 3812 ++++++++++++++++++++++++++++++++ + graphics.h | 107 + + icat-mini.sh | 801 +++++++ + khash.h | 627 ++++++ + kvec.h | 90 + + rowcolumn_diacritics_helpers.c | 391 ++++ + st.c | 279 ++- + st.h | 84 +- + st.info | 6 + + win.h | 3 + + x.c | 411 +++- + 14 files changed, 6615 insertions(+), 51 deletions(-) + create mode 100644 graphics.c + create mode 100644 graphics.h + create mode 100755 icat-mini.sh + create mode 100644 khash.h + create mode 100644 kvec.h + create mode 100644 rowcolumn_diacritics_helpers.c + +diff --git a/Makefile b/Makefile +index 15db421..e79b89e 100644 +--- a/Makefile ++++ b/Makefile +@@ -4,7 +4,7 @@ + + include config.mk + +-SRC = st.c x.c ++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c + OBJ = $(SRC:.c=.o) + + all: st +@@ -16,7 +16,7 @@ config.h: + $(CC) $(STCFLAGS) -c $< + + st.o: config.h st.h win.h +-x.o: arg.h config.h st.h win.h ++x.o: arg.h config.h st.h win.h graphics.h + + $(OBJ): config.h config.mk + +diff --git a/config.def.h b/config.def.h +index 2cd740a..4aadbbc 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -8,6 +8,13 @@ + static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; + static int borderpx = 2; + ++/* How to align the content in the window when the size of the terminal ++ * doesn't perfectly match the size of the window. The values are percentages. ++ * 50 means center, 0 means flush left/top, 100 means flush right/bottom. ++ */ ++static int anysize_halign = 50; ++static int anysize_valign = 50; ++ + /* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e +@@ -23,7 +30,8 @@ char *scroll = NULL; + char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + + /* identification sequence returned in DA and DECID */ +-char *vtiden = "\033[?6c"; ++/* By default, use the same one as kitty. */ ++char *vtiden = "\033[?62c"; + + /* Kerning / character bounding-box multipliers */ + static float cwscale = 1.0; +@@ -163,6 +171,28 @@ static unsigned int mousebg = 0; + */ + static unsigned int defaultattr = 11; + ++/* ++ * Graphics configuration ++ */ ++ ++/// The template for the cache directory. ++const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX"; ++/// The max size of a single image file, in bytes. ++unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024; ++/// The max size of the cache, in bytes. ++unsigned graphics_total_file_cache_size = 300 * 1024 * 1024; ++/// The max ram size of an image or placement, in bytes. ++unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024; ++/// The max total size of all images loaded into RAM. ++unsigned graphics_max_total_ram_size = 300 * 1024 * 1024; ++/// The max total number of image placements and images. ++unsigned graphics_max_total_placements = 4096; ++/// The ratio by which limits can be exceeded. This is to reduce the frequency ++/// of image removal. ++double graphics_excess_tolerance_ratio = 0.05; ++/// The minimum delay between redraws caused by animations, in milliseconds. ++unsigned graphics_animation_min_delay = 20; ++ + /* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other +@@ -170,12 +200,18 @@ static unsigned int defaultattr = 11; + */ + static uint forcemousemod = ShiftMask; + ++/* Internal keyboard shortcuts. */ ++#define MODKEY Mod1Mask ++#define TERMMOD (ControlMask|ShiftMask) ++ + /* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ + static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ ++ { TERMMOD, Button3, previewimage, {.s = "feh"} }, ++ { TERMMOD, Button2, showimageinfo, {}, 1 }, + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, +@@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = { + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, + }; + +-/* Internal keyboard shortcuts. */ +-#define MODKEY Mod1Mask +-#define TERMMOD (ControlMask|ShiftMask) +- + static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, +@@ -201,6 +233,10 @@ static Shortcut shortcuts[] = { + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, ++ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} }, ++ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, ++ { TERMMOD, XK_F7, unloadimages, {.i = 0} }, ++ { TERMMOD, XK_F8, toggleimages, {.i = 0} }, + }; + + /* +diff --git a/config.mk b/config.mk +index fdc29a7..cb2875c 100644 +--- a/config.mk ++++ b/config.mk +@@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config + + # includes and libs + INCS = -I$(X11INC) \ ++ `$(PKG_CONFIG) --cflags imlib2` \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +-LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ ++LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \ ++ `$(PKG_CONFIG) --libs imlib2` \ ++ `$(PKG_CONFIG) --libs zlib` \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +diff --git a/graphics.c b/graphics.c +new file mode 100644 +index 0000000..64e6fe0 +--- /dev/null ++++ b/graphics.c +@@ -0,0 +1,3812 @@ ++/* The MIT License ++ ++ Copyright (c) 2021-2024 Sergei Grechanik ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// This file implements a subset of the kitty graphics protocol. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define _POSIX_C_SOURCE 200809L ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "graphics.h" ++#include "khash.h" ++#include "kvec.h" ++ ++extern char **environ; ++ ++#define MAX_FILENAME_SIZE 256 ++#define MAX_INFO_LEN 256 ++#define MAX_IMAGE_RECTS 20 ++ ++/// The type used in this file to represent time. Used both for time differences ++/// and absolute times (as milliseconds since an arbitrary point in time, see ++/// `initialization_time`). ++typedef int64_t Milliseconds; ++ ++enum ScaleMode { ++ SCALE_MODE_UNSET = 0, ++ /// Stretch or shrink the image to fill the box, ignoring aspect ratio. ++ SCALE_MODE_FILL = 1, ++ /// Preserve aspect ratio and fit to width or to height so that the ++ /// whole image is visible. ++ SCALE_MODE_CONTAIN = 2, ++ /// Do not scale. The image may be cropped if the box is too small. ++ SCALE_MODE_NONE = 3, ++ /// Do not scale, unless the box is too small, in which case the image ++ /// will be shrunk like with `SCALE_MODE_CONTAIN`. ++ SCALE_MODE_NONE_OR_CONTAIN = 4, ++}; ++ ++enum AnimationState { ++ ANIMATION_STATE_UNSET = 0, ++ /// The animation is stopped. Display the current frame, but don't ++ /// advance to the next one. ++ ANIMATION_STATE_STOPPED = 1, ++ /// Run the animation to then end, then wait for the next frame. ++ ANIMATION_STATE_LOADING = 2, ++ /// Run the animation in a loop. ++ ANIMATION_STATE_LOOPING = 3, ++}; ++ ++/// The status of an image. Each image uploaded to the terminal is cached on ++/// disk, then it is loaded to ram when needed. ++enum ImageStatus { ++ STATUS_UNINITIALIZED = 0, ++ STATUS_UPLOADING = 1, ++ STATUS_UPLOADING_ERROR = 2, ++ STATUS_UPLOADING_SUCCESS = 3, ++ STATUS_RAM_LOADING_ERROR = 4, ++ STATUS_RAM_LOADING_IN_PROGRESS = 5, ++ STATUS_RAM_LOADING_SUCCESS = 6, ++}; ++ ++const char *image_status_strings[6] = { ++ "STATUS_UNINITIALIZED", ++ "STATUS_UPLOADING", ++ "STATUS_UPLOADING_ERROR", ++ "STATUS_UPLOADING_SUCCESS", ++ "STATUS_RAM_LOADING_ERROR", ++ "STATUS_RAM_LOADING_SUCCESS", ++}; ++ ++enum ImageUploadingFailure { ++ ERROR_OVER_SIZE_LIMIT = 1, ++ ERROR_CANNOT_OPEN_CACHED_FILE = 2, ++ ERROR_UNEXPECTED_SIZE = 3, ++ ERROR_CANNOT_COPY_FILE = 4, ++}; ++ ++const char *image_uploading_failure_strings[5] = { ++ "NO_ERROR", ++ "ERROR_OVER_SIZE_LIMIT", ++ "ERROR_CANNOT_OPEN_CACHED_FILE", ++ "ERROR_UNEXPECTED_SIZE", ++ "ERROR_CANNOT_COPY_FILE", ++}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// We use the following structures to represent images and placements: ++// ++// - Image: this is the main structure representing an image, usually created ++// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id, ++// specified by 'i='). An image may have multiple frames (ImageFrame) and ++// placements (ImagePlacement). ++// ++// - ImageFrame: represents a single frame of an image, usually created by ++// the action 'a=f' (and the first frame is created with the image itself). ++// Each frame has an index and also: ++// - a file containing the frame data (considered to be "on disk", although ++// it's probably in tmpfs), ++// - an imlib object containing the fully composed frame (i.e. the frame ++// data from the file composed onto the background frame or color). It is ++// not ready for display yet, because it needs to be scaled and uploaded ++// to the X server. ++// ++// - ImagePlacement: represents a placement of an image, created by 'a=p' and ++// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also ++// each placement has an array of pixmaps: one for each frame of the image. ++// Each pixmap is a scaled and uploaded image ready to be displayed. ++// ++// Images are store in the `images` hash table, mapping image ids to Image ++// objects (allocated on the heap). ++// ++// Placements are stored in the `placements` hash table of each Image object, ++// mapping placement ids to ImagePlacement objects (also allocated on the heap). ++// ++// ImageFrames are stored in the `first_frame` field and in the ++// `frames_beyond_the_first` array of each Image object. They are stored by ++// value, so ImageFrame pointer may be invalidated when frames are ++// added/deleted, be careful. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++struct Image; ++struct ImageFrame; ++struct ImagePlacement; ++ ++KHASH_MAP_INIT_INT(id2image, struct Image *) ++KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *) ++ ++typedef struct ImageFrame { ++ /// The image this frame belongs to. ++ struct Image *image; ++ /// The 1-based index of the frame. Zero if the frame isn't initialized. ++ int index; ++ /// The last time when the frame was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The background color of the frame in the 0xRRGGBBAA format. ++ uint32_t background_color; ++ /// The index of the background frame. Zero to use the color instead. ++ int background_frame_index; ++ /// The duration of the frame in milliseconds. ++ int gap; ++ /// The expected size of the frame image file (specified with 'S='), ++ /// used to check if uploading succeeded. ++ unsigned expected_size; ++ /// Format specification (see the `f=` key). ++ int format; ++ /// Pixel width and height of the non-composed (on-disk) frame data. May ++ /// differ from the image (i.e. first frame) dimensions. ++ int data_pix_width, data_pix_height; ++ /// The offset of the frame relative to the first frame. ++ int x, y; ++ /// Compression mode (see the `o=` key). ++ char compression; ++ /// The status (see `ImageStatus`). ++ char status; ++ /// The reason of uploading failure (see `ImageUploadingFailure`). ++ char uploading_failure; ++ /// Whether failures and successes should be reported ('q='). ++ char quiet; ++ /// Whether to blend the frame with the background or replace it. ++ char blend; ++ /// The file corresponding to the on-disk cache, used when uploading. ++ FILE *open_file; ++ /// The size of the corresponding file cached on disk. ++ unsigned disk_size; ++ /// The imlib object containing the fully composed frame. It's not ++ /// scaled for screen display yet. ++ Imlib_Image imlib_object; ++} ImageFrame; ++ ++typedef struct Image { ++ /// The client id (the one specified with 'i='). Must be nonzero. ++ uint32_t image_id; ++ /// The client id specified in the query command (`a=q`). This one must ++ /// be used to create the response if it's non-zero. ++ uint32_t query_id; ++ /// The number specified in the transmission command (`I=`). If ++ /// non-zero, it may be used to identify the image instead of the ++ /// image_id, and it also should be mentioned in responses. ++ uint32_t image_number; ++ /// The last time when the image was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The total duration of the animation in milliseconds. ++ int total_duration; ++ /// The total size of cached image files for all frames. ++ int total_disk_size; ++ /// The global index of the creation command. Used to decide which image ++ /// is newer if they have the same image number. ++ uint64_t global_command_index; ++ /// The 1-based index of the currently displayed frame. ++ int current_frame; ++ /// The state of the animation, see `AnimationState`. ++ char animation_state; ++ /// The absolute time that is assumed to be the start of the current ++ /// frame (in ms since initialization). ++ Milliseconds current_frame_time; ++ /// The absolute time of the last redraw (in ms since initialization). ++ /// Used to check whether it's the first time we draw the image in the ++ /// current redraw cycle. ++ Milliseconds last_redraw; ++ /// The absolute time of the next redraw (in ms since initialization). ++ /// 0 means no redraw is scheduled. ++ Milliseconds next_redraw; ++ /// The unscaled pixel width and height of the image. Usually inherited ++ /// from the first frame. ++ int pix_width, pix_height; ++ /// The first frame. ++ ImageFrame first_frame; ++ /// The array of frames beyond the first one. ++ kvec_t(ImageFrame) frames_beyond_the_first; ++ /// Image placements. ++ khash_t(id2placement) *placements; ++ /// The default placement. ++ uint32_t default_placement; ++ /// The initial placement id, specified with the transmission command, ++ /// used to report success or failure. ++ uint32_t initial_placement_id; ++} Image; ++ ++typedef struct ImagePlacement { ++ /// The image this placement belongs to. ++ Image *image; ++ /// The id of the placement. Must be nonzero. ++ uint32_t placement_id; ++ /// The last time when the placement was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The 1-based index of the protected pixmap. We protect a pixmap in ++ /// gr_load_pixmap to avoid unloading it right after it was loaded. ++ int protected_frame; ++ /// Whether the placement is used only for Unicode placeholders. ++ char virtual; ++ /// The scaling mode (see `ScaleMode`). ++ char scale_mode; ++ /// Height and width in cells. ++ uint16_t rows, cols; ++ /// Top-left corner of the source rectangle ('x=' and 'y='). ++ int src_pix_x, src_pix_y; ++ /// Height and width of the source rectangle (zero if full image). ++ int src_pix_width, src_pix_height; ++ /// The image appropriately scaled and uploaded to the X server. This ++ /// pixmap is premultiplied by alpha. ++ Pixmap first_pixmap; ++ /// The array of pixmaps beyond the first one. ++ kvec_t(Pixmap) pixmaps_beyond_the_first; ++ /// The dimensions of the cell used to scale the image. If cell ++ /// dimensions are changed (font change), the image will be rescaled. ++ uint16_t scaled_cw, scaled_ch; ++ /// If true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++} ImagePlacement; ++ ++/// A rectangular piece of an image to be drawn. ++typedef struct { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// The position of the rectangle in pixels. ++ int screen_x_pix, screen_y_pix; ++ /// The starting row on the screen. ++ int screen_y_row; ++ /// The part of the whole image to be drawn, in cells. Starts are ++ /// zero-based, ends are exclusive. ++ int img_start_col, img_end_col, img_start_row, img_end_row; ++ /// The current cell width and height in pixels. ++ int cw, ch; ++ /// Whether colors should be inverted. ++ int reverse; ++} ImageRect; ++ ++/// Executes `code` for each frame of an image. Example: ++/// ++/// foreach_frame(image, frame, { ++/// printf("Frame %d\n", frame->index); ++/// }); ++/// ++#define foreach_frame(image, framevar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \ ++ ImageFrame *framevar = \ ++ __i == 0 ? &(image).first_frame \ ++ : &kv_A((image).frames_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++/// Executes `code` for each pixmap of a placement. Example: ++/// ++/// foreach_pixmap(placement, pixmap, { ++/// ... ++/// }); ++/// ++#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \ ++ Pixmap pixmapvar = \ ++ __i == 0 ? (placement).first_pixmap \ ++ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++ ++static Image *gr_find_image(uint32_t image_id); ++static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len); ++static void gr_delete_image(Image *img); ++static void gr_check_limits(); ++static char *gr_base64dec(const char *src, size_t *size); ++static void sanitize_str(char *str, size_t max_len); ++static const char *sanitized_filename(const char *str); ++ ++/// The array of image rectangles to draw. It is reset each frame. ++static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}}; ++/// The known images (including the ones being uploaded). ++static khash_t(id2image) *images = NULL; ++/// The total number of placements in all images. ++static unsigned total_placement_count = 0; ++/// The total size of all image files stored in the on-disk cache. ++static int64_t images_disk_size = 0; ++/// The total size of all images and placements loaded into ram. ++static int64_t images_ram_size = 0; ++/// The id of the last loaded image. ++static uint32_t last_image_id = 0; ++/// Current cell width and heigh in pixels. ++static int current_cw = 0, current_ch = 0; ++/// The id of the currently uploaded image (when using direct uploading). ++static uint32_t current_upload_image_id = 0; ++/// The index of the frame currently being uploaded. ++static int current_upload_frame_index = 0; ++/// The time when the graphics module was initialized. ++static struct timespec initialization_time = {0}; ++/// The time when the current frame drawing started, used for debugging fps and ++/// to calculate the current frame for animations. ++static Milliseconds drawing_start_time; ++/// The global index of the current command. ++static uint64_t global_command_counter = 0; ++/// The next redraw times for each row of the terminal. Used for animations. ++/// 0 means no redraw is scheduled. ++static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL}; ++/// The number of files loaded in the current redraw cycle. ++static int this_redraw_cycle_loaded_files = 0; ++/// The number of pixmaps loaded in the current redraw cycle. ++static int this_redraw_cycle_loaded_pixmaps = 0; ++ ++/// The directory where the cache files are stored. ++static char cache_dir[MAX_FILENAME_SIZE - 16]; ++ ++/// The table used for color inversion. ++static unsigned char reverse_table[256]; ++ ++// Declared in the header. ++GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE; ++char graphics_display_images = 1; ++GraphicsCommandResult graphics_command_result = {0}; ++int graphics_next_redraw_delay = INT_MAX; ++ ++// Defined in config.h ++extern const char graphics_cache_dir_template[]; ++extern unsigned graphics_max_single_image_file_size; ++extern unsigned graphics_total_file_cache_size; ++extern unsigned graphics_max_single_image_ram_size; ++extern unsigned graphics_max_total_ram_size; ++extern unsigned graphics_max_total_placements; ++extern double graphics_excess_tolerance_ratio; ++extern unsigned graphics_animation_min_delay; ++ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic helpers. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define MIN(a, b) ((a) < (b) ? (a) : (b)) ++#define MAX(a, b) ((a) < (b) ? (b) : (a)) ++ ++/// Returns the difference between `end` and `start` in milliseconds. ++static int64_t gr_timediff_ms(const struct timespec *end, ++ const struct timespec *start) { ++ return (end->tv_sec - start->tv_sec) * 1000 + ++ (end->tv_nsec - start->tv_nsec) / 1000000; ++} ++ ++/// Returns the current time in milliseconds since the initialization. ++static Milliseconds gr_now_ms() { ++ struct timespec now; ++ clock_gettime(CLOCK_MONOTONIC, &now); ++ return gr_timediff_ms(&now, &initialization_time); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Logging. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define GR_LOG(...) \ ++ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0) ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic image management functions (create, delete, find, etc). ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns the 1-based index of the last frame. Note that you may want to use ++/// `gr_last_uploaded_frame_index` instead since the last frame may be not ++/// fully uploaded yet. ++static inline int gr_last_frame_index(Image *img) { ++ return kv_size(img->frames_beyond_the_first) + 1; ++} ++ ++/// Returns the frame with the given index. Returns NULL if the index is out of ++/// bounds. The index is 1-based. ++static ImageFrame *gr_get_frame(Image *img, int index) { ++ if (!img) ++ return NULL; ++ if (index == 1) ++ return &img->first_frame; ++ if (2 <= index && index <= gr_last_frame_index(img)) ++ return &kv_A(img->frames_beyond_the_first, index - 2); ++ return NULL; ++} ++ ++/// Returns the last frame of the image. Returns NULL if `img` is NULL. ++static ImageFrame *gr_get_last_frame(Image *img) { ++ if (!img) ++ return NULL; ++ return gr_get_frame(img, gr_last_frame_index(img)); ++} ++ ++/// Returns the 1-based index of the last frame or the second-to-last frame if ++/// the last frame is not fully uploaded yet. ++static inline int gr_last_uploaded_frame_index(Image *img) { ++ int last_index = gr_last_frame_index(img); ++ if (last_index > 1 && ++ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS) ++ return last_index - 1; ++ return last_index; ++} ++ ++/// Returns the pixmap for the frame with the given index. Returns 0 if the ++/// index is out of bounds. The index is 1-based. ++static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) { ++ if (index == 1) ++ return placement->first_pixmap; ++ if (2 <= index && ++ index <= kv_size(placement->pixmaps_beyond_the_first) + 1) ++ return kv_A(placement->pixmaps_beyond_the_first, index - 2); ++ return 0; ++} ++ ++/// Sets the pixmap for the frame with the given index. The index is 1-based. ++/// The array of pixmaps is resized if needed. ++static void gr_set_frame_pixmap(ImagePlacement *placement, int index, ++ Pixmap pixmap) { ++ if (index == 1) { ++ placement->first_pixmap = pixmap; ++ return; ++ } ++ // Resize the array if needed. ++ size_t old_size = kv_size(placement->pixmaps_beyond_the_first); ++ if (old_size < index - 1) { ++ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2); ++ for (size_t i = old_size; i < index - 1; i++) ++ kv_A(placement->pixmaps_beyond_the_first, i) = 0; ++ } ++ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap; ++} ++ ++/// Finds the image corresponding to the client id. Returns NULL if cannot find. ++static Image *gr_find_image(uint32_t image_id) { ++ khiter_t k = kh_get(id2image, images, image_id); ++ if (k == kh_end(images)) ++ return NULL; ++ Image *res = kh_value(images, k); ++ return res; ++} ++ ++/// Finds the newest image corresponding to the image number. Returns NULL if ++/// cannot find. ++static Image *gr_find_image_by_number(uint32_t image_number) { ++ if (image_number == 0) ++ return NULL; ++ Image *newest_img = NULL; ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ if (img->image_number == image_number && ++ (!newest_img || newest_img->global_command_index < ++ img->global_command_index)) ++ newest_img = img; ++ }); ++ if (!newest_img) ++ GR_LOG("Image number %u not found\n", image_number); ++ else ++ GR_LOG("Found image number %u, its id is %u\n", image_number, ++ img->image_id); ++ return newest_img; ++} ++ ++/// Finds the placement corresponding to the id. If the placement id is 0, ++/// returns some default placement. ++static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) { ++ if (!img) ++ return NULL; ++ if (placement_id == 0) { ++ // Try to get the default placement. ++ ImagePlacement *dflt = NULL; ++ if (img->default_placement != 0) ++ dflt = gr_find_placement(img, img->default_placement); ++ if (dflt) ++ return dflt; ++ // If there is no default placement, return the first one and ++ // set it as the default. ++ kh_foreach_value(img->placements, dflt, { ++ img->default_placement = dflt->placement_id; ++ return dflt; ++ }); ++ // If there are no placements, return NULL. ++ return NULL; ++ } ++ khiter_t k = kh_get(id2placement, img->placements, placement_id); ++ if (k == kh_end(img->placements)) ++ return NULL; ++ ImagePlacement *res = kh_value(img->placements, k); ++ return res; ++} ++ ++/// Finds the placement by image id and placement id. ++static ImagePlacement *gr_find_image_and_placement(uint32_t image_id, ++ uint32_t placement_id) { ++ return gr_find_placement(gr_find_image(image_id), placement_id); ++} ++ ++/// Writes the name of the on-disk cache file to `out`. `max_len` should be the ++/// size of `out`. The name will be something like ++/// "/tmp/st-images-xxx/img-ID-FRAME". ++static void gr_get_frame_filename(ImageFrame *frame, char *out, ++ size_t max_len) { ++ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir, ++ frame->image->image_id, frame->index); ++} ++ ++/// Returns the (estimation) of the RAM size used by the frame right now. ++static unsigned gr_frame_current_ram_size(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return 0; ++ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by a single frame pixmap. ++static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) { ++ return (unsigned)placement->rows * placement->cols * ++ placement->scaled_ch * placement->scaled_cw * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by the placemenet right now. ++static unsigned gr_placement_current_ram_size(ImagePlacement *placement) { ++ unsigned single_frame_size = ++ gr_placement_single_frame_ram_size(placement); ++ unsigned result = 0; ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ result += single_frame_size; ++ }); ++ return result; ++} ++ ++/// Unload the frame from RAM (i.e. delete the corresponding imlib object). ++/// If the on-disk file of the frame is preserved, it can be reloaded later. ++static void gr_unload_frame(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return; ++ ++ unsigned frame_ram_size = gr_frame_current_ram_size(frame); ++ images_ram_size -= frame_ram_size; ++ ++ imlib_context_set_image(frame->imlib_object); ++ imlib_free_image_and_decache(); ++ frame->imlib_object = NULL; ++ ++ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_ram_size / 1024, ++ frame_ram_size / 1024); ++} ++ ++/// Unload all frames of the image. ++static void gr_unload_all_frames(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_unload_frame(frame); ++ }); ++} ++ ++/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps). ++/// If the on-disk files or imlib objects of the corresponding image are ++/// preserved, the placement can be reloaded later. ++static void gr_unload_placement(ImagePlacement *placement) { ++ unsigned placement_ram_size = gr_placement_current_ram_size(placement); ++ images_ram_size -= placement_ram_size; ++ ++ Display *disp = imlib_context_get_display(); ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ XFreePixmap(disp, pixmap); ++ }); ++ ++ placement->first_pixmap = 0; ++ placement->pixmaps_beyond_the_first.n = 0; ++ placement->scaled_ch = placement->scaled_cw = 0; ++ ++ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, images_ram_size / 1024, ++ placement_ram_size / 1024); ++} ++ ++/// Unload a single pixmap of the placement from RAM. ++static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) { ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (!pixmap) ++ return; ++ ++ Display *disp = imlib_context_get_display(); ++ XFreePixmap(disp, pixmap); ++ gr_set_frame_pixmap(placement, frameidx, 0); ++ images_ram_size -= gr_placement_single_frame_ram_size(placement); ++ ++ GR_LOG("After unloading pixmap %ld of " ++ "placement %u/%u (atime %ld ms ago) " ++ "frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ pixmap, placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, frameidx, ++ drawing_start_time - ++ gr_get_frame(placement->image, frameidx)->atime, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024); ++} ++ ++/// Deletes the on-disk cache file corresponding to the frame. The in-ram image ++/// object (if it exists) is not deleted, placements are not unloaded either. ++static void gr_delete_imagefile(ImageFrame *frame) { ++ // It may still be being loaded. Close the file in this case. ++ if (frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++ ++ if (frame->disk_size == 0) ++ return; ++ ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ remove(filename); ++ ++ unsigned disk_size = frame->disk_size; ++ images_disk_size -= disk_size; ++ frame->image->total_disk_size -= disk_size; ++ frame->disk_size = 0; ++ ++ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) " ++ "disk: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_disk_size / 1024, ++ disk_size / 1024); ++} ++ ++/// Deletes all on-disk cache files of the image (for each frame). ++static void gr_delete_imagefiles(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ }); ++} ++ ++/// Deletes the given placement: unloads, frees the object, but doesn't change ++/// the `placements` hash table. ++static void gr_delete_placement_keep_id(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id, ++ placement->placement_id); ++ gr_unload_placement(placement); ++ kv_destroy(placement->pixmaps_beyond_the_first); ++ free(placement); ++ total_placement_count--; ++} ++ ++/// Deletes all placements of `img`. ++static void gr_delete_all_placements(Image *img) { ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_delete_placement_keep_id(placement); ++ }); ++ kh_clear(id2placement, img->placements); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// but doesn't change the `images` hash table. ++static void gr_delete_image_keep_id(Image *img) { ++ if (!img) ++ return; ++ GR_LOG("Deleting image %u\n", img->image_id); ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ gr_unload_frame(frame); ++ }); ++ kv_destroy(img->frames_beyond_the_first); ++ gr_delete_all_placements(img); ++ kh_destroy(id2placement, img->placements); ++ free(img); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// and also removes it from `images`. ++static void gr_delete_image(Image *img) { ++ if (!img) ++ return; ++ uint32_t id = img->image_id; ++ gr_delete_image_keep_id(img); ++ khiter_t k = kh_get(id2image, images, id); ++ kh_del(id2image, images, k); ++} ++ ++/// Deletes the given placement: unloads, frees the object, and also removes it ++/// from `placements`. ++static void gr_delete_placement(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ uint32_t id = placement->placement_id; ++ Image *img = placement->image; ++ gr_delete_placement_keep_id(placement); ++ khiter_t k = kh_get(id2placement, img->placements, id); ++ kh_del(id2placement, img->placements, k); ++} ++ ++/// Deletes all images and clears `images`. ++static void gr_delete_all_images() { ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ gr_delete_image_keep_id(img); ++ }); ++ kh_clear(id2image, images); ++} ++ ++/// Update the atime of the image. ++static void gr_touch_image(Image *img) { ++ img->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the frame. ++static void gr_touch_frame(ImageFrame *frame) { ++ frame->image->atime = frame->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the placement. Touches the images too. ++static void gr_touch_placement(ImagePlacement *placement) { ++ placement->image->atime = placement->atime = gr_now_ms(); ++} ++ ++/// Creates a new image with the given id. If an image with that id already ++/// exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static Image *gr_new_image(uint32_t id) { ++ if (id == 0) { ++ do { ++ id = rand(); ++ // Avoid IDs that don't need full 32 bits. ++ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 || ++ gr_find_image(id)); ++ GR_LOG("Generated random image id %u\n", id); ++ } ++ Image *img = gr_find_image(id); ++ gr_delete_image_keep_id(img); ++ GR_LOG("Creating image %u\n", id); ++ img = malloc(sizeof(Image)); ++ memset(img, 0, sizeof(Image)); ++ img->placements = kh_init(id2placement); ++ int ret; ++ khiter_t k = kh_put(id2image, images, id, &ret); ++ kh_value(images, k) = img; ++ img->image_id = id; ++ gr_touch_image(img); ++ img->global_command_index = global_command_counter; ++ return img; ++} ++ ++/// Creates a new frame at the end of the frame array. It may be the first frame ++/// if there are no frames yet. ++static ImageFrame *gr_append_new_frame(Image *img) { ++ ImageFrame *frame = NULL; ++ if (img->first_frame.index == 0 && ++ kv_size(img->frames_beyond_the_first) == 0) { ++ frame = &img->first_frame; ++ frame->index = 1; ++ } else { ++ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first); ++ memset(frame, 0, sizeof(ImageFrame)); ++ frame->index = kv_size(img->frames_beyond_the_first) + 1; ++ } ++ frame->image = img; ++ gr_touch_frame(frame); ++ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id); ++ return frame; ++} ++ ++/// Creates a new placement with the given id. If a placement with that id ++/// already exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static ImagePlacement *gr_new_placement(Image *img, uint32_t id) { ++ if (id == 0) { ++ do { ++ // Currently we support only 24-bit IDs. ++ id = rand() & 0xFFFFFF; ++ // Avoid IDs that need only one byte. ++ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id)); ++ } ++ ImagePlacement *placement = gr_find_placement(img, id); ++ gr_delete_placement_keep_id(placement); ++ GR_LOG("Creating placement %u/%u\n", img->image_id, id); ++ placement = malloc(sizeof(ImagePlacement)); ++ memset(placement, 0, sizeof(ImagePlacement)); ++ total_placement_count++; ++ int ret; ++ khiter_t k = kh_put(id2placement, img->placements, id, &ret); ++ kh_value(img->placements, k) = placement; ++ placement->image = img; ++ placement->placement_id = id; ++ gr_touch_placement(placement); ++ if (img->default_placement == 0) ++ img->default_placement = id; ++ return placement; ++} ++ ++static int64_t ceil_div(int64_t a, int64_t b) { ++ return (a + b - 1) / b; ++} ++ ++/// Computes the best number of rows and columns for a placement if it's not ++/// specified, and also adjusts the source rectangle size. ++static void gr_infer_placement_size_maybe(ImagePlacement *placement) { ++ // The size of the image. ++ int image_pix_width = placement->image->pix_width; ++ int image_pix_height = placement->image->pix_height; ++ // Negative values are not allowed. Quietly set them to 0. ++ if (placement->src_pix_x < 0) ++ placement->src_pix_x = 0; ++ if (placement->src_pix_y < 0) ++ placement->src_pix_y = 0; ++ if (placement->src_pix_width < 0) ++ placement->src_pix_width = 0; ++ if (placement->src_pix_height < 0) ++ placement->src_pix_height = 0; ++ // If the source rectangle is outside the image, truncate it. ++ if (placement->src_pix_x > image_pix_width) ++ placement->src_pix_x = image_pix_width; ++ if (placement->src_pix_y > image_pix_height) ++ placement->src_pix_y = image_pix_height; ++ // If the source rectangle is not specified, use the whole image. If ++ // it's partially outside the image, truncate it. ++ if (placement->src_pix_width == 0 || ++ placement->src_pix_x + placement->src_pix_width > image_pix_width) ++ placement->src_pix_width = ++ image_pix_width - placement->src_pix_x; ++ if (placement->src_pix_height == 0 || ++ placement->src_pix_y + placement->src_pix_height > image_pix_height) ++ placement->src_pix_height = ++ image_pix_height - placement->src_pix_y; ++ ++ if (placement->cols != 0 && placement->rows != 0) ++ return; ++ if (placement->src_pix_width == 0 || placement->src_pix_height == 0) ++ return; ++ if (current_cw == 0 || current_ch == 0) ++ return; ++ ++ // If no size is specified, use the image size. ++ if (placement->cols == 0 && placement->rows == 0) { ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ return; ++ } ++ ++ // Some applications specify only one of the dimensions. ++ if (placement->scale_mode == SCALE_MODE_CONTAIN) { ++ // If we preserve aspect ratio and fit to width/height, the most ++ // logical thing is to find the minimum size of the ++ // non-specified dimension that allows the image to fit the ++ // specified dimension. ++ if (placement->cols == 0) { ++ placement->cols = ceil_div( ++ placement->src_pix_width * placement->rows * ++ current_ch, ++ placement->src_pix_height * current_cw); ++ return; ++ } ++ if (placement->rows == 0) { ++ placement->rows = ++ ceil_div(placement->src_pix_height * ++ placement->cols * current_cw, ++ placement->src_pix_width * current_ch); ++ return; ++ } ++ } else { ++ // Otherwise we stretch the image or preserve the original size. ++ // In both cases we compute the best number of columns from the ++ // pixel size and cell size. ++ // TODO: In the case of stretching it's not the most logical ++ // thing to do, may need to revisit in the future. ++ // Currently we switch to SCALE_MODE_CONTAIN when only one ++ // of the dimensions is specified, so this case shouldn't ++ // happen in practice. ++ if (!placement->cols) ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ if (!placement->rows) ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ } ++} ++ ++/// Adjusts the current frame index if enough time has passed since the display ++/// of the current frame. Also computes the time of the next redraw of this ++/// image (`img->next_redraw`). The current time is passed as an argument so ++/// that all animations are in sync. ++static void gr_update_frame_index(Image *img, Milliseconds now) { ++ if (img->current_frame == 0) { ++ img->current_frame_time = now; ++ img->current_frame = 1; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ // If the animation is stopped, show the current frame. ++ if (!img->animation_state || ++ img->animation_state == ANIMATION_STATE_STOPPED || ++ img->animation_state == ANIMATION_STATE_UNSET) { ++ // The next redraw is never (unless the state is changed). ++ img->next_redraw = 0; ++ return; ++ } ++ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img); ++ // If we are loading and we reached the last frame, show the last frame. ++ if (img->animation_state == ANIMATION_STATE_LOADING && ++ img->current_frame == last_uploaded_frame_index) { ++ // The next redraw is never (unless the state is changed or ++ // frames are added). ++ img->next_redraw = 0; ++ return; ++ } ++ ++ // Check how many milliseconds passed since the current frame was shown. ++ int passed_ms = now - img->current_frame_time; ++ // If the animation is looping and too much time has passes, we can ++ // make a shortcut. ++ if (img->animation_state == ANIMATION_STATE_LOOPING && ++ img->total_duration > 0 && passed_ms >= img->total_duration) { ++ passed_ms %= img->total_duration; ++ img->current_frame_time = now - passed_ms; ++ } ++ // Find the next frame. ++ int original_frame_index = img->current_frame; ++ while (1) { ++ ImageFrame *frame = gr_get_frame(img, img->current_frame); ++ if (!frame) { ++ // The frame doesn't exist, go to the first frame. ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ if (frame->gap >= 0 && passed_ms < frame->gap) { ++ // Not enough time has passed, we are still in the same ++ // frame, and it's not a gapless frame. ++ img->next_redraw = ++ img->current_frame_time + MAX(1, frame->gap); ++ return; ++ } ++ // Otherwise go to the next frame. ++ passed_ms -= MAX(0, frame->gap); ++ if (img->current_frame >= last_uploaded_frame_index) { ++ // It's the last frame, if the animation is loading, ++ // remain on it. ++ if (img->animation_state == ANIMATION_STATE_LOADING) { ++ img->next_redraw = 0; ++ return; ++ } ++ // Otherwise the animation is looping. ++ img->current_frame = 1; ++ // TODO: Support finite number of loops. ++ } else { ++ img->current_frame++; ++ } ++ // Make sure we don't get stuck in an infinite loop. ++ if (img->current_frame == original_frame_index) { ++ // We looped through all frames, but haven't reached the ++ // next frame yet. This may happen if too much time has ++ // passed since the last redraw or all the frames are ++ // gapless. Just move on to the next frame. ++ img->current_frame++; ++ if (img->current_frame > ++ last_uploaded_frame_index) ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX( ++ 1, gr_get_frame(img, img->current_frame)->gap); ++ return; ++ } ++ // Adjust the start time of the frame. The next redraw time will ++ // be set in the next iteration. ++ img->current_frame_time += MAX(0, frame->gap); ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Unloading and deleting images to save resources. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// A helper to compare frames by atime for qsort. ++static int gr_cmp_frames_by_atime(const void *a, const void *b) { ++ ImageFrame *frame_a = *(ImageFrame *const *)a; ++ ImageFrame *frame_b = *(ImageFrame *const *)b; ++ if (frame_a->atime == frame_b->atime) ++ return frame_a->image->global_command_index - ++ frame_b->image->global_command_index; ++ return frame_a->atime - frame_b->atime; ++} ++ ++/// A helper to compare images by atime for qsort. ++static int gr_cmp_images_by_atime(const void *a, const void *b) { ++ Image *img_a = *(Image *const *)a; ++ Image *img_b = *(Image *const *)b; ++ if (img_a->atime == img_b->atime) ++ return img_a->global_command_index - ++ img_b->global_command_index; ++ return img_a->atime - img_b->atime; ++} ++ ++/// A helper to compare placements by atime for qsort. ++static int gr_cmp_placements_by_atime(const void *a, const void *b) { ++ ImagePlacement *p_a = *(ImagePlacement **)a; ++ ImagePlacement *p_b = *(ImagePlacement **)b; ++ if (p_a->atime == p_b->atime) ++ return p_a->image->global_command_index - ++ p_b->image->global_command_index; ++ return p_a->atime - p_b->atime; ++} ++ ++typedef kvec_t(Image *) ImageVec; ++typedef kvec_t(ImagePlacement *) ImagePlacementVec; ++typedef kvec_t(ImageFrame *) ImageFrameVec; ++ ++/// Returns an array of pointers to all images sorted by atime. ++static ImageVec gr_get_images_sorted_by_atime() { ++ ImageVec vec; ++ kv_init(vec); ++ if (kh_size(images) == 0) ++ return vec; ++ kv_resize(Image *, vec, kh_size(images)); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { kv_push(Image *, vec, img); }); ++ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all placements sorted by atime. ++static ImagePlacementVec gr_get_placements_sorted_by_atime() { ++ ImagePlacementVec vec; ++ kv_init(vec); ++ if (total_placement_count == 0) ++ return vec; ++ kv_resize(ImagePlacement *, vec, total_placement_count); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ kv_push(ImagePlacement *, vec, placement); ++ }); ++ }); ++ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *), ++ gr_cmp_placements_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all frames sorted by atime. ++static ImageFrameVec gr_get_frames_sorted_by_atime() { ++ ImageFrameVec frames; ++ kv_init(frames); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ kv_push(ImageFrame *, frames, frame); ++ }); ++ }); ++ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *), ++ gr_cmp_frames_by_atime); ++ return frames; ++} ++ ++/// An object that can be unloaded from RAM. ++typedef struct { ++ /// Some score, probably based on access time. The lower the score, the ++ /// more likely that the object should be unloaded. ++ int64_t score; ++ union { ++ ImagePlacement *placement; ++ ImageFrame *frame; ++ }; ++ /// If zero, the object is the imlib object of `frame`, if non-zero, ++ /// the object is a pixmap of `frameidx`-th frame of `placement`. ++ int frameidx; ++} UnloadableObject; ++ ++typedef kvec_t(UnloadableObject) UnloadableObjectVec; ++ ++/// A helper to compare unloadable objects by score for qsort. ++static int gr_cmp_unloadable_objects(const void *a, const void *b) { ++ UnloadableObject *obj_a = (UnloadableObject *)a; ++ UnloadableObject *obj_b = (UnloadableObject *)b; ++ return obj_a->score - obj_b->score; ++} ++ ++/// Unloads an unloadable object from RAM. ++static void gr_unload_object(UnloadableObject *obj) { ++ if (obj->frameidx) { ++ if (obj->placement->protected_frame == obj->frameidx) ++ return; ++ gr_unload_pixmap(obj->placement, obj->frameidx); ++ } else { ++ gr_unload_frame(obj->frame); ++ } ++} ++ ++/// Returns the recency threshold for an image. Frames that were accessed within ++/// this threshold from now are considered recent and may be handled ++/// differently because we may need them again very soon. ++static Milliseconds gr_recency_threshold(Image *img) { ++ return img->total_duration * 2 + 1000; ++} ++ ++/// Creates an unloadable object for the imlib object of a frame. ++static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now, ++ ImageFrame *frame) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = 0; ++ obj.frame = frame; ++ Milliseconds atime = frame->atime; ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent frame, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Randomize the score because it's not very clear in which ++ // order we want to unload them: reloading a frame may require ++ // reloading other frames. ++ obj.score = now + 1000 + rand() % 1000; ++ } ++ return obj; ++} ++ ++/// Creates an unloadable object for a pixmap. ++static UnloadableObject ++gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame, ++ ImagePlacement *placement) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = frame->index; ++ obj.placement = placement; ++ obj.score = placement->atime; ++ // Since we don't store pixmap atimes, use the ++ // oldest atime of the frame and the placement. ++ Milliseconds atime = MIN(placement->atime, frame->atime); ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent pixmap, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Also assign higher scores to frames that are closer to the ++ // current frame (more likely to be used soon). ++ int num_frames = gr_last_frame_index(frame->image); ++ int dist = frame->index - frame->image->current_frame; ++ if (dist < 0) ++ dist += num_frames; ++ obj.score = ++ now + 1000 + (num_frames - dist) * 1000 / num_frames; ++ // If the pixmap is much larger than the imlib image, prefer to ++ // unload the pixmap by adding up to -1000 to the score. If the ++ // imlib image is larger, add up to +1000. ++ float imlib_size = gr_frame_current_ram_size(frame); ++ float pixmap_size = ++ gr_placement_single_frame_ram_size(placement); ++ obj.score += ++ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5); ++ } ++ return obj; ++} ++ ++/// Returns an array of unloadable objects sorted by score. ++static UnloadableObjectVec ++gr_get_unloadable_objects_sorted_by_score(Milliseconds now) { ++ UnloadableObjectVec objects; ++ kv_init(objects); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ if (!frame->imlib_object) ++ continue; ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_frame(now, frame)); ++ int frameidx = frame->index; ++ kh_foreach_value(img->placements, placement, { ++ if (!gr_get_frame_pixmap(placement, frameidx)) ++ continue; ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_pixmap( ++ now, frame, placement)); ++ }); ++ }); ++ }); ++ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject), ++ gr_cmp_unloadable_objects); ++ return objects; ++} ++ ++/// Returns the limit adjusted by the excess tolerance ratio. ++static inline unsigned apply_tolerance(unsigned limit) { ++ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio); ++} ++ ++/// Checks RAM and disk cache limits and deletes/unloads some images. ++static void gr_check_limits() { ++ Milliseconds now = gr_now_ms(); ++ ImageVec images_sorted = {0}; ++ ImagePlacementVec placements_sorted = {0}; ++ ImageFrameVec frames_sorted = {0}; ++ UnloadableObjectVec objects_sorted = {0}; ++ int images_begin = 0; ++ int placements_begin = 0; ++ char changed = 0; ++ // First reduce the number of images if there are too many. ++ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many images: %d\n", kh_size(images)); ++ changed = 1; ++ images_sorted = gr_get_images_sorted_by_atime(); ++ int to_delete = kv_size(images_sorted) - ++ graphics_max_total_placements; ++ for (; images_begin < to_delete; images_begin++) ++ gr_delete_image(images_sorted.a[images_begin]); ++ } ++ // Then reduce the number of placements if there are too many. ++ if (total_placement_count > ++ apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many placements: %d\n", total_placement_count); ++ changed = 1; ++ placements_sorted = gr_get_placements_sorted_by_atime(); ++ int to_delete = kv_size(placements_sorted) - ++ graphics_max_total_placements; ++ for (; placements_begin < to_delete; placements_begin++) { ++ ImagePlacement *placement = ++ placements_sorted.a[placements_begin]; ++ if (placement->protected_frame) ++ break; ++ gr_delete_placement(placement); ++ } ++ } ++ // Then reduce the size of the image file cache. The files correspond to ++ // image frames. ++ if (images_disk_size > ++ apply_tolerance(graphics_total_file_cache_size)) { ++ GR_LOG("Too big disk cache: %ld KiB\n", ++ images_disk_size / 1024); ++ changed = 1; ++ frames_sorted = gr_get_frames_sorted_by_atime(); ++ for (int i = 0; i < kv_size(frames_sorted); i++) { ++ if (images_disk_size <= graphics_total_file_cache_size) ++ break; ++ gr_delete_imagefile(kv_A(frames_sorted, i)); ++ } ++ } ++ // Then unload images from RAM. ++ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) { ++ changed = 1; ++ int frames_begin = 0; ++ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024); ++ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now); ++ for (int i = 0; i < kv_size(objects_sorted); i++) { ++ if (images_ram_size <= graphics_max_total_ram_size) ++ break; ++ gr_unload_object(&kv_A(objects_sorted, i)); ++ } ++ } ++ if (changed) { ++ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " ++ "img count: %d placement count: %d\n", ++ images_ram_size / 1024, images_disk_size / 1024, ++ kh_size(images), total_placement_count); ++ } ++ kv_destroy(images_sorted); ++ kv_destroy(placements_sorted); ++ kv_destroy(frames_sorted); ++ kv_destroy(objects_sorted); ++} ++ ++/// Unloads all images by user request. ++void gr_unload_images_to_reduce_ram() { ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ if (placement->protected_frame) ++ continue; ++ gr_unload_placement(placement); ++ }); ++ gr_unload_all_frames(img); ++ }); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Image loading. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2 ++/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted ++/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout ++/// on little-endian architectures). ++static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format, ++ size_t num_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ if (format == 32) { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | ++ ((DATA32)from[byte_i + 3]) << 24; ++ } ++ } else { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | 0xFF000000; ++ } ++ } ++} ++ ++/// Loads uncompressed RGB or RGBA image data from a file. ++static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file, ++ int format, ++ size_t total_pixels) { ++ unsigned char chunk[BUFSIZ]; ++ size_t pixel_size = format == 24 ? 3 : 4; ++ size_t chunk_size_pix = BUFSIZ / 4; ++ size_t chunk_size_bytes = chunk_size_pix * pixel_size; ++ size_t bytes = total_pixels * pixel_size; ++ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels; ++ chunk_start_pix += chunk_size_pix) { ++ size_t read_size = fread(chunk, 1, chunk_size_bytes, file); ++ size_t read_pixels = read_size / pixel_size; ++ if (chunk_start_pix + read_pixels > total_pixels) ++ read_pixels = total_pixels - chunk_start_pix; ++ gr_copy_pixels(data + chunk_start_pix, chunk, format, ++ read_pixels); ++ } ++} ++ ++#define COMPRESSED_CHUNK_SIZE BUFSIZ ++#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4) ++ ++/// Loads compressed RGB or RGBA image data from a file. ++static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file, ++ int format, size_t total_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE]; ++ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE]; ++ ++ z_stream strm; ++ strm.zalloc = Z_NULL; ++ strm.zfree = Z_NULL; ++ strm.opaque = Z_NULL; ++ strm.next_out = decompressed_chunk; ++ strm.avail_out = DECOMPRESSED_CHUNK_SIZE; ++ strm.avail_in = 0; ++ strm.next_in = Z_NULL; ++ int ret = inflateInit(&strm); ++ if (ret != Z_OK) ++ return 1; ++ ++ int error = 0; ++ int progress = 0; ++ size_t total_copied_pixels = 0; ++ while (1) { ++ // If we don't have enough data in the input buffer, try to read ++ // from the file. ++ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) { ++ // Move the existing data to the beginning. ++ memmove(compressed_chunk, strm.next_in, strm.avail_in); ++ strm.next_in = compressed_chunk; ++ // Read more data. ++ size_t bytes_read = fread( ++ compressed_chunk + strm.avail_in, 1, ++ COMPRESSED_CHUNK_SIZE - strm.avail_in, file); ++ strm.avail_in += bytes_read; ++ if (bytes_read != 0) ++ progress = 1; ++ } ++ ++ // Try to inflate the data. ++ int ret = inflate(&strm, Z_SYNC_FLUSH); ++ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) { ++ error = 1; ++ fprintf(stderr, ++ "error: could not decompress the image, error " ++ "%s\n", ++ ret == Z_MEM_ERROR ? "Z_MEM_ERROR" ++ : "Z_DATA_ERROR"); ++ break; ++ } ++ ++ // Copy the data from the output buffer to the image. ++ size_t full_pixels = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size; ++ // Make sure we don't overflow the image. ++ if (full_pixels > total_pixels - total_copied_pixels) ++ full_pixels = total_pixels - total_copied_pixels; ++ if (full_pixels > 0) { ++ // Copy pixels. ++ gr_copy_pixels(data, decompressed_chunk, format, ++ full_pixels); ++ data += full_pixels; ++ total_copied_pixels += full_pixels; ++ if (total_copied_pixels >= total_pixels) { ++ // We filled the whole image, there may be some ++ // data left, but we just truncate it. ++ break; ++ } ++ // Move the remaining data to the beginning. ++ size_t copied_bytes = full_pixels * pixel_size; ++ size_t leftover = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) - ++ copied_bytes; ++ memmove(decompressed_chunk, ++ decompressed_chunk + copied_bytes, leftover); ++ strm.next_out -= copied_bytes; ++ strm.avail_out += copied_bytes; ++ progress = 1; ++ } ++ ++ // If we haven't made any progress, then we have reached the end ++ // of both the file and the inflated data. ++ if (!progress) ++ break; ++ progress = 0; ++ } ++ ++ inflateEnd(&strm); ++ return error; ++} ++ ++#undef COMPRESSED_CHUNK_SIZE ++#undef DECOMPRESSED_CHUNK_SIZE ++ ++/// Load the image from a file containing raw pixel data (RGB or RGBA), the data ++/// may be compressed. ++static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame, ++ const char *filename) { ++ size_t total_pixels = frame->data_pix_width * frame->data_pix_height; ++ if (total_pixels * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: image %u frame %u is too big too load: %zu > %u\n", ++ frame->image->image_id, frame->index, total_pixels * 4, ++ graphics_max_single_image_ram_size); ++ return NULL; ++ } ++ ++ FILE* file = fopen(filename, "rb"); ++ if (!file) { ++ fprintf(stderr, ++ "error: could not open image file: %s\n", ++ sanitized_filename(filename)); ++ return NULL; ++ } ++ ++ Imlib_Image image = imlib_create_image(frame->data_pix_width, ++ frame->data_pix_height); ++ if (!image) { ++ fprintf(stderr, ++ "error: could not create an image of size %d x %d\n", ++ frame->data_pix_width, frame->data_pix_height); ++ fclose(file); ++ return NULL; ++ } ++ ++ imlib_context_set_image(image); ++ imlib_image_set_has_alpha(1); ++ DATA32* data = imlib_image_get_data(); ++ ++ // The default format is 32. ++ int format = frame->format ? frame->format : 32; ++ ++ if (frame->compression == 0) { ++ gr_load_raw_pixel_data_uncompressed(data, file, format, ++ total_pixels); ++ } else { ++ int ret = gr_load_raw_pixel_data_compressed(data, file, format, ++ total_pixels); ++ if (ret != 0) { ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ fclose(file); ++ return NULL; ++ } ++ } ++ ++ fclose(file); ++ imlib_image_put_back_data(data); ++ return image; ++} ++ ++/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object ++/// is fully composed on top of the background frame. If the frame is already ++/// loaded, does nothing. Loading may fail, in which case the status of the ++/// frame will be set to STATUS_RAM_LOADING_ERROR. ++static void gr_load_imlib_object(ImageFrame *frame) { ++ if (frame->imlib_object) ++ return; ++ ++ // If the image is uninitialized or uploading has failed, or the file ++ // has been deleted, we cannot load the image. ++ if (frame->status < STATUS_UPLOADING_SUCCESS) ++ return; ++ if (frame->disk_size == 0) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: cached image was deleted: %u frame %u\n", ++ frame->image->image_id, frame->index); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ // Prevent recursive dependences between frames. ++ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) { ++ fprintf(stderr, ++ "error: recursive loading of image %u frame %u\n", ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ frame->status = STATUS_RAM_LOADING_IN_PROGRESS; ++ ++ // Load the background frame if needed. Hopefully it's not recursive. ++ ImageFrame *bg_frame = NULL; ++ if (frame->background_frame_index) { ++ bg_frame = gr_get_frame(frame->image, ++ frame->background_frame_index); ++ if (!bg_frame) { ++ fprintf(stderr, ++ "error: could not find background " ++ "frame %d for image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ gr_load_imlib_object(bg_frame); ++ if (!bg_frame->imlib_object) { ++ fprintf(stderr, ++ "error: could not load background frame %d for " ++ "image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ } ++ ++ // Load the frame data image. ++ Imlib_Image frame_data_image = NULL; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ GR_LOG("Loading image: %s\n", sanitized_filename(filename)); ++ if (frame->format == 100 || frame->format == 0) ++ frame_data_image = imlib_load_image(filename); ++ if (frame->format == 32 || frame->format == 24 || ++ (!frame_data_image && frame->format == 0)) ++ frame_data_image = gr_load_raw_pixel_data(frame, filename); ++ this_redraw_cycle_loaded_files++; ++ ++ if (!frame_data_image) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, "error: could not load image: %s\n", ++ sanitized_filename(filename)); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ imlib_context_set_image(frame_data_image); ++ int frame_data_width = imlib_image_get_width(); ++ int frame_data_height = imlib_image_get_height(); ++ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width, ++ frame_data_height); ++ // If imlib loading succeeded, and it is the first frame, set the ++ // information about the original image size, unless it's already set. ++ if (frame->index == 1 && frame->image->pix_width == 0 && ++ frame->image->pix_height == 0) { ++ frame->image->pix_width = frame_data_width; ++ frame->image->pix_height = frame_data_height; ++ } ++ ++ int image_width = frame->image->pix_width; ++ int image_height = frame->image->pix_height; ++ ++ // Compose the image with the background color or frame. ++ if (frame->background_color != 0 || bg_frame || ++ image_width != frame_data_width || ++ image_height != frame_data_height) { ++ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n", ++ frame->background_color, frame->background_frame_index); ++ Imlib_Image composed_image = imlib_create_image( ++ image_width, image_height); ++ imlib_context_set_image(composed_image); ++ imlib_image_set_has_alpha(1); ++ imlib_context_set_anti_alias(0); ++ ++ // Start with the background frame or color. ++ imlib_context_set_blend(0); ++ if (bg_frame && bg_frame->imlib_object) { ++ imlib_blend_image_onto_image( ++ bg_frame->imlib_object, 1, 0, 0, ++ image_width, image_height, 0, 0, ++ image_width, image_height); ++ } else { ++ int r = (frame->background_color >> 24) & 0xFF; ++ int g = (frame->background_color >> 16) & 0xFF; ++ int b = (frame->background_color >> 8) & 0xFF; ++ int a = frame->background_color & 0xFF; ++ imlib_context_set_color(r, g, b, a); ++ imlib_image_fill_rectangle(0, 0, image_width, ++ image_height); ++ } ++ ++ // Blend the frame data image onto the background. ++ imlib_context_set_blend(1); ++ imlib_blend_image_onto_image( ++ frame_data_image, 1, 0, 0, frame->data_pix_width, ++ frame->data_pix_height, frame->x, frame->y, ++ frame->data_pix_width, frame->data_pix_height); ++ ++ // Free the frame data image. ++ imlib_context_set_image(frame_data_image); ++ imlib_free_image(); ++ ++ frame_data_image = composed_image; ++ } ++ ++ frame->imlib_object = frame_data_image; ++ ++ images_ram_size += gr_frame_current_ram_size(frame); ++ frame->status = STATUS_RAM_LOADING_SUCCESS; ++ ++ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n", ++ frame->image->image_id, frame->index, ++ images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024); ++} ++ ++/// Premultiplies the alpha channel of the image data. The data is an array of ++/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB. ++static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) { ++ for (size_t i = 0; i < num_pixels; ++i) { ++ DATA32 pixel = data[i]; ++ unsigned char a = pixel >> 24; ++ if (a == 0) { ++ data[i] = 0; ++ } else if (a != 255) { ++ unsigned char b = (pixel & 0xFF) * a / 255; ++ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255; ++ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255; ++ data[i] = (a << 24) | (r << 16) | (g << 8) | b; ++ } ++ } ++} ++ ++/// Creates a pixmap for the frame of an image placement. The pixmap contain the ++/// image data correctly scaled and fit to the box defined by the number of ++/// rows/columns of the image placement and the provided cell dimensions in ++/// pixels. If the placement is already loaded, it will be reloaded only if the ++/// cell dimensions have changed. ++Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) { ++ Image *img = placement->image; ++ ImageFrame *frame = gr_get_frame(img, frameidx); ++ ++ // Update the atime uncoditionally. ++ gr_touch_placement(placement); ++ if (frame) ++ gr_touch_frame(frame); ++ ++ // If cw or ch are different, unload all the pixmaps. ++ if (placement->scaled_cw != cw || placement->scaled_ch != ch) { ++ gr_unload_placement(placement); ++ placement->scaled_cw = cw; ++ placement->scaled_ch = ch; ++ } ++ ++ // If it's already loaded, do nothing. ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (pixmap) ++ return pixmap; ++ ++ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id, ++ placement->placement_id, frameidx); ++ ++ // Load the imlib object for the frame. ++ if (!frame) { ++ fprintf(stderr, ++ "error: could not find frame %u for image %u\n", ++ frameidx, img->image_id); ++ return 0; ++ } ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) ++ return 0; ++ ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ ++ // Create the scaled image. This is temporary, we will scale it ++ // appropriately, upload to the X server, and then delete immediately. ++ int scaled_w = (int)placement->cols * cw; ++ int scaled_h = (int)placement->rows * ch; ++ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: placement %u/%u would be too big to load: %d x " ++ "%d x 4 > %u\n", ++ img->image_id, placement->placement_id, scaled_w, ++ scaled_h, graphics_max_single_image_ram_size); ++ return 0; ++ } ++ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h); ++ if (!scaled_image) { ++ fprintf(stderr, ++ "error: imlib_create_image(%d, %d) returned " ++ "null\n", ++ scaled_w, scaled_h); ++ return 0; ++ } ++ imlib_context_set_image(scaled_image); ++ imlib_image_set_has_alpha(1); ++ ++ // First fill the scaled image with the transparent color. ++ imlib_context_set_blend(0); ++ imlib_context_set_color(0, 0, 0, 0); ++ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ ++ // The source rectangle. ++ int src_x = placement->src_pix_x; ++ int src_y = placement->src_pix_y; ++ int src_w = placement->src_pix_width; ++ int src_h = placement->src_pix_height; ++ // Whether the box is too small to use the true size of the image. ++ char box_too_small = scaled_w < src_w || scaled_h < src_h; ++ char mode = placement->scale_mode; ++ ++ // Then blend the original image onto the transparent background. ++ if (src_w <= 0 || src_h <= 0) { ++ fprintf(stderr, "warning: image of zero size\n"); ++ } else if (mode == SCALE_MODE_FILL) { ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, 0, 0, ++ scaled_w, scaled_h); ++ } else if (mode == SCALE_MODE_NONE || ++ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, 0, 0, src_w, ++ src_h); ++ } else { ++ if (mode != SCALE_MODE_CONTAIN && ++ mode != SCALE_MODE_NONE_OR_CONTAIN) { ++ fprintf(stderr, ++ "warning: unknown scale mode %u, using " ++ "'contain' instead\n", ++ mode); ++ } ++ int dest_x, dest_y; ++ int dest_w, dest_h; ++ if (scaled_w * src_h > src_w * scaled_h) { ++ // If the box is wider than the original image, fit to ++ // height. ++ dest_h = scaled_h; ++ dest_y = 0; ++ dest_w = src_w * scaled_h / src_h; ++ dest_x = (scaled_w - dest_w) / 2; ++ } else { ++ // Otherwise, fit to width. ++ dest_w = scaled_w; ++ dest_x = 0; ++ dest_h = src_h * scaled_w / src_w; ++ dest_y = (scaled_h - dest_h) / 2; ++ } ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, dest_x, ++ dest_y, dest_w, dest_h); ++ } ++ ++ // XRender needs the alpha channel premultiplied. ++ DATA32 *data = imlib_image_get_data(); ++ gr_premultiply_alpha(data, scaled_w * scaled_h); ++ ++ // Upload the image to the X server. ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ Colormap cmap = imlib_context_get_colormap(); ++ Drawable drawable = imlib_context_get_drawable(); ++ if (!drawable) ++ drawable = DefaultRootWindow(disp); ++ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32); ++ XVisualInfo visinfo; ++ XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo); ++ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, ++ (char *)data, scaled_w, scaled_h, 32, 0); ++ GC gc = XCreateGC(disp, pixmap, 0, NULL); ++ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w, ++ scaled_h); ++ XFreeGC(disp, gc); ++ // XDestroyImage will free the data as well, but it is managed by imlib, ++ // so set it to NULL. ++ ximage->data = NULL; ++ XDestroyImage(ximage); ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ ++ // Assign the pixmap to the frame and increase the ram size. ++ gr_set_frame_pixmap(placement, frameidx, pixmap); ++ images_ram_size += gr_placement_single_frame_ram_size(placement); ++ this_redraw_cycle_loaded_pixmaps++; ++ ++ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " ++ "KiB)\n", ++ frame->image->image_id, placement->placement_id, frame->index, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ ++ // Free up ram if needed, but keep the pixmap we've loaded no matter ++ // what. ++ placement->protected_frame = frameidx; ++ gr_check_limits(); ++ placement->protected_frame = 0; ++ ++ return pixmap; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Initialization and deinitialization. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Creates a temporary directory. ++static int gr_create_cache_dir() { ++ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); ++ if (!mkdtemp(cache_dir)) { ++ fprintf(stderr, ++ "error: could not create temporary dir from template " ++ "%s\n", ++ sanitized_filename(cache_dir)); ++ return 0; ++ } ++ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); ++ return 1; ++} ++ ++/// Checks whether `tmp_dir` exists and recreates it if it doesn't. ++static void gr_make_sure_tmpdir_exists() { ++ struct stat st; ++ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) ++ return; ++ fprintf(stderr, ++ "error: %s is not a directory, will need to create a new " ++ "graphics cache directory\n", ++ sanitized_filename(cache_dir)); ++ gr_create_cache_dir(); ++} ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm) { ++ // Set the initialization time. ++ clock_gettime(CLOCK_MONOTONIC, &initialization_time); ++ ++ // Create the temporary dir. ++ if (!gr_create_cache_dir()) ++ abort(); ++ ++ // Initialize imlib. ++ imlib_context_set_display(disp); ++ imlib_context_set_visual(vis); ++ imlib_context_set_colormap(cm); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ // Imlib2 checks only the file name when caching, which is not enough ++ // for us since we reuse file names. Disable caching. ++ imlib_set_cache_size(0); ++ ++ // Prepare for color inversion. ++ for (size_t i = 0; i < 256; ++i) ++ reverse_table[i] = 255 - i; ++ ++ // Create data structures. ++ images = kh_init(id2image); ++ kv_init(next_redraw_times); ++ ++ atexit(gr_deinit); ++} ++ ++/// Deinitialize the graphics module. ++void gr_deinit() { ++ // Remove the cache dir. ++ remove(cache_dir); ++ kv_destroy(next_redraw_times); ++ if (images) { ++ // Delete all images. ++ gr_delete_all_images(); ++ // Destroy the data structures. ++ kh_destroy(id2image, images); ++ images = NULL; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Dumping, debugging, and image preview. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns a string containing a time difference in a human-readable format. ++/// Uses a static buffer, so be careful. ++static const char *gr_ago(Milliseconds diff) { ++ static char result[32]; ++ double seconds = (double)diff / 1000.0; ++ if (seconds < 1) ++ snprintf(result, sizeof(result), "%.2f sec ago", seconds); ++ else if (seconds < 60) ++ snprintf(result, sizeof(result), "%d sec ago", (int)seconds); ++ else if (seconds < 3600) ++ snprintf(result, sizeof(result), "%d min %d sec ago", ++ (int)(seconds / 60), (int)(seconds) % 60); ++ else { ++ snprintf(result, sizeof(result), "%d hr %d min %d sec ago", ++ (int)(seconds / 3600), (int)(seconds) % 3600 / 60, ++ (int)(seconds) % 60); ++ } ++ return result; ++} ++ ++/// Prints to `file` with an indentation of `ind` spaces. ++static void fprintf_ind(FILE *file, int ind, const char *format, ...) { ++ fprintf(file, "%*s", ind, ""); ++ va_list args; ++ va_start(args, format); ++ vfprintf(file, format, args); ++ va_end(args); ++} ++ ++/// Dumps the image info to `file` with an indentation of `ind` spaces. ++static void gr_dump_image_info(FILE *file, Image *img, int ind) { ++ if (!img) { ++ fprintf_ind(file, ind, "Image is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Image %u\n", img->image_id); ++ ind += 4; ++ fprintf_ind(file, ind, "number: %u\n", img->image_number); ++ fprintf_ind(file, ind, "global command index: %lu\n", ++ img->global_command_index); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, ++ gr_ago(now - img->atime)); ++ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, ++ img->pix_height); ++ fprintf_ind(file, ind, "cur frame start time: %ld %s\n", ++ img->current_frame_time, ++ gr_ago(now - img->current_frame_time)); ++ if (img->next_redraw) ++ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", ++ img->next_redraw, img->next_redraw - now); ++ fprintf_ind(file, ind, "total disk size: %u KiB\n", ++ img->total_disk_size / 1024); ++ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); ++ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); ++ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); ++ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); ++ fprintf_ind(file, ind, "default_placement: %u\n", ++ img->default_placement); ++} ++ ++/// Dumps the frame info to `file` with an indentation of `ind` spaces. ++static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { ++ if (!frame) { ++ fprintf_ind(file, ind, "Frame is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Frame %d\n", frame->index); ++ ind += 4; ++ if (frame->index == 0) { ++ fprintf_ind(file, ind, "NOT INITIALIZED\n"); ++ return; ++ } ++ if (frame->uploading_failure) ++ fprintf_ind(file, ind, "uploading failure: %s\n", ++ image_uploading_failure_strings ++ [frame->uploading_failure]); ++ fprintf_ind(file, ind, "gap: %d\n", frame->gap); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, ++ gr_ago(now - frame->atime)); ++ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, ++ frame->data_pix_height); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (access(filename, F_OK) != -1) ++ fprintf_ind(file, ind, "file: %s\n", ++ sanitized_filename(filename)); ++ else ++ fprintf_ind(file, ind, "not on disk\n"); ++ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); ++ if (frame->imlib_object) { ++ unsigned ram_size = gr_frame_current_ram_size(frame); ++ fprintf_ind(file, ind, ++ "loaded into ram, size: %d " ++ "KiB\n", ++ ram_size / 1024); ++ } else { ++ fprintf_ind(file, ind, "not loaded into ram\n"); ++ } ++} ++ ++/// Dumps the placement info to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) { ++ fprintf_ind(file, ind, "Placement is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); ++ ind += 4; ++ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, ++ gr_ago(now - placement->atime)); ++ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); ++ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, ++ placement->rows); ++ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, ++ placement->scaled_ch); ++ fprintf_ind(file, ind, "ram per frame: %u KiB\n", ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ unsigned ram_size = gr_placement_current_ram_size(placement); ++ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); ++} ++ ++/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) ++ return; ++ int frameidx = 1; ++ foreach_pixmap(*placement, pixmap, { ++ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, ++ pixmap); ++ ++frameidx; ++ }); ++} ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state() { ++ FILE *file = stderr; ++ int ind = 0; ++ fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); ++ fprintf_ind(file, ind, ++ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " ++ "sizeof(ImagePlacement) = %lu\n", ++ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); ++ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); ++ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); ++ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", ++ images_ram_size / 1024); ++ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", ++ images_disk_size / 1024); ++ ++ Milliseconds now = gr_now_ms(); ++ ++ int64_t images_ram_size_computed = 0; ++ int64_t images_disk_size_computed = 0; ++ ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ fprintf_ind(file, ind, "----------------\n"); ++ gr_dump_image_info(file, img, 0); ++ int64_t total_disk_size_computed = 0; ++ int total_duration_computed = 0; ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ if (frame->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ total_duration_computed += frame->gap; ++ images_disk_size_computed += frame->disk_size; ++ total_disk_size_computed += frame->disk_size; ++ if (frame->imlib_object) ++ images_ram_size_computed += ++ gr_frame_current_ram_size(frame); ++ }); ++ if (img->total_disk_size != total_disk_size_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_disk_size is %u, but " ++ "computed value is %ld\n", ++ img->total_disk_size, total_disk_size_computed); ++ } ++ if (img->total_duration != total_duration_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_duration is %d, but computed " ++ "value is %d\n", ++ img->total_duration, total_duration_computed); ++ } ++ kh_foreach_value(img->placements, placement, { ++ gr_dump_placement_info(file, placement, 4); ++ if (placement->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ fprintf_ind(file, 8, ++ "Pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 12); ++ unsigned ram_size = ++ gr_placement_current_ram_size(placement); ++ images_ram_size_computed += ram_size; ++ }); ++ }); ++ if (images_ram_size != images_ram_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_ram_size is %ld, but computed value " ++ "is %ld\n", ++ images_ram_size, images_ram_size_computed); ++ } ++ if (images_disk_size != images_disk_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_disk_size is %ld, but computed value " ++ "is %ld\n", ++ images_disk_size, images_disk_size_computed); ++ } ++ fprintf_ind(file, ind, "===========================================\n"); ++} ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++// TODO: Currently we do this for the first frame only. Not sure what to do with ++// animations. ++void gr_preview_image(uint32_t image_id, const char *exec) { ++ char command[256]; ++ size_t len; ++ Image *img = gr_find_image(image_id); ++ if (img) { ++ ImageFrame *frame = &img->first_frame; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (frame->disk_size == 0) { ++ len = snprintf(command, 255, ++ "xmessage 'Image with id=%u is not " ++ "fully copied to %s'", ++ image_id, sanitized_filename(filename)); ++ } else { ++ len = snprintf(command, 255, "%s %s &", exec, ++ sanitized_filename(filename)); ++ } ++ } else { ++ len = snprintf(command, 255, ++ "xmessage 'Cannot find image with id=%u'", ++ image_id); ++ } ++ if (len > 255) { ++ fprintf(stderr, "error: command too long: %s\n", command); ++ snprintf(command, 255, "xmessage 'error: command too long'"); ++ } ++ if (system(command) != 0) { ++ fprintf(stderr, "error: could not execute command %s\n", ++ command); ++ } ++} ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable) { ++ char filename[MAX_FILENAME_SIZE]; ++ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); ++ FILE *file = fopen(filename, "w"); ++ if (!file) { ++ perror("fopen"); ++ return; ++ } ++ // Basic information about the cell. ++ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); ++ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); ++ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); ++ fprintf(file, "classic/unicode placeholder = %s\n", ++ is_classic_placeholder ? "classic" : "unicode"); ++ fprintf(file, "original diacritic count = %d\n", diacritic_count); ++ // Information about the image and the placement. ++ Image *img = gr_find_image(image_id); ++ ImagePlacement *placement = gr_find_placement(img, placement_id); ++ gr_dump_image_info(file, img, 0); ++ gr_dump_placement_info(file, placement, 0); ++ if (img) { ++ fprintf(file, "Frames:\n"); ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ }); ++ } ++ if (placement) { ++ fprintf(file, "Placement pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 4); ++ } ++ fclose(file); ++ char *argv[] = {st_executable, "-e", "less", filename, NULL}; ++ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) { ++ perror("posix_spawnp"); ++ return; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Appending and displaying image rectangles. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Displays debug information in the rectangle using colors col1 and col2. ++static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2, ++ const char *message) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ char info[MAX_INFO_LEN]; ++ if (rect->placement_id) ++ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->placement_id, ++ rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ else ++ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ XSetForeground(disp, gc, col1); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 4, ++ rect->screen_y_pix + h_pix - 3, info, strlen(info)); ++ XSetForeground(disp, gc, col2); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 2, ++ rect->screen_y_pix + h_pix - 5, info, strlen(info)); ++ XFreeGC(disp, gc); ++} ++ ++/// Draws a rectangle (bounding box) for debugging. ++static void gr_showrect(Drawable buf, ImageRect *rect) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ XSetForeground(disp, gc, 0xFF00FF00); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix, ++ w_pix - 1, h_pix - 1); ++ XSetForeground(disp, gc, 0xFFFF0000); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1, ++ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3); ++ XFreeGC(disp, gc); ++} ++ ++/// Updates the next redraw time for the given row. Resizes the ++/// next_redraw_times array if needed. ++static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) { ++ if (next_redraw == 0) ++ return; ++ if (row >= kv_size(next_redraw_times)) { ++ size_t old_size = kv_size(next_redraw_times); ++ kv_a(Milliseconds, next_redraw_times, row); ++ for (size_t i = old_size; i <= row; ++i) ++ kv_A(next_redraw_times, i) = 0; ++ } ++ Milliseconds old_value = kv_A(next_redraw_times, row); ++ if (old_value == 0 || old_value > next_redraw) ++ kv_A(next_redraw_times, row) = next_redraw; ++} ++ ++/// Draws the given part of an image. ++static void gr_drawimagerect(Drawable buf, ImageRect *rect) { ++ ImagePlacement *placement = ++ gr_find_image_and_placement(rect->image_id, rect->placement_id); ++ // If the image does not exist or image display is switched off, draw ++ // the bounding box. ++ if (!placement || !graphics_display_images) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ Image *img = placement->image; ++ ++ if (img->last_redraw < drawing_start_time) { ++ // This is the first time we draw this image in this redraw ++ // cycle. Update the frame index we are going to display. Note ++ // that currently all image placements are synchronized. ++ int old_frame = img->current_frame; ++ gr_update_frame_index(img, drawing_start_time); ++ img->last_redraw = drawing_start_time; ++ } ++ ++ // Adjust next redraw times for the rows of this image rect. ++ if (img->next_redraw) { ++ for (int row = rect->screen_y_row; ++ row <= rect->screen_y_row + rect->img_end_row - ++ rect->img_start_row - 1; ++row) { ++ gr_update_next_redraw_time( ++ row, img->next_redraw); ++ } ++ } ++ ++ // Load the frame. ++ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw, ++ rect->ch); ++ ++ // If the image couldn't be loaded, display the bounding box. ++ if (!pixmap) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ int src_x = rect->img_start_col * rect->cw; ++ int src_y = rect->img_start_row * rect->ch; ++ int width = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int height = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ int dst_x = rect->screen_x_pix; ++ int dst_y = rect->screen_y_pix; ++ ++ // Display the image. ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ ++ // Create an xrender picture for the window. ++ XRenderPictFormat *win_format = ++ XRenderFindVisualFormat(disp, vis); ++ Picture window_pic = ++ XRenderCreatePicture(disp, buf, win_format, 0, NULL); ++ ++ // If needed, invert the image pixmap. Note that this naive approach of ++ // inverting the pixmap is not entirely correct, because the pixmap is ++ // premultiplied. But the result is good enough to visually indicate ++ // selection. ++ if (rect->reverse) { ++ unsigned pixmap_w = ++ (unsigned)placement->cols * placement->scaled_cw; ++ unsigned pixmap_h = ++ (unsigned)placement->rows * placement->scaled_ch; ++ Pixmap invpixmap = ++ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32); ++ XGCValues gcv = {.function = GXcopyInverted}; ++ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv); ++ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w, ++ pixmap_h, 0, 0); ++ XFreeGC(disp, gc); ++ pixmap = invpixmap; ++ } ++ ++ // Create a picture for the image pixmap. ++ XRenderPictFormat *pic_format = ++ XRenderFindStandardFormat(disp, PictStandardARGB32); ++ Picture pixmap_pic = ++ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL); ++ ++ // Composite the image onto the window. In the reverse mode we ignore ++ // the alpha channel of the image because the naive inversion above ++ // seems to invert the alpha channel as well. ++ int pictop = rect->reverse ? PictOpSrc : PictOpOver; ++ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, ++ src_x, src_y, src_x, src_y, dst_x, dst_y, width, ++ height); ++ ++ // Free resources ++ XRenderFreePicture(disp, pixmap_pic); ++ XRenderFreePicture(disp, window_pic); ++ if (rect->reverse) ++ XFreePixmap(disp, pixmap); ++ ++ // In debug mode always draw bounding boxes and print info. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) { ++ gr_showrect(buf, rect); ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ } ++} ++ ++/// Removes the given image rectangle. ++static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); } ++ ++/// Returns the bottom coordinate of the rect. ++static int gr_getrectbottom(ImageRect *rect) { ++ return rect->screen_y_pix + ++ (rect->img_end_row - rect->img_start_row) * rect->ch; ++} ++ ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch) { ++ current_cw = cw; ++ current_ch = ch; ++ this_redraw_cycle_loaded_files = 0; ++ this_redraw_cycle_loaded_pixmaps = 0; ++ drawing_start_time = gr_now_ms(); ++ imlib_context_set_drawable(buf); ++} ++ ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf) { ++ // Draw and then delete all known image rectangles. ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!rect->image_id) ++ continue; ++ gr_drawimagerect(buf, rect); ++ gr_freerect(rect); ++ } ++ ++ // Compute the delay until the next redraw as the minimum of the next ++ // redraw delays for all rows. ++ Milliseconds drawing_end_time = gr_now_ms(); ++ graphics_next_redraw_delay = INT_MAX; ++ for (int row = 0; row < kv_size(next_redraw_times); ++row) { ++ Milliseconds row_next_redraw = kv_A(next_redraw_times, row); ++ if (row_next_redraw > 0) { ++ int delay = MAX(graphics_animation_min_delay, ++ row_next_redraw - drawing_end_time); ++ graphics_next_redraw_delay = ++ MIN(graphics_next_redraw_delay, delay); ++ } ++ } ++ ++ // In debug mode display additional info. ++ if (graphics_debug_mode) { ++ int milliseconds = drawing_end_time - drawing_start_time; ++ ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ const char *debug_mode_str = ++ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES ++ ? "(boxes shown) " ++ : ""; ++ int redraw_delay = graphics_next_redraw_delay == INT_MAX ++ ? -1 ++ : graphics_next_redraw_delay; ++ char info[MAX_INFO_LEN]; ++ snprintf(info, MAX_INFO_LEN, ++ "%sRender time: %d ms ram %ld K disk %ld K count " ++ "%d cell %dx%d delay %d", ++ debug_mode_str, milliseconds, images_ram_size / 1024, ++ images_disk_size / 1024, kh_size(images), current_cw, ++ current_ch, redraw_delay); ++ XSetForeground(disp, gc, 0xFF000000); ++ XFillRectangle(disp, buf, gc, 0, 0, 600, 16); ++ XSetForeground(disp, gc, 0xFFFFFFFF); ++ XDrawString(disp, buf, gc, 0, 14, info, strlen(info)); ++ XFreeGC(disp, gc); ++ ++ if (milliseconds > 0) { ++ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n", ++ info, this_redraw_cycle_loaded_files, ++ this_redraw_cycle_loaded_pixmaps); ++ } ++ } ++ ++ // Check the limits in case we have used too much ram for placements. ++ gr_check_limits(); ++} ++ ++// Add an image rectangle to the list of rectangles to draw. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse) { ++ current_cw = cw; ++ current_ch = ch; ++ ++ ImageRect new_rect; ++ new_rect.image_id = image_id; ++ new_rect.placement_id = placement_id; ++ new_rect.img_start_col = img_start_col; ++ new_rect.img_end_col = img_end_col; ++ new_rect.img_start_row = img_start_row; ++ new_rect.img_end_row = img_end_row; ++ new_rect.screen_y_row = y_row; ++ new_rect.screen_x_pix = x_pix; ++ new_rect.screen_y_pix = y_pix; ++ new_rect.ch = ch; ++ new_rect.cw = cw; ++ new_rect.reverse = reverse; ++ ++ // Display some red text in debug mode. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? "); ++ ++ // If it's the empty image (image_id=0) or an empty rectangle, do ++ // nothing. ++ if (image_id == 0 || img_end_col - img_start_col <= 0 || ++ img_end_row - img_start_row <= 0) ++ return; ++ // Try to find a rect to merge with. ++ ImageRect *free_rect = NULL; ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (rect->image_id == 0) { ++ if (!free_rect) ++ free_rect = rect; ++ continue; ++ } ++ if (rect->image_id != image_id || ++ rect->placement_id != placement_id || rect->cw != cw || ++ rect->ch != ch || rect->reverse != reverse) ++ continue; ++ // We only support the case when the new stripe is added to the ++ // bottom of an existing rectangle and they are perfectly ++ // aligned. ++ if (rect->img_end_row == img_start_row && ++ gr_getrectbottom(rect) == y_pix) { ++ if (rect->img_start_col == img_start_col && ++ rect->img_end_col == img_end_col && ++ rect->screen_x_pix == x_pix) { ++ rect->img_end_row = img_end_row; ++ return; ++ } ++ } ++ } ++ // If we haven't merged the new rect with any existing rect, and there ++ // is no free rect, we have to render one of the existing rects. ++ if (!free_rect) { ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!free_rect || gr_getrectbottom(free_rect) > ++ gr_getrectbottom(rect)) ++ free_rect = rect; ++ } ++ gr_drawimagerect(buf, free_rect); ++ gr_freerect(free_rect); ++ } ++ // Start a new rectangle in `free_rect`. ++ *free_rect = new_rect; ++} ++ ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows) { ++ if (rows < kv_size(next_redraw_times)) ++ kv_size(next_redraw_times) = rows; ++ if (rows * 2 < kv_max(next_redraw_times)) ++ kv_resize(Milliseconds, next_redraw_times, rows); ++ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) { ++ if (dirty[i]) { ++ kv_A(next_redraw_times, i) = 0; ++ continue; ++ } ++ Milliseconds next_update = kv_A(next_redraw_times, i); ++ if (next_update > 0 && next_update <= drawing_start_time) { ++ dirty[i] = 1; ++ kv_A(next_redraw_times, i) = 0; ++ } ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Command parsing and handling. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// A parsed kitty graphics protocol command. ++typedef struct { ++ /// The command itself, without the 'G'. ++ char *command; ++ /// The payload (after ';'). ++ char *payload; ++ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'. ++ char action; ++ /// 'q=', 1 to suppress OK response, 2 to suppress errors too. ++ int quiet; ++ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with ++ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to ++ /// 32-bit pixel data. ++ int format; ++ /// 'o=', may be 'z' for RFC 1950 ZLIB. ++ int compression; ++ /// 't=', may be 'f', 't' or 'd'. ++ char transmission_medium; ++ /// 'd=' ++ char delete_specifier; ++ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'. ++ /// When 'a=f', this is the size of the frame rectangle when composed on ++ /// top of another frame. ++ int frame_pix_width, frame_pix_height; ++ /// 'x=', 'y=' - top-left corner of the source rectangle. ++ int src_pix_x, src_pix_y; ++ /// 'w=', 'h=' - width and height of the source rectangle. ++ int src_pix_width, src_pix_height; ++ /// 'r=', 'c=' ++ int rows, columns; ++ /// 'i=' ++ uint32_t image_id; ++ /// 'I=' ++ uint32_t image_number; ++ /// 'p=' ++ uint32_t placement_id; ++ /// 'm=', may be 0 or 1. ++ int more; ++ /// True if either 'm=0' or 'm=1' is specified. ++ char is_data_transmission; ++ /// True if turns out that this command is a continuation of a data ++ /// transmission and not the first one for this image. Populated by ++ /// `gr_handle_transmit_command`. ++ char is_direct_transmission_continuation; ++ /// 'S=', used to check the size of uploaded data. ++ int size; ++ /// 'U=', whether it's a virtual placement for Unicode placeholders. ++ int virtual; ++ /// 'C=', if true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++ // --------------------------------------------------------------------- ++ // Animation-related fields. Their keys often overlap with keys of other ++ // commands, so these make sense only if the action is 'a=f' (frame ++ // transmission) or 'a=a' (animation control). ++ // ++ // 'x=' and 'y=', the relative position of the frame image when it's ++ // composed on top of another frame. ++ int frame_dst_pix_x, frame_dst_pix_y; ++ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of ++ /// the background color or frame. ++ char replace_instead_of_blending; ++ /// 'Y=', the background color in the 0xRRGGBBAA format (still ++ /// transmitted as a decimal number). ++ uint32_t background_color; ++ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame. ++ int background_frame; ++ /// (Only for 'a=a'). 'c=', sets the index of the current frame. ++ int current_frame; ++ /// 'r=', the 1-based index of the frame to edit. ++ int edit_frame; ++ /// 'z=', the duration of the frame. Zero if not specified, negative if ++ /// the frame is gapless (i.e. skipped). ++ int gap; ++ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the ++ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop. ++ int animation_state; ++ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the ++ /// animation will loop. 1 to loop infinitely, N to loop N-1 times. ++ int loops; ++} GraphicsCommand; ++ ++/// Replaces all non-printed characters in `str` with '?' and truncates the ++/// string to `max_size`, maybe inserting ellipsis at the end. ++static void sanitize_str(char *str, size_t max_size) { ++ assert(max_size >= 4); ++ for (size_t i = 0; i < max_size; ++i) { ++ unsigned c = str[i]; ++ if (c == '\0') ++ return; ++ if (c >= 128 || !isprint(c)) ++ str[i] = '?'; ++ } ++ str[max_size - 1] = '\0'; ++ str[max_size - 2] = '.'; ++ str[max_size - 3] = '.'; ++ str[max_size - 4] = '.'; ++} ++ ++/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be ++/// careful. ++static const char *sanitized_filename(const char *str) { ++ static char buf[MAX_FILENAME_SIZE]; ++ strncpy(buf, str, sizeof(buf)); ++ sanitize_str(buf, sizeof(buf)); ++ return buf; ++} ++ ++/// Creates a response to the current command in `graphics_command_result`. ++static void gr_createresponse(uint32_t image_id, uint32_t image_number, ++ uint32_t placement_id, const char *msg) { ++ if (!image_id && !image_number && !placement_id) { ++ // Nobody expects the response in this case, so just print it to ++ // stderr. ++ fprintf(stderr, ++ "error: No image id or image number or placement_id, " ++ "but still there is a response: %s\n", ++ msg); ++ return; ++ } ++ char *buf = graphics_command_result.response; ++ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN; ++ size_t written; ++ written = snprintf(buf, maxlen, "\033_G"); ++ buf += written; ++ maxlen -= written; ++ if (image_id) { ++ written = snprintf(buf, maxlen, "i=%u,", image_id); ++ buf += written; ++ maxlen -= written; ++ } ++ if (image_number) { ++ written = snprintf(buf, maxlen, "I=%u,", image_number); ++ buf += written; ++ maxlen -= written; ++ } ++ if (placement_id) { ++ written = snprintf(buf, maxlen, "p=%u,", placement_id); ++ buf += written; ++ maxlen -= written; ++ } ++ buf[-1] = ';'; ++ written = snprintf(buf, maxlen, "%s\033\\", msg); ++ buf += written; ++ maxlen -= written; ++ buf[-2] = '\033'; ++ buf[-1] = '\\'; ++} ++ ++/// Creates the 'OK' response to the current command, unless suppressed or a ++/// non-final data transmission. ++static void gr_reportsuccess_cmd(GraphicsCommand *cmd) { ++ if (cmd->quiet < 1 && !cmd->more) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, "OK"); ++} ++ ++/// Creates the 'OK' response to the current command (unless suppressed). ++static void gr_reportsuccess_frame(ImageFrame *frame) { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ if (frame->quiet < 1) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, "OK"); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command); ++ if (cmd->quiet < 2) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, errmsg); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ if (!frame) { ++ fprintf(stderr, "%s\n", errmsg); ++ gr_createresponse(0, 0, 0, errmsg); ++ } else { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ fprintf(stderr, "%s id=%u\n", errmsg, id); ++ if (frame->quiet < 2) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, ++ errmsg); ++ } ++} ++ ++/// Loads an image and creates a success/failure response. Returns `frame`, or ++/// NULL if it's a query action and the image was deleted. ++static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) { ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) { ++ gr_reporterror_frame(frame, "EBADF: could not load image"); ++ } else { ++ gr_reportsuccess_frame(frame); ++ } ++ // If it was a query action, discard the image. ++ if (frame->image->query_id) { ++ gr_delete_image(frame->image); ++ return NULL; ++ } ++ return frame; ++} ++ ++/// Creates an appropriate uploading failure response to the current command. ++static void gr_reportuploaderror(ImageFrame *frame) { ++ switch (frame->uploading_failure) { ++ case 0: ++ return; ++ case ERROR_CANNOT_OPEN_CACHED_FILE: ++ gr_reporterror_frame(frame, ++ "EIO: could not create a file for image"); ++ break; ++ case ERROR_OVER_SIZE_LIMIT: ++ gr_reporterror_frame( ++ frame, ++ "EFBIG: the size of the uploaded image exceeded " ++ "the image size limit %u", ++ graphics_max_single_image_file_size); ++ break; ++ case ERROR_UNEXPECTED_SIZE: ++ gr_reporterror_frame(frame, ++ "EINVAL: the size of the uploaded image %u " ++ "doesn't match the expected size %u", ++ frame->disk_size, frame->expected_size); ++ break; ++ }; ++} ++ ++/// Displays a non-virtual placement. This functions records the information in ++/// `graphics_command_result`, the placeholder itself is created by the terminal ++/// after handling the current command in the graphics module. ++static void gr_display_nonvirtual_placement(ImagePlacement *placement) { ++ if (placement->virtual) ++ return; ++ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS) ++ return; ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ // Populate the information about the placeholder which will be created ++ // by the terminal. ++ graphics_command_result.create_placeholder = 1; ++ graphics_command_result.placeholder.image_id = placement->image->image_id; ++ graphics_command_result.placeholder.placement_id = placement->placement_id; ++ graphics_command_result.placeholder.columns = placement->cols; ++ graphics_command_result.placeholder.rows = placement->rows; ++ graphics_command_result.placeholder.do_not_move_cursor = ++ placement->do_not_move_cursor; ++ GR_LOG("Creating a placeholder for %u/%u %d x %d\n", ++ placement->image->image_id, placement->placement_id, ++ placement->cols, placement->rows); ++} ++ ++/// Marks the rows that are occupied by the image as dirty. ++static void gr_schedule_image_redraw(Image *img) { ++ if (!img) ++ return; ++ gr_schedule_image_redraw_by_id(img->image_id); ++} ++ ++/// Appends data from `payload` to the frame `frame` when using direct ++/// transmission. Note that we report errors only for the final command ++/// (`!more`) to avoid spamming the client. If the frame is not specified, use ++/// the image id and frame index we are currently uploading. ++static void gr_append_data(ImageFrame *frame, const char *payload, int more) { ++ if (!frame) { ++ Image *img = gr_find_image(current_upload_image_id); ++ frame = gr_get_frame(img, current_upload_frame_index); ++ GR_LOG("Appending data to image %u frame %d\n", ++ current_upload_image_id, current_upload_frame_index); ++ if (!img) ++ GR_LOG("ERROR: this image doesn't exist\n"); ++ if (!frame) ++ GR_LOG("ERROR: this frame doesn't exist\n"); ++ } ++ if (!more) { ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ } ++ if (!frame) { ++ if (!more) ++ gr_reporterror_frame(NULL, "ENOENT: could not find the " ++ "image to append data to"); ++ return; ++ } ++ if (frame->status != STATUS_UPLOADING) { ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // Decode the data. ++ size_t data_size = 0; ++ char *data = gr_base64dec(payload, &data_size); ++ ++ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size, ++ frame->disk_size + data_size); ++ ++ // Do not append this data if the image exceeds the size limit. ++ if (frame->disk_size + data_size > ++ graphics_max_single_image_file_size || ++ frame->expected_size > graphics_max_single_image_file_size) { ++ free(data); ++ gr_delete_imagefile(frame); ++ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // If there is no open file corresponding to the image, create it. ++ if (!frame->open_file) { ++ gr_make_sure_tmpdir_exists(); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ FILE *file = fopen(filename, frame->disk_size ? "a" : "w"); ++ if (!file) { ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ frame->open_file = file; ++ } ++ ++ // Write data to the file and update disk size variables. ++ fwrite(data, 1, data_size, frame->open_file); ++ free(data); ++ frame->disk_size += data_size; ++ frame->image->total_disk_size += data_size; ++ images_disk_size += data_size; ++ gr_touch_frame(frame); ++ ++ if (more) { ++ current_upload_image_id = frame->image->image_id; ++ current_upload_frame_index = frame->index; ++ } else { ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ // Close the file. ++ if (frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ uint32_t placement_id = frame->image->default_placement; ++ if (frame->expected_size && ++ frame->expected_size != frame->disk_size) { ++ // Report failure if the uploaded image size doesn't ++ // match the expected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ gr_reportuploaderror(frame); ++ } else { ++ // Make sure to redraw all existing image instances. ++ gr_schedule_image_redraw(frame->image); ++ // Try to load the image into ram and report the result. ++ frame = gr_loadimage_and_report(frame); ++ // If there is a non-virtual image placement, we may ++ // need to display it. ++ if (frame && frame->index == 1) { ++ Image *img = frame->image; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_display_nonvirtual_placement(placement); ++ }); ++ } ++ } ++ } ++ ++ // Check whether we need to delete old images. ++ gr_check_limits(); ++} ++ ++/// Finds the image either by id or by number specified in the command and sets ++/// the image_id of `cmd` if the image was found. ++static Image *gr_find_image_for_command(GraphicsCommand *cmd) { ++ if (cmd->image_id) ++ return gr_find_image(cmd->image_id); ++ Image *img = NULL; ++ // If the image number is not specified, we can't find the image, unless ++ // it's a put command, in which case we will try the last image. ++ if (cmd->image_number == 0 && cmd->action == 'p') ++ img = gr_find_image(last_image_id); ++ else ++ img = gr_find_image_by_number(cmd->image_number); ++ if (img) ++ cmd->image_id = img->image_id; ++ return img; ++} ++ ++/// Creates a new image or a new frame in an existing image (depending on the ++/// command's action) and initializes its parameters from the command. ++static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) { ++ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 && ++ cmd->compression != 0) { ++ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only " ++ "for raw pixel data (f=32 or f=24)"); ++ // Even though we report an error, we still create an image. ++ } ++ ++ Image *img = NULL; ++ if (cmd->action == 'f') { ++ // If it's a frame transmission action, there must be an ++ // existing image. ++ img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return NULL; ++ } ++ } else { ++ // Otherwise create a new image object. If the action is `q`, ++ // we'll use random id instead of the one specified in the ++ // command. ++ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id; ++ img = gr_new_image(image_id); ++ if (!img) ++ return NULL; ++ if (cmd->action == 'q') ++ img->query_id = cmd->image_id; ++ else if (!cmd->image_id) ++ cmd->image_id = img->image_id; ++ // Set the image number. ++ img->image_number = cmd->image_number; ++ } ++ ++ ImageFrame *frame = gr_append_new_frame(img); ++ // Initialize the frame. ++ frame->expected_size = cmd->size; ++ frame->format = cmd->format; ++ frame->compression = cmd->compression; ++ frame->background_color = cmd->background_color; ++ frame->background_frame_index = cmd->background_frame; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ frame->blend = !cmd->replace_instead_of_blending; ++ frame->data_pix_width = cmd->frame_pix_width; ++ frame->data_pix_height = cmd->frame_pix_height; ++ if (cmd->action == 'f') { ++ frame->x = cmd->frame_dst_pix_x; ++ frame->y = cmd->frame_dst_pix_y; ++ } ++ // We save the quietness information in the frame because for direct ++ // transmission subsequent transmission command won't contain this info. ++ frame->quiet = cmd->quiet; ++ return frame; ++} ++ ++/// Removes a file if it actually looks like a temporary file. ++static void gr_delete_tmp_file(const char *filename) { ++ if (strstr(filename, "tty-graphics-protocol") == NULL) ++ return; ++ if (strstr(filename, "/tmp/") != filename) { ++ const char *tmpdir = getenv("TMPDIR"); ++ if (!tmpdir || !tmpdir[0] || ++ strstr(filename, tmpdir) != filename) ++ return; ++ } ++ unlink(filename); ++} ++ ++/// Handles a data transmission command. ++static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) { ++ // The default is direct transmission. ++ if (!cmd->transmission_medium) ++ cmd->transmission_medium = 'd'; ++ ++ // If neither id, nor image number is specified, and the transmission ++ // medium is 'd' (or unspecified), and there is an active direct upload, ++ // this is a continuation of the upload. ++ if (current_upload_image_id != 0 && cmd->image_id == 0 && ++ cmd->image_number == 0 && cmd->transmission_medium == 'd') { ++ cmd->image_id = current_upload_image_id; ++ GR_LOG("No images id is specified, continuing uploading %u\n", ++ cmd->image_id); ++ } ++ ++ ImageFrame *frame = NULL; ++ if (cmd->transmission_medium == 'f' || ++ cmd->transmission_medium == 't') { ++ // File transmission. ++ // Create a new image or a new frame of an existing image. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ // Decode the filename. ++ char *original_filename = gr_base64dec(cmd->payload, NULL); ++ GR_LOG("Copying image %s\n", ++ sanitized_filename(original_filename)); ++ // Stat the file and check that it's a regular file and not too ++ // big. ++ struct stat st; ++ int stat_res = stat(original_filename, &st); ++ const char *stat_error = NULL; ++ if (stat_res) ++ stat_error = strerror(errno); ++ else if (!S_ISREG(st.st_mode)) ++ stat_error = "Not a regular file"; ++ else if (st.st_size == 0) ++ stat_error = "The size of the file is zero"; ++ else if (st.st_size > graphics_max_single_image_file_size) ++ stat_error = "The file is too large"; ++ if (stat_error) { ++ gr_reporterror_cmd(cmd, ++ "EBADF: %s", stat_error); ++ fprintf(stderr, "Could not load the file %s\n", ++ sanitized_filename(original_filename)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ } else { ++ gr_make_sure_tmpdir_exists(); ++ // Build the filename for the cached copy of the file. ++ char cache_filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, cache_filename, ++ MAX_FILENAME_SIZE); ++ // We will create a symlink to the original file, and ++ // then copy the file to the temporary cache dir. We do ++ // this symlink trick mostly to be able to use cp for ++ // copying, and avoid escaping file name characters when ++ // calling system at the same time. ++ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0}; ++ strcat(tmp_filename_symlink, cache_filename); ++ strcat(tmp_filename_symlink, ".sym"); ++ char command[MAX_FILENAME_SIZE + 256]; ++ size_t len = ++ snprintf(command, MAX_FILENAME_SIZE + 255, ++ "cp '%s' '%s'", tmp_filename_symlink, ++ cache_filename); ++ if (len > MAX_FILENAME_SIZE + 255 || ++ symlink(original_filename, tmp_filename_symlink) || ++ system(command) != 0) { ++ gr_reporterror_cmd(cmd, ++ "EBADF: could not copy the " ++ "image to the cache dir"); ++ fprintf(stderr, ++ "Could not copy the image " ++ "%s (symlink %s) to %s", ++ sanitized_filename(original_filename), ++ tmp_filename_symlink, cache_filename); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ } else { ++ // Get the file size of the copied file. ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ frame->disk_size = st.st_size; ++ frame->image->total_disk_size += st.st_size; ++ images_disk_size += frame->disk_size; ++ if (frame->expected_size && ++ frame->expected_size != frame->disk_size) { ++ // The file has unexpected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ++ ERROR_UNEXPECTED_SIZE; ++ gr_reportuploaderror(frame); ++ } else { ++ // Everything seems fine, try to load ++ // and redraw existing instances. ++ gr_schedule_image_redraw(frame->image); ++ frame = gr_loadimage_and_report(frame); ++ } ++ } ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ // Delete the original file if it's temporary. ++ if (cmd->transmission_medium == 't') ++ gr_delete_tmp_file(original_filename); ++ } ++ free(original_filename); ++ gr_check_limits(); ++ } else if (cmd->transmission_medium == 'd') { ++ // Direct transmission (default if 't' is not specified). ++ frame = gr_get_last_frame(gr_find_image_for_command(cmd)); ++ if (frame && frame->status == STATUS_UPLOADING) { ++ // This is a continuation of the previous transmission. ++ cmd->is_direct_transmission_continuation = 1; ++ gr_append_data(frame, cmd->payload, cmd->more); ++ return frame; ++ } ++ // If no action is specified, it's not the first transmission ++ // command. If we couldn't find the image, something went wrong ++ // and we should just drop this command. ++ if (cmd->action == 0) ++ return NULL; ++ // Otherwise create a new image or frame structure. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ frame->status = STATUS_UPLOADING; ++ // Start appending data. ++ gr_append_data(frame, cmd->payload, cmd->more); ++ } else { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: transmission medium '%c' is not supported", ++ cmd->transmission_medium); ++ return NULL; ++ } ++ ++ return frame; ++} ++ ++/// Handles the 'put' command by creating a placement. ++static void gr_handle_put_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Create a placement. If a placement with the same id already exists, ++ // it will be deleted. If the id is zero, a random id will be generated. ++ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id); ++ placement->virtual = cmd->virtual; ++ placement->src_pix_x = cmd->src_pix_x; ++ placement->src_pix_y = cmd->src_pix_y; ++ placement->src_pix_width = cmd->src_pix_width; ++ placement->src_pix_height = cmd->src_pix_height; ++ placement->cols = cmd->columns; ++ placement->rows = cmd->rows; ++ placement->do_not_move_cursor = cmd->do_not_move_cursor; ++ ++ if (placement->virtual) { ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else if (placement->cols && placement->rows) { ++ // For classic placements the default is to stretch the image if ++ // both cols and rows are specified. ++ placement->scale_mode = SCALE_MODE_FILL; ++ } else if (placement->cols || placement->rows) { ++ // But if only one of them is specified, the default is to ++ // contain. ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else { ++ // If none of them are specified, the default is to use the ++ // original size. ++ placement->scale_mode = SCALE_MODE_NONE; ++ } ++ ++ // Display the placement unless it's virtual. ++ gr_display_nonvirtual_placement(placement); ++ ++ // Report success. ++ gr_reportsuccess_cmd(cmd); ++} ++ ++/// Information about what to delete. ++typedef struct DeletionData { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// If true, delete the image object if there are no more placements. ++ char delete_image_if_no_ref; ++} DeletionData; ++ ++/// The callback called for each cell to perform deletion. ++static int gr_deletion_callback(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic) { ++ DeletionData *del_data = data; ++ // Leave unicode placeholders alone. ++ if (!is_classic) ++ return 0; ++ if (del_data->image_id && del_data->image_id != image_id) ++ return 0; ++ if (del_data->placement_id && del_data->placement_id != placement_id) ++ return 0; ++ Image *img = gr_find_image(image_id); ++ // If the image is already deleted, just erase the placeholder. ++ if (!img) ++ return 1; ++ // Delete the placement. ++ if (placement_id) ++ gr_delete_placement(gr_find_placement(img, placement_id)); ++ // Delete the image if image deletion is requested (uppercase delete ++ // specifier) and there are no more placements. ++ if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0) ++ gr_delete_image(img); ++ return 1; ++} ++ ++/// Handles the delete command. ++static void gr_handle_delete_command(GraphicsCommand *cmd) { ++ DeletionData del_data = {0}; ++ del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0; ++ char d = tolower(cmd->delete_specifier); ++ ++ if (d == 'n') { ++ d = 'i'; ++ Image *img = gr_find_image_by_number(cmd->image_number); ++ if (!img) ++ return; ++ del_data.image_id = img->image_id; ++ } ++ ++ if (!d || d == 'a') { ++ // Delete all visible placements. ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else if (d == 'i') { ++ // Delete the specified image by image id and maybe placement ++ // id. ++ if (!del_data.image_id) ++ del_data.image_id = cmd->image_id; ++ if (!del_data.image_id) { ++ fprintf(stderr, ++ "ERROR: image id is not specified in the " ++ "delete command\n"); ++ return; ++ } ++ del_data.placement_id = cmd->placement_id; ++ // NOTE: It's not very clear whether we should delete the image ++ // even if there are no _visible_ placements to delete. We do ++ // this because otherwise there is no way to delete an image ++ // with virtual placements in one command. ++ if (!del_data.placement_id && del_data.delete_image_if_no_ref) ++ gr_delete_image(gr_find_image(cmd->image_id)); ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else { ++ fprintf(stderr, ++ "WARNING: unsupported value of the d key: '%c'. The " ++ "command is ignored.\n", ++ cmd->delete_specifier); ++ } ++} ++ ++static void gr_handle_animation_control_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Find the frame to edit, if requested. ++ ImageFrame *frame = NULL; ++ if (cmd->edit_frame) ++ frame = gr_get_frame(img, cmd->edit_frame); ++ if (cmd->edit_frame || cmd->gap) { ++ if (!frame) { ++ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found", ++ cmd->edit_frame); ++ return; ++ } ++ if (cmd->gap) { ++ img->total_duration -= frame->gap; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ } ++ } ++ ++ // Set animation-related parameters of the image. ++ if (cmd->current_frame) ++ img->current_frame = cmd->current_frame; ++ if (cmd->animation_state) { ++ if (cmd->animation_state == 1) { ++ img->animation_state = ANIMATION_STATE_STOPPED; ++ } else if (cmd->animation_state == 2) { ++ img->animation_state = ANIMATION_STATE_LOADING; ++ } else if (cmd->animation_state == 3) { ++ img->animation_state = ANIMATION_STATE_LOOPING; ++ } else { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: invalid animation state: %d", ++ cmd->animation_state); ++ } ++ } ++ // TODO: Set the number of loops to cmd->loops ++ ++ // Make sure we redraw all instances of the image. ++ gr_schedule_image_redraw(img); ++} ++ ++/// Handles a command. ++static void gr_handle_command(GraphicsCommand *cmd) { ++ if (!cmd->image_id && !cmd->image_number) { ++ // If there is no image id or image number, nobody expects a ++ // response, so set quiet to 2. ++ cmd->quiet = 2; ++ } ++ ImageFrame *frame = NULL; ++ switch (cmd->action) { ++ case 0: ++ // If no action is specified, it may be a data transmission ++ // command if 'm=' is specified. ++ if (cmd->is_data_transmission) { ++ gr_handle_transmit_command(cmd); ++ break; ++ } ++ gr_reporterror_cmd(cmd, "EINVAL: no action specified"); ++ break; ++ case 't': ++ case 'q': ++ case 'f': ++ // Transmit data. 'q' means query, which is basically the same ++ // as transmit, but the image is discarded, and the id is fake. ++ // 'f' appends a frame to an existing image. ++ gr_handle_transmit_command(cmd); ++ break; ++ case 'p': ++ // Display (put) the image. ++ gr_handle_put_command(cmd); ++ break; ++ case 'T': ++ // Transmit and display. ++ frame = gr_handle_transmit_command(cmd); ++ if (frame && !cmd->is_direct_transmission_continuation) { ++ gr_handle_put_command(cmd); ++ if (cmd->placement_id) ++ frame->image->initial_placement_id = ++ cmd->placement_id; ++ } ++ break; ++ case 'd': ++ gr_handle_delete_command(cmd); ++ break; ++ case 'a': ++ gr_handle_animation_control_command(cmd); ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c", ++ cmd->action); ++ return; ++ } ++} ++ ++/// A partially parsed key-value pair. ++typedef struct KeyAndValue { ++ char *key_start; ++ char *val_start; ++ unsigned key_len, val_len; ++} KeyAndValue; ++ ++/// Parses the value of a key and assigns it to the appropriate field of `cmd`. ++static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) { ++ char *key_start = kv->key_start; ++ char *key_end = key_start + kv->key_len; ++ char *value_start = kv->val_start; ++ char *value_end = value_start + kv->val_len; ++ // Currently all keys are one-character. ++ if (key_end - key_start != 1) { ++ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s", ++ key_end - key_start, key_start); ++ return; ++ } ++ long num = 0; ++ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' || ++ *key_start == 'o') { ++ // Some keys have one-character values. ++ if (value_end - value_start != 1) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: value of 'a', 't' or 'd' must be a " ++ "single char: %s", ++ key_start); ++ return; ++ } ++ } else { ++ // All the other keys have integer values. ++ char *num_end = NULL; ++ num = strtol(value_start, &num_end, 10); ++ if (num_end != value_end) { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: could not parse number value: %s", ++ key_start); ++ return; ++ } ++ } ++ switch (*key_start) { ++ case 'a': ++ cmd->action = *value_start; ++ break; ++ case 't': ++ cmd->transmission_medium = *value_start; ++ break; ++ case 'd': ++ cmd->delete_specifier = *value_start; ++ break; ++ case 'q': ++ cmd->quiet = num; ++ break; ++ case 'f': ++ cmd->format = num; ++ if (num != 0 && num != 24 && num != 32 && num != 100) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: unsupported format specification: %s", ++ key_start); ++ } ++ break; ++ case 'o': ++ cmd->compression = *value_start; ++ if (cmd->compression != 'z') { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: unsupported compression " ++ "specification: %s", ++ key_start); ++ } ++ break; ++ case 's': ++ if (cmd->action == 'a') ++ cmd->animation_state = num; ++ else ++ cmd->frame_pix_width = num; ++ break; ++ case 'v': ++ if (cmd->action == 'a') ++ cmd->loops = num; ++ else ++ cmd->frame_pix_height = num; ++ break; ++ case 'i': ++ cmd->image_id = num; ++ break; ++ case 'I': ++ cmd->image_number = num; ++ break; ++ case 'p': ++ cmd->placement_id = num; ++ break; ++ case 'x': ++ cmd->src_pix_x = num; ++ cmd->frame_dst_pix_x = num; ++ break; ++ case 'y': ++ if (cmd->action == 'f') ++ cmd->frame_dst_pix_y = num; ++ else ++ cmd->src_pix_y = num; ++ break; ++ case 'w': ++ cmd->src_pix_width = num; ++ break; ++ case 'h': ++ cmd->src_pix_height = num; ++ break; ++ case 'c': ++ if (cmd->action == 'f') ++ cmd->background_frame = num; ++ else if (cmd->action == 'a') ++ cmd->current_frame = num; ++ else ++ cmd->columns = num; ++ break; ++ case 'r': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->edit_frame = num; ++ else ++ cmd->rows = num; ++ break; ++ case 'm': ++ cmd->is_data_transmission = 1; ++ cmd->more = num; ++ break; ++ case 'S': ++ cmd->size = num; ++ break; ++ case 'U': ++ cmd->virtual = num; ++ break; ++ case 'X': ++ if (cmd->action == 'f') ++ cmd->replace_instead_of_blending = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'Y': ++ if (cmd->action == 'f') ++ cmd->background_color = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'z': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->gap = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'C': ++ cmd->do_not_move_cursor = num; ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s", ++ key_start); ++ return; ++ } ++} ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters. Returns 1 on success. ++int gr_parse_command(char *buf, size_t len) { ++ if (buf[0] != 'G') ++ return 0; ++ ++ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); ++ ++ global_command_counter++; ++ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); ++ ++ // Eat the 'G'. ++ ++buf; ++ --len; ++ ++ GraphicsCommand cmd = {.command = buf}; ++ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to ++ // parse the payload. ++ char state = 'k'; ++ // An array of partially parsed key-value pairs. ++ KeyAndValue key_vals[32]; ++ unsigned key_vals_count = 0; ++ char *key_start = buf; ++ char *key_end = NULL; ++ char *val_start = NULL; ++ char *val_end = NULL; ++ char *c = buf; ++ while (c - buf < len + 1) { ++ if (state == 'k') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ key_end = c; ++ gr_reporterror_cmd( ++ &cmd, "EINVAL: key without value: %s ", ++ key_start); ++ break; ++ case '=': ++ key_end = c; ++ state = 'v'; ++ val_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'v') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ val_end = c; ++ if (key_vals_count >= ++ sizeof(key_vals) / sizeof(*key_vals)) { ++ gr_reporterror_cmd(&cmd, ++ "EINVAL: too many " ++ "key-value pairs"); ++ break; ++ } ++ key_vals[key_vals_count].key_start = key_start; ++ key_vals[key_vals_count].val_start = val_start; ++ key_vals[key_vals_count].key_len = ++ key_end - key_start; ++ key_vals[key_vals_count].val_len = ++ val_end - val_start; ++ ++key_vals_count; ++ key_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'p') { ++ cmd.payload = c; ++ // break out of the loop, we don't check the payload ++ break; ++ } ++ ++c; ++ } ++ ++ // Set the action key ('a=') first because we need it to disambiguate ++ // some keys. Also set 'i=' and 'I=' for better error reporting. ++ for (unsigned i = 0; i < key_vals_count; ++i) { ++ if (key_vals[i].key_len == 1) { ++ char *start = key_vals[i].key_start; ++ if (*start == 'a' || *start == 'i' || *start == 'I') { ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ break; ++ } ++ } ++ } ++ // Set the rest of the keys. ++ for (unsigned i = 0; i < key_vals_count; ++i) ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ ++ if (!cmd.payload) ++ cmd.payload = buf + len; ++ ++ if (cmd.payload && cmd.payload[0]) ++ GR_LOG(" payload size: %ld\n", strlen(cmd.payload)); ++ ++ if (!graphics_command_result.error) ++ gr_handle_command(&cmd); ++ ++ if (graphics_debug_mode) { ++ fprintf(stderr, "Response: "); ++ for (const char *resp = graphics_command_result.response; ++ *resp != '\0'; ++resp) { ++ if (isprint(*resp)) ++ fprintf(stderr, "%c", *resp); ++ else ++ fprintf(stderr, "(0x%x)", *resp); ++ } ++ fprintf(stderr, "\n"); ++ } ++ ++ // Make sure that we suppress response if needed. Usually cmd.quiet is ++ // taken into account when creating the response, but it's not very ++ // reliable in the current implementation. ++ if (cmd.quiet) { ++ if (!graphics_command_result.error || cmd.quiet >= 2) ++ graphics_command_result.response[0] = '\0'; ++ } ++ ++ return 1; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// base64 decoding part is basically copied from st.c ++//////////////////////////////////////////////////////////////////////////////// ++ ++static const char gr_base64_digits[] = { ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, ++ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, ++ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ++ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, ++ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, ++ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; ++ ++static char gr_base64_getc(const char **src) { ++ while (**src && !isprint(**src)) ++ (*src)++; ++ return **src ? *((*src)++) : '='; /* emulate padding if string ends */ ++} ++ ++char *gr_base64dec(const char *src, size_t *size) { ++ size_t in_len = strlen(src); ++ char *result, *dst; ++ ++ result = dst = malloc((in_len + 3) / 4 * 3 + 1); ++ while (*src) { ++ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ ++ if (a == -1 || b == -1) ++ break; ++ ++ *dst++ = (a << 2) | ((b & 0x30) >> 4); ++ if (c == -1) ++ break; ++ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); ++ if (d == -1) ++ break; ++ *dst++ = ((c & 0x03) << 6) | d; ++ } ++ *dst = '\0'; ++ if (size) { ++ *size = dst - result; ++ } ++ return result; ++} +diff --git a/graphics.h b/graphics.h +new file mode 100644 +index 0000000..2e75dea +--- /dev/null ++++ b/graphics.h +@@ -0,0 +1,107 @@ ++ ++#include ++#include ++#include ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm); ++/// Deinitialize the graphics module. ++void gr_deinit(); ++ ++/// Add an image rectangle to a list if rectangles to draw. This function may ++/// actually draw some rectangles, or it may wait till more rectangles are ++/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. ++/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the ++/// part of the image to draw (row/col indices are zero-based, ends are ++/// excluded). ++/// - `x_col` and `y_row` are the coordinates of the top-left corner of the ++/// image in the terminal grid. ++/// - `x_pix` and `y_pix` are the same but in pixels. ++/// - `reverse` indicates whether colors should be inverted. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse); ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch); ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf); ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows); ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters (including '\0'). Returns 1 on success. ++/// Additional informations is returned through `graphics_command_result`. ++int gr_parse_command(char *buf, size_t len); ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++void gr_preview_image(uint32_t image_id, const char *command); ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable); ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state(); ++ ++/// Unloads images to reduce RAM usage. ++void gr_unload_images_to_reduce_ram(); ++ ++/// Executes `callback` for each image cell. `callback` may return 1 to erase ++/// the cell or 0 to keep it. This function is implemented in `st.c`. ++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic), ++ void *data); ++ ++/// Marks all the rows containing the image with `image_id` as dirty. ++void gr_schedule_image_redraw_by_id(uint32_t image_id); ++ ++typedef enum { ++ GRAPHICS_DEBUG_NONE = 0, ++ GRAPHICS_DEBUG_LOG = 1, ++ GRAPHICS_DEBUG_LOG_AND_BOXES = 2, ++} GraphicsDebugMode; ++ ++/// Print additional information, draw bounding bounding boxes, etc. ++extern GraphicsDebugMode graphics_debug_mode; ++ ++/// Whether to display images or just draw bounding boxes. ++extern char graphics_display_images; ++ ++/// The time in milliseconds until the next redraw to update animations. ++/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`. ++extern int graphics_next_redraw_delay; ++ ++#define MAX_GRAPHICS_RESPONSE_LEN 256 ++ ++/// A structure representing the result of a graphics command. ++typedef struct { ++ /// Indicates if the terminal needs to be redrawn. ++ char redraw; ++ /// The response of the command that should be sent back to the client ++ /// (may be empty if the quiet flag is set). ++ char response[MAX_GRAPHICS_RESPONSE_LEN]; ++ /// Whether there was an error executing this command (not very useful, ++ /// the response must be sent back anyway). ++ char error; ++ /// Whether the terminal has to create a placeholder for a non-virtual ++ /// placement. ++ char create_placeholder; ++ /// The placeholder that needs to be created. ++ struct { ++ uint32_t rows, columns; ++ uint32_t image_id, placement_id; ++ char do_not_move_cursor; ++ } placeholder; ++} GraphicsCommandResult; ++ ++/// The result of a graphics command. ++extern GraphicsCommandResult graphics_command_result; +diff --git a/icat-mini.sh b/icat-mini.sh +new file mode 100755 +index 0000000..0a8ebab +--- /dev/null ++++ b/icat-mini.sh +@@ -0,0 +1,801 @@ ++#!/bin/sh ++ ++# vim: shiftwidth=4 ++ ++script_name="$(basename "$0")" ++ ++short_help="Usage: $script_name [OPTIONS] ++ ++This is a script to display images in the terminal using the kitty graphics ++protocol with Unicode placeholders. It is very basic, please use something else ++if you have alternatives. ++ ++Options: ++ -h Show this help. ++ -s SCALE The scale of the image, may be floating point. ++ -c N, --cols N The number of columns. ++ -r N, --rows N The number of rows. ++ --max-cols N The maximum number of columns. ++ --max-rows N The maximum number of rows. ++ --cell-size WxH The cell size in pixels. ++ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. ++ --speed SPEED The multiplier for the animation speed (float). ++" ++ ++# Exit the script on keyboard interrupt ++trap "echo 'icat-mini was interrupted' >&2; exit 1" INT ++ ++cols="" ++rows="" ++file="" ++tty="/dev/tty" ++uploading_method="auto" ++cell_size="" ++scale=1 ++max_cols="" ++max_rows="" ++speed="" ++ ++# Parse the command line. ++while [ $# -gt 0 ]; do ++ case "$1" in ++ -c|--columns|--cols) ++ cols="$2" ++ shift 2 ++ ;; ++ -r|--rows|-l|--lines) ++ rows="$2" ++ shift 2 ++ ;; ++ -s|--scale) ++ scale="$2" ++ shift 2 ++ ;; ++ -h|--help) ++ echo "$short_help" ++ exit 0 ++ ;; ++ -m|--upload-method|--uploading-method) ++ uploading_method="$2" ++ shift 2 ++ ;; ++ --cell-size) ++ cell_size="$2" ++ shift 2 ++ ;; ++ --max-cols) ++ max_cols="$2" ++ shift 2 ++ ;; ++ --max-rows) ++ max_rows="$2" ++ shift 2 ++ ;; ++ --speed) ++ speed="$2" ++ shift 2 ++ ;; ++ --) ++ file="$2" ++ shift 2 ++ ;; ++ -*) ++ echo "Unknown option: $1" >&2 ++ exit 1 ++ ;; ++ *) ++ if [ -n "$file" ]; then ++ echo "Multiple image files are not supported: $file and $1" >&2 ++ exit 1 ++ fi ++ file="$1" ++ shift ++ ;; ++ esac ++done ++ ++file="$(realpath "$file")" ++ ++##################################################################### ++# Adjust the terminal state ++##################################################################### ++ ++stty_orig="$(stty -g < "$tty")" ++stty -echo < "$tty" ++# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some ++# horrible issues otherwise. ++stty susp undef < "$tty" ++stty -icanon < "$tty" ++ ++restore_echo() { ++ [ -n "$stty_orig" ] || return ++ stty $stty_orig < "$tty" ++} ++ ++trap restore_echo EXIT TERM ++ ++##################################################################### ++# Detect imagemagick ++##################################################################### ++ ++# If there is the 'magick' command, use it instead of separate 'convert' and ++# 'identify' commands. ++if command -v magick > /dev/null; then ++ identify="magick identify" ++ convert="magick" ++else ++ identify="identify" ++ convert="convert" ++fi ++ ++##################################################################### ++# Detect tmux ++##################################################################### ++ ++# Check if we are inside tmux. ++inside_tmux="" ++if [ -n "$TMUX" ]; then ++ case "$TERM" in ++ *tmux*|*screen*) ++ inside_tmux=1 ++ ;; ++ esac ++fi ++ ++##################################################################### ++# Compute the number of rows and columns ++##################################################################### ++ ++is_pos_int() { ++ if [ -z "$1" ]; then ++ return 1 # false ++ fi ++ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then ++ if [ "$1" -gt 0 ]; then ++ return 0 # true ++ fi ++ fi ++ return 1 # false ++} ++ ++if [ -n "$cols" ] || [ -n "$rows" ]; then ++ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then ++ echo "You can't specify both max-cols/rows and cols/rows" >&2 ++ exit 1 ++ fi ++fi ++ ++# Get the max number of cols and rows. ++[ -n "$max_cols" ] || max_cols="$(tput cols)" ++[ -n "$max_rows" ] || max_rows="$(tput lines)" ++if [ "$max_rows" -gt 255 ]; then ++ max_rows=255 ++fi ++ ++python_ioctl_command="import array, fcntl, termios ++buf = array.array('H', [0, 0, 0, 0]) ++fcntl.ioctl(0, termios.TIOCGWINSZ, buf) ++print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" ++ ++# Get the cell size in pixels if either cols or rows are not specified. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ cell_width="" ++ cell_height="" ++ # If the cell size is specified, use it. ++ if [ -n "$cell_size" ]; then ++ cell_width="${cell_size%x*}" ++ cell_height="${cell_size#*x}" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ echo "Invalid cell size: $cell_size" >&2 ++ exit 1 ++ fi ++ fi ++ # Otherwise try to use TIOCGWINSZ ioctl via python. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)" ++ cell_width="${cell_size_ioctl% *}" ++ cell_height="${cell_size_ioctl#* }" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width="" ++ cell_height="" ++ fi ++ fi ++ # If it didn't work, try to use csi XTWINOPS. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ if [ -n "$inside_tmux" ]; then ++ printf '\ePtmux;\e\e[16t\e\\' >> "$tty" ++ else ++ printf '\e[16t' >> "$tty" ++ fi ++ # The expected response will look like ^[[6;;t ++ term_response="" ++ while true; do ++ char=$(dd bs=1 count=1 <"$tty" 2>/dev/null) ++ if [ "$char" = "t" ]; then ++ break ++ fi ++ term_response="$term_response$char" ++ done ++ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" ++ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width=8 ++ cell_height=16 ++ fi ++ fi ++fi ++ ++# Compute a formula with bc and round to the nearest integer. ++bc_round() { ++ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" ++} ++ ++# Compute the number of rows and columns of the image. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ # Get the size of the image and its resolution. If it's an animation, use ++ # the first frame. ++ format_output="$($identify -format '%w %h\n' "$file" | head -1)" ++ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" ++ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" ++ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then ++ echo "Couldn't get image size from identify: $format_output" >&2 ++ echo >&2 ++ exit 1 ++ fi ++ opt_cols_expr="(${scale}*${img_width}/${cell_width})" ++ opt_rows_expr="(${scale}*${img_height}/${cell_height})" ++ if [ -z "$cols" ] && [ -z "$rows" ]; then ++ # If columns and rows are not specified, compute the optimal values ++ # using the information about rows and columns per inch. ++ cols="$(bc_round "$opt_cols_expr")" ++ rows="$(bc_round "$opt_rows_expr")" ++ # Make sure that automatically computed rows and columns are within some ++ # sane limits ++ if [ "$cols" -gt "$max_cols" ]; then ++ rows="$(bc_round "$rows * $max_cols / $cols")" ++ cols="$max_cols" ++ fi ++ if [ "$rows" -gt "$max_rows" ]; then ++ cols="$(bc_round "$cols * $max_rows / $rows")" ++ rows="$max_rows" ++ fi ++ elif [ -z "$cols" ]; then ++ # If only one dimension is specified, compute the other one to match the ++ # aspect ratio as close as possible. ++ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" ++ elif [ -z "$rows" ]; then ++ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" ++ fi ++ ++ if [ "$cols" -lt 1 ]; then ++ cols=1 ++ fi ++ if [ "$rows" -lt 1 ]; then ++ rows=1 ++ fi ++fi ++ ++##################################################################### ++# Generate an image id ++##################################################################### ++ ++image_id="" ++while [ -z "$image_id" ]; do ++ image_id="$(shuf -i 16777217-4294967295 -n 1)" ++ # Check that the id requires 24-bit fg colors. ++ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then ++ image_id="" ++ fi ++done ++ ++##################################################################### ++# Uploading the image ++##################################################################### ++ ++# Choose the uploading method ++if [ "$uploading_method" = "auto" ]; then ++ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then ++ uploading_method="direct" ++ else ++ uploading_method="file" ++ fi ++fi ++ ++# Functions to emit the start and the end of a graphics command. ++if [ -n "$inside_tmux" ]; then ++ # If we are in tmux we have to wrap the command in Ptmux. ++ graphics_command_start='\ePtmux;\e\e_G' ++ graphics_command_end='\e\e\\\e\\' ++else ++ graphics_command_start='\e_G' ++ graphics_command_end='\e\\' ++fi ++ ++start_gr_command() { ++ printf "$graphics_command_start" >> "$tty" ++} ++end_gr_command() { ++ printf "$graphics_command_end" >> "$tty" ++} ++ ++# Send a graphics command with the correct start and end ++gr_command() { ++ start_gr_command ++ printf '%s' "$1" >> "$tty" ++ end_gr_command ++} ++ ++# Send an uploading command. Usage: gr_upload ++# Where is a part of command that specifies the action, it will be ++# repeated for every chunk (if the method is direct), and is the rest ++# of the command that specifies the image parameters. and ++# must not include the transmission method or ';'. ++# Example: ++# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++gr_upload() { ++ arg_action="$1" ++ arg_command="$2" ++ arg_file="$3" ++ if [ "$uploading_method" = "file" ]; then ++ # base64-encode the filename ++ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) ++ gr_command "${arg_action},${arg_command},t=f;${encoded_filename}" ++ fi ++ if [ "$uploading_method" = "direct" ]; then ++ # Create a temporary directory to store the chunked image. ++ chunkdir="$(mktemp -d)" ++ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then ++ echo "Can't create a temp dir" >&2 ++ exit 1 ++ fi ++ # base64-encode the file and split it into chunks. The size of each ++ # graphics command shouldn't be more than 4096, so we set the size of an ++ # encoded chunk to be 3968, slightly less than that. ++ chunk_size=3968 ++ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" ++ ++ # Issue a command indicating that we want to start data transmission for ++ # a new image. ++ gr_command "${arg_action},${arg_command},t=d,m=1" ++ ++ # Transmit chunks. ++ for chunk in "$chunkdir/chunk_"*; do ++ start_gr_command ++ printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty" ++ cat "$chunk" >> "$tty" ++ end_gr_command ++ rm "$chunk" ++ done ++ ++ # Tell the terminal that we are done. ++ gr_command "${arg_action},i=$image_id,m=0" ++ ++ # Remove the temporary directory. ++ rmdir "$chunkdir" ++ fi ++} ++ ++delayed_frame_dir_cleanup() { ++ arg_frame_dir="$1" ++ sleep 2 ++ if [ -n "$arg_frame_dir" ]; then ++ for frame in "$arg_frame_dir"/frame_*.png; do ++ rm "$frame" ++ done ++ rmdir "$arg_frame_dir" ++ fi ++} ++ ++upload_image_and_print_placeholder() { ++ # Check if the file is an animation. ++ frame_count=$($identify -format '%n\n' "$file" | head -n 1) ++ if [ "$frame_count" -gt 1 ]; then ++ # The file is an animation, decompose into frames and upload each frame. ++ frame_dir="$(mktemp -d)" ++ frame_dir="$HOME/temp/frames${frame_dir}" ++ mkdir -p "$frame_dir" ++ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then ++ echo "Can't create a temp dir for frames" >&2 ++ exit 1 ++ fi ++ ++ # Decompose the animation into separate frames. ++ $convert "$file" -coalesce "$frame_dir/frame_%06d.png" ++ ++ # Get all frame delays at once, in centiseconds, as a space-separated ++ # string. ++ delays=$($identify -format "%T " "$file") ++ ++ frame_number=1 ++ for frame in "$frame_dir"/frame_*.png; do ++ # Read the delay for the current frame and convert it from ++ # centiseconds to milliseconds. ++ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") ++ delay=$((delay * 10)) ++ # If the delay is 0, set it to 100ms. ++ if [ "$delay" -eq 0 ]; then ++ delay=100 ++ fi ++ ++ if [ -n "$speed" ]; then ++ delay=$(bc_round "$delay / $speed") ++ fi ++ ++ if [ "$frame_number" -eq 1 ]; then ++ # Upload the first frame with a=T ++ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" ++ # Set the delay for the first frame and also play the animation ++ # in loading mode (s=2). ++ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" ++ # Print the placeholder after the first frame to reduce the wait ++ # time. ++ print_placeholder ++ else ++ # Upload subsequent frames with a=f ++ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" ++ fi ++ ++ frame_number=$((frame_number + 1)) ++ done ++ ++ # Play the animation in loop mode (s=3). ++ gr_command "a=a,v=1,s=3,i=${image_id}" ++ ++ # Remove the temporary directory, but do it in the background with a ++ # delay to avoid removing files before they are loaded by the terminal. ++ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & ++ else ++ # The file is not an animation, upload it directly ++ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++ # Print the placeholder ++ print_placeholder ++ fi ++} ++ ++##################################################################### ++# Printing the image placeholder ++##################################################################### ++ ++print_placeholder() { ++ # Each line starts with the escape sequence to set the foreground color to ++ # the image id. ++ blue="$(expr "$image_id" % 256 )" ++ green="$(expr \( "$image_id" / 256 \) % 256 )" ++ red="$(expr \( "$image_id" / 65536 \) % 256 )" ++ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" ++ line_end="$(printf "\e[39;m")" ++ ++ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" ++ eval "id_diacritic=\$d${id4th}" ++ ++ # Reset the brush state, mostly to reset the underline color. ++ printf "\e[0m" ++ ++ # Fill the output with characters representing the image ++ for y in $(seq 0 "$(expr "$rows" - 1)"); do ++ eval "row_diacritic=\$d${y}" ++ printf '%s' "$line_start" ++ for x in $(seq 0 "$(expr "$cols" - 1)"); do ++ eval "col_diacritic=\$d${x}" ++ # Note that when $x is out of bounds, the column diacritic will ++ # be empty, meaning that the column should be guessed by the ++ # terminal. ++ if [ "$x" -ge "$num_diacritics" ]; then ++ printf '%s' "${placeholder}${row_diacritic}" ++ else ++ printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" ++ fi ++ done ++ printf '%s\n' "$line_end" ++ done ++ ++ printf "\e[0m" ++} ++ ++d0="̅" ++d1="̍" ++d2="̎" ++d3="̐" ++d4="̒" ++d5="̽" ++d6="̾" ++d7="̿" ++d8="͆" ++d9="͊" ++d10="͋" ++d11="͌" ++d12="͐" ++d13="͑" ++d14="͒" ++d15="͗" ++d16="͛" ++d17="ͣ" ++d18="ͤ" ++d19="ͥ" ++d20="ͦ" ++d21="ͧ" ++d22="ͨ" ++d23="ͩ" ++d24="ͪ" ++d25="ͫ" ++d26="ͬ" ++d27="ͭ" ++d28="ͮ" ++d29="ͯ" ++d30="҃" ++d31="҄" ++d32="҅" ++d33="҆" ++d34="҇" ++d35="֒" ++d36="֓" ++d37="֔" ++d38="֕" ++d39="֗" ++d40="֘" ++d41="֙" ++d42="֜" ++d43="֝" ++d44="֞" ++d45="֟" ++d46="֠" ++d47="֡" ++d48="֨" ++d49="֩" ++d50="֫" ++d51="֬" ++d52="֯" ++d53="ׄ" ++d54="ؐ" ++d55="ؑ" ++d56="ؒ" ++d57="ؓ" ++d58="ؔ" ++d59="ؕ" ++d60="ؖ" ++d61="ؗ" ++d62="ٗ" ++d63="٘" ++d64="ٙ" ++d65="ٚ" ++d66="ٛ" ++d67="ٝ" ++d68="ٞ" ++d69="ۖ" ++d70="ۗ" ++d71="ۘ" ++d72="ۙ" ++d73="ۚ" ++d74="ۛ" ++d75="ۜ" ++d76="۟" ++d77="۠" ++d78="ۡ" ++d79="ۢ" ++d80="ۤ" ++d81="ۧ" ++d82="ۨ" ++d83="۫" ++d84="۬" ++d85="ܰ" ++d86="ܲ" ++d87="ܳ" ++d88="ܵ" ++d89="ܶ" ++d90="ܺ" ++d91="ܽ" ++d92="ܿ" ++d93="݀" ++d94="݁" ++d95="݃" ++d96="݅" ++d97="݇" ++d98="݉" ++d99="݊" ++d100="߫" ++d101="߬" ++d102="߭" ++d103="߮" ++d104="߯" ++d105="߰" ++d106="߱" ++d107="߳" ++d108="ࠖ" ++d109="ࠗ" ++d110="࠘" ++d111="࠙" ++d112="ࠛ" ++d113="ࠜ" ++d114="ࠝ" ++d115="ࠞ" ++d116="ࠟ" ++d117="ࠠ" ++d118="ࠡ" ++d119="ࠢ" ++d120="ࠣ" ++d121="ࠥ" ++d122="ࠦ" ++d123="ࠧ" ++d124="ࠩ" ++d125="ࠪ" ++d126="ࠫ" ++d127="ࠬ" ++d128="࠭" ++d129="॑" ++d130="॓" ++d131="॔" ++d132="ྂ" ++d133="ྃ" ++d134="྆" ++d135="྇" ++d136="፝" ++d137="፞" ++d138="፟" ++d139="៝" ++d140="᤺" ++d141="ᨗ" ++d142="᩵" ++d143="᩶" ++d144="᩷" ++d145="᩸" ++d146="᩹" ++d147="᩺" ++d148="᩻" ++d149="᩼" ++d150="᭫" ++d151="᭭" ++d152="᭮" ++d153="᭯" ++d154="᭰" ++d155="᭱" ++d156="᭲" ++d157="᭳" ++d158="᳐" ++d159="᳑" ++d160="᳒" ++d161="᳚" ++d162="᳛" ++d163="᳠" ++d164="᷀" ++d165="᷁" ++d166="᷃" ++d167="᷄" ++d168="᷅" ++d169="᷆" ++d170="᷇" ++d171="᷈" ++d172="᷉" ++d173="᷋" ++d174="᷌" ++d175="᷑" ++d176="᷒" ++d177="ᷓ" ++d178="ᷔ" ++d179="ᷕ" ++d180="ᷖ" ++d181="ᷗ" ++d182="ᷘ" ++d183="ᷙ" ++d184="ᷚ" ++d185="ᷛ" ++d186="ᷜ" ++d187="ᷝ" ++d188="ᷞ" ++d189="ᷟ" ++d190="ᷠ" ++d191="ᷡ" ++d192="ᷢ" ++d193="ᷣ" ++d194="ᷤ" ++d195="ᷥ" ++d196="ᷦ" ++d197="᷾" ++d198="⃐" ++d199="⃑" ++d200="⃔" ++d201="⃕" ++d202="⃖" ++d203="⃗" ++d204="⃛" ++d205="⃜" ++d206="⃡" ++d207="⃧" ++d208="⃩" ++d209="⃰" ++d210="⳯" ++d211="⳰" ++d212="⳱" ++d213="ⷠ" ++d214="ⷡ" ++d215="ⷢ" ++d216="ⷣ" ++d217="ⷤ" ++d218="ⷥ" ++d219="ⷦ" ++d220="ⷧ" ++d221="ⷨ" ++d222="ⷩ" ++d223="ⷪ" ++d224="ⷫ" ++d225="ⷬ" ++d226="ⷭ" ++d227="ⷮ" ++d228="ⷯ" ++d229="ⷰ" ++d230="ⷱ" ++d231="ⷲ" ++d232="ⷳ" ++d233="ⷴ" ++d234="ⷵ" ++d235="ⷶ" ++d236="ⷷ" ++d237="ⷸ" ++d238="ⷹ" ++d239="ⷺ" ++d240="ⷻ" ++d241="ⷼ" ++d242="ⷽ" ++d243="ⷾ" ++d244="ⷿ" ++d245="꙯" ++d246="꙼" ++d247="꙽" ++d248="꛰" ++d249="꛱" ++d250="꣠" ++d251="꣡" ++d252="꣢" ++d253="꣣" ++d254="꣤" ++d255="꣥" ++d256="꣦" ++d257="꣧" ++d258="꣨" ++d259="꣩" ++d260="꣪" ++d261="꣫" ++d262="꣬" ++d263="꣭" ++d264="꣮" ++d265="꣯" ++d266="꣰" ++d267="꣱" ++d268="ꪰ" ++d269="ꪲ" ++d270="ꪳ" ++d271="ꪷ" ++d272="ꪸ" ++d273="ꪾ" ++d274="꪿" ++d275="꫁" ++d276="︠" ++d277="︡" ++d278="︢" ++d279="︣" ++d280="︤" ++d281="︥" ++d282="︦" ++d283="𐨏" ++d284="𐨸" ++d285="𝆅" ++d286="𝆆" ++d287="𝆇" ++d288="𝆈" ++d289="𝆉" ++d290="𝆪" ++d291="𝆫" ++d292="𝆬" ++d293="𝆭" ++d294="𝉂" ++d295="𝉃" ++d296="𝉄" ++ ++num_diacritics="297" ++ ++placeholder="􎻮" ++ ++##################################################################### ++# Upload the image and print the placeholder ++##################################################################### ++ ++upload_image_and_print_placeholder +diff --git a/khash.h b/khash.h +new file mode 100644 +index 0000000..f75f347 +--- /dev/null ++++ b/khash.h +@@ -0,0 +1,627 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, 2009, 2011 by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "khash.h" ++KHASH_MAP_INIT_INT(32, char) ++int main() { ++ int ret, is_missing; ++ khiter_t k; ++ khash_t(32) *h = kh_init(32); ++ k = kh_put(32, h, 5, &ret); ++ kh_value(h, k) = 10; ++ k = kh_get(32, h, 10); ++ is_missing = (k == kh_end(h)); ++ k = kh_get(32, h, 5); ++ kh_del(32, h, k); ++ for (k = kh_begin(h); k != kh_end(h); ++k) ++ if (kh_exist(h, k)) kh_value(h, k) = 1; ++ kh_destroy(32, h); ++ return 0; ++} ++*/ ++ ++/* ++ 2013-05-02 (0.2.8): ++ ++ * Use quadratic probing. When the capacity is power of 2, stepping function ++ i*(i+1)/2 guarantees to traverse each bucket. It is better than double ++ hashing on cache performance and is more robust than linear probing. ++ ++ In theory, double hashing should be more robust than quadratic probing. ++ However, my implementation is probably not for large hash tables, because ++ the second hash function is closely tied to the first hash function, ++ which reduce the effectiveness of double hashing. ++ ++ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php ++ ++ 2011-12-29 (0.2.7): ++ ++ * Minor code clean up; no actual effect. ++ ++ 2011-09-16 (0.2.6): ++ ++ * The capacity is a power of 2. This seems to dramatically improve the ++ speed for simple keys. Thank Zilong Tan for the suggestion. Reference: ++ ++ - http://code.google.com/p/ulib/ ++ - http://nothings.org/computer/judy/ ++ ++ * Allow to optionally use linear probing which usually has better ++ performance for random input. Double hashing is still the default as it ++ is more robust to certain non-random input. ++ ++ * Added Wang's integer hash function (not used by default). This hash ++ function is more robust to certain non-random input. ++ ++ 2011-02-14 (0.2.5): ++ ++ * Allow to declare global functions. ++ ++ 2009-09-26 (0.2.4): ++ ++ * Improve portability ++ ++ 2008-09-19 (0.2.3): ++ ++ * Corrected the example ++ * Improved interfaces ++ ++ 2008-09-11 (0.2.2): ++ ++ * Improved speed a little in kh_put() ++ ++ 2008-09-10 (0.2.1): ++ ++ * Added kh_clear() ++ * Fixed a compiling error ++ ++ 2008-09-02 (0.2.0): ++ ++ * Changed to token concatenation which increases flexibility. ++ ++ 2008-08-31 (0.1.2): ++ ++ * Fixed a bug in kh_get(), which has not been tested previously. ++ ++ 2008-08-31 (0.1.1): ++ ++ * Added destructor ++*/ ++ ++ ++#ifndef __AC_KHASH_H ++#define __AC_KHASH_H ++ ++/*! ++ @header ++ ++ Generic hash table library. ++ */ ++ ++#define AC_VERSION_KHASH_H "0.2.8" ++ ++#include ++#include ++#include ++ ++/* compiler specific configuration */ ++ ++#if UINT_MAX == 0xffffffffu ++typedef unsigned int khint32_t; ++#elif ULONG_MAX == 0xffffffffu ++typedef unsigned long khint32_t; ++#endif ++ ++#if ULONG_MAX == ULLONG_MAX ++typedef unsigned long khint64_t; ++#else ++typedef unsigned long long khint64_t; ++#endif ++ ++#ifndef kh_inline ++#ifdef _MSC_VER ++#define kh_inline __inline ++#else ++#define kh_inline inline ++#endif ++#endif /* kh_inline */ ++ ++#ifndef klib_unused ++#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) ++#define klib_unused __attribute__ ((__unused__)) ++#else ++#define klib_unused ++#endif ++#endif /* klib_unused */ ++ ++typedef khint32_t khint_t; ++typedef khint_t khiter_t; ++ ++#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) ++#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) ++#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) ++#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) ++#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) ++#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) ++#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) ++ ++#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) ++ ++#ifndef kroundup32 ++#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++#endif ++ ++#ifndef kcalloc ++#define kcalloc(N,Z) calloc(N,Z) ++#endif ++#ifndef kmalloc ++#define kmalloc(Z) malloc(Z) ++#endif ++#ifndef krealloc ++#define krealloc(P,Z) realloc(P,Z) ++#endif ++#ifndef kfree ++#define kfree(P) free(P) ++#endif ++ ++static const double __ac_HASH_UPPER = 0.77; ++ ++#define __KHASH_TYPE(name, khkey_t, khval_t) \ ++ typedef struct kh_##name##_s { \ ++ khint_t n_buckets, size, n_occupied, upper_bound; \ ++ khint32_t *flags; \ ++ khkey_t *keys; \ ++ khval_t *vals; \ ++ } kh_##name##_t; ++ ++#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ ++ extern kh_##name##_t *kh_init_##name(void); \ ++ extern void kh_destroy_##name(kh_##name##_t *h); \ ++ extern void kh_clear_##name(kh_##name##_t *h); \ ++ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ ++ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ ++ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ ++ extern void kh_del_##name(kh_##name##_t *h, khint_t x); ++ ++#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ SCOPE kh_##name##_t *kh_init_##name(void) { \ ++ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ ++ } \ ++ SCOPE void kh_destroy_##name(kh_##name##_t *h) \ ++ { \ ++ if (h) { \ ++ kfree((void *)h->keys); kfree(h->flags); \ ++ kfree((void *)h->vals); \ ++ kfree(h); \ ++ } \ ++ } \ ++ SCOPE void kh_clear_##name(kh_##name##_t *h) \ ++ { \ ++ if (h && h->flags) { \ ++ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ ++ h->size = h->n_occupied = 0; \ ++ } \ ++ } \ ++ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ ++ { \ ++ if (h->n_buckets) { \ ++ khint_t k, i, last, mask, step = 0; \ ++ mask = h->n_buckets - 1; \ ++ k = __hash_func(key); i = k & mask; \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) return h->n_buckets; \ ++ } \ ++ return __ac_iseither(h->flags, i)? h->n_buckets : i; \ ++ } else return 0; \ ++ } \ ++ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ ++ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ ++ khint32_t *new_flags = 0; \ ++ khint_t j = 1; \ ++ { \ ++ kroundup32(new_n_buckets); \ ++ if (new_n_buckets < 4) new_n_buckets = 4; \ ++ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ ++ else { /* hash table size to be changed (shrink or expand); rehash */ \ ++ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (!new_flags) return -1; \ ++ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (h->n_buckets < new_n_buckets) { /* expand */ \ ++ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (!new_keys) { kfree(new_flags); return -1; } \ ++ h->keys = new_keys; \ ++ if (kh_is_map) { \ ++ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ if (!new_vals) { kfree(new_flags); return -1; } \ ++ h->vals = new_vals; \ ++ } \ ++ } /* otherwise shrink */ \ ++ } \ ++ } \ ++ if (j) { /* rehashing is needed */ \ ++ for (j = 0; j != h->n_buckets; ++j) { \ ++ if (__ac_iseither(h->flags, j) == 0) { \ ++ khkey_t key = h->keys[j]; \ ++ khval_t val; \ ++ khint_t new_mask; \ ++ new_mask = new_n_buckets - 1; \ ++ if (kh_is_map) val = h->vals[j]; \ ++ __ac_set_isdel_true(h->flags, j); \ ++ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ ++ khint_t k, i, step = 0; \ ++ k = __hash_func(key); \ ++ i = k & new_mask; \ ++ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ ++ __ac_set_isempty_false(new_flags, i); \ ++ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ ++ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ ++ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ ++ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ ++ } else { /* write the element and jump out of the loop */ \ ++ h->keys[i] = key; \ ++ if (kh_is_map) h->vals[i] = val; \ ++ break; \ ++ } \ ++ } \ ++ } \ ++ } \ ++ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ ++ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ } \ ++ kfree(h->flags); /* free the working space */ \ ++ h->flags = new_flags; \ ++ h->n_buckets = new_n_buckets; \ ++ h->n_occupied = h->size; \ ++ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ ++ } \ ++ return 0; \ ++ } \ ++ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ ++ { \ ++ khint_t x; \ ++ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ ++ if (h->n_buckets > (h->size<<1)) { \ ++ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ ++ { \ ++ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ ++ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ ++ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ ++ else { \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ if (__ac_isdel(h->flags, i)) site = i; \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) { x = site; break; } \ ++ } \ ++ if (x == h->n_buckets) { \ ++ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ ++ else x = i; \ ++ } \ ++ } \ ++ } \ ++ if (__ac_isempty(h->flags, x)) { /* not present at all */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; ++h->n_occupied; \ ++ *ret = 1; \ ++ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; \ ++ *ret = 2; \ ++ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ ++ return x; \ ++ } \ ++ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ ++ { \ ++ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ ++ __ac_set_isdel_true(h->flags, x); \ ++ --h->size; \ ++ } \ ++ } ++ ++#define KHASH_DECLARE(name, khkey_t, khval_t) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_PROTOTYPES(name, khkey_t, khval_t) ++ ++#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++/* --- BEGIN OF HASH FUNCTIONS --- */ ++ ++/*! @function ++ @abstract Integer hash function ++ @param key The integer [khint32_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int_hash_func(key) (khint32_t)(key) ++/*! @function ++ @abstract Integer comparison function ++ */ ++#define kh_int_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract 64-bit integer hash function ++ @param key The integer [khint64_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) ++/*! @function ++ @abstract 64-bit integer comparison function ++ */ ++#define kh_int64_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract const char* hash function ++ @param s Pointer to a null terminated string ++ @return The hash value ++ */ ++static kh_inline khint_t __ac_X31_hash_string(const char *s) ++{ ++ khint_t h = (khint_t)*s; ++ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; ++ return h; ++} ++/*! @function ++ @abstract Another interface to const char* hash function ++ @param key Pointer to a null terminated string [const char*] ++ @return The hash value [khint_t] ++ */ ++#define kh_str_hash_func(key) __ac_X31_hash_string(key) ++/*! @function ++ @abstract Const char* comparison function ++ */ ++#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) ++ ++static kh_inline khint_t __ac_Wang_hash(khint_t key) ++{ ++ key += ~(key << 15); ++ key ^= (key >> 10); ++ key += (key << 3); ++ key ^= (key >> 6); ++ key += ~(key << 11); ++ key ^= (key >> 16); ++ return key; ++} ++#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) ++ ++/* --- END OF HASH FUNCTIONS --- */ ++ ++/* Other convenient macros... */ ++ ++/*! ++ @abstract Type of the hash table. ++ @param name Name of the hash table [symbol] ++ */ ++#define khash_t(name) kh_##name##_t ++ ++/*! @function ++ @abstract Initiate a hash table. ++ @param name Name of the hash table [symbol] ++ @return Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_init(name) kh_init_##name() ++ ++/*! @function ++ @abstract Destroy a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_destroy(name, h) kh_destroy_##name(h) ++ ++/*! @function ++ @abstract Reset a hash table without deallocating memory. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_clear(name, h) kh_clear_##name(h) ++ ++/*! @function ++ @abstract Resize a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param s New size [khint_t] ++ */ ++#define kh_resize(name, h, s) kh_resize_##name(h, s) ++ ++/*! @function ++ @abstract Insert a key to the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @param r Extra return code: -1 if the operation failed; ++ 0 if the key is present in the hash table; ++ 1 if the bucket is empty (never used); 2 if the element in ++ the bucket has been deleted [int*] ++ @return Iterator to the inserted element [khint_t] ++ */ ++#define kh_put(name, h, k, r) kh_put_##name(h, k, r) ++ ++/*! @function ++ @abstract Retrieve a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] ++ */ ++#define kh_get(name, h, k) kh_get_##name(h, k) ++ ++/*! @function ++ @abstract Remove a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Iterator to the element to be deleted [khint_t] ++ */ ++#define kh_del(name, h, k) kh_del_##name(h, k) ++ ++/*! @function ++ @abstract Test whether a bucket contains data. ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return 1 if containing data; 0 otherwise [int] ++ */ ++#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) ++ ++/*! @function ++ @abstract Get key given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Key [type of keys] ++ */ ++#define kh_key(h, x) ((h)->keys[x]) ++ ++/*! @function ++ @abstract Get value given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Value [type of values] ++ @discussion For hash sets, calling this results in segfault. ++ */ ++#define kh_val(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Alias of kh_val() ++ */ ++#define kh_value(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Get the start iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The start iterator [khint_t] ++ */ ++#define kh_begin(h) (khint_t)(0) ++ ++/*! @function ++ @abstract Get the end iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The end iterator [khint_t] ++ */ ++#define kh_end(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Get the number of elements in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of elements in the hash table [khint_t] ++ */ ++#define kh_size(h) ((h)->size) ++ ++/*! @function ++ @abstract Get the number of buckets in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of buckets in the hash table [khint_t] ++ */ ++#define kh_n_buckets(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Iterate over the entries in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param kvar Variable to which key will be assigned ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (kvar) = kh_key(h,__i); \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/*! @function ++ @abstract Iterate over the values in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach_value(h, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/* More convenient interfaces */ ++ ++/*! @function ++ @abstract Instantiate a hash set containing integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT(name) \ ++ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT(name, khval_t) \ ++ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash set containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT64(name) \ ++ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT64(name, khval_t) \ ++ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) ++ ++typedef const char *kh_cstr_t; ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_STR(name) \ ++ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_STR(name, khval_t) \ ++ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) ++ ++#endif /* __AC_KHASH_H */ +diff --git a/kvec.h b/kvec.h +new file mode 100644 +index 0000000..10f1c5b +--- /dev/null ++++ b/kvec.h +@@ -0,0 +1,90 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "kvec.h" ++int main() { ++ kvec_t(int) array; ++ kv_init(array); ++ kv_push(int, array, 10); // append ++ kv_a(int, array, 20) = 5; // dynamic ++ kv_A(array, 20) = 4; // static ++ kv_destroy(array); ++ return 0; ++} ++*/ ++ ++/* ++ 2008-09-22 (0.1.0): ++ ++ * The initial version. ++ ++*/ ++ ++#ifndef AC_KVEC_H ++#define AC_KVEC_H ++ ++#include ++ ++#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++ ++#define kvec_t(type) struct { size_t n, m; type *a; } ++#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) ++#define kv_destroy(v) free((v).a) ++#define kv_A(v, i) ((v).a[(i)]) ++#define kv_pop(v) ((v).a[--(v).n]) ++#define kv_size(v) ((v).n) ++#define kv_max(v) ((v).m) ++ ++#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) ++ ++#define kv_copy(type, v1, v0) do { \ ++ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ ++ (v1).n = (v0).n; \ ++ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ ++ } while (0) \ ++ ++#define kv_push(type, v, x) do { \ ++ if ((v).n == (v).m) { \ ++ (v).m = (v).m? (v).m<<1 : 2; \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ ++ } \ ++ (v).a[(v).n++] = (x); \ ++ } while (0) ++ ++#define kv_pushp(type, v) ((((v).n == (v).m)? \ ++ ((v).m = ((v).m? (v).m<<1 : 2), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : 0), ((v).a + ((v).n++))) ++ ++#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ ++ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ ++ : 0), (v).a[(i)]) ++ ++#endif +diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c +new file mode 100644 +index 0000000..829c0fc +--- /dev/null ++++ b/rowcolumn_diacritics_helpers.c +@@ -0,0 +1,391 @@ ++#include ++ ++uint16_t diacritic_to_num(uint32_t code) ++{ ++ switch (code) { ++ case 0x305: ++ return code - 0x305 + 1; ++ case 0x30d: ++ case 0x30e: ++ return code - 0x30d + 2; ++ case 0x310: ++ return code - 0x310 + 4; ++ case 0x312: ++ return code - 0x312 + 5; ++ case 0x33d: ++ case 0x33e: ++ case 0x33f: ++ return code - 0x33d + 6; ++ case 0x346: ++ return code - 0x346 + 9; ++ case 0x34a: ++ case 0x34b: ++ case 0x34c: ++ return code - 0x34a + 10; ++ case 0x350: ++ case 0x351: ++ case 0x352: ++ return code - 0x350 + 13; ++ case 0x357: ++ return code - 0x357 + 16; ++ case 0x35b: ++ return code - 0x35b + 17; ++ case 0x363: ++ case 0x364: ++ case 0x365: ++ case 0x366: ++ case 0x367: ++ case 0x368: ++ case 0x369: ++ case 0x36a: ++ case 0x36b: ++ case 0x36c: ++ case 0x36d: ++ case 0x36e: ++ case 0x36f: ++ return code - 0x363 + 18; ++ case 0x483: ++ case 0x484: ++ case 0x485: ++ case 0x486: ++ case 0x487: ++ return code - 0x483 + 31; ++ case 0x592: ++ case 0x593: ++ case 0x594: ++ case 0x595: ++ return code - 0x592 + 36; ++ case 0x597: ++ case 0x598: ++ case 0x599: ++ return code - 0x597 + 40; ++ case 0x59c: ++ case 0x59d: ++ case 0x59e: ++ case 0x59f: ++ case 0x5a0: ++ case 0x5a1: ++ return code - 0x59c + 43; ++ case 0x5a8: ++ case 0x5a9: ++ return code - 0x5a8 + 49; ++ case 0x5ab: ++ case 0x5ac: ++ return code - 0x5ab + 51; ++ case 0x5af: ++ return code - 0x5af + 53; ++ case 0x5c4: ++ return code - 0x5c4 + 54; ++ case 0x610: ++ case 0x611: ++ case 0x612: ++ case 0x613: ++ case 0x614: ++ case 0x615: ++ case 0x616: ++ case 0x617: ++ return code - 0x610 + 55; ++ case 0x657: ++ case 0x658: ++ case 0x659: ++ case 0x65a: ++ case 0x65b: ++ return code - 0x657 + 63; ++ case 0x65d: ++ case 0x65e: ++ return code - 0x65d + 68; ++ case 0x6d6: ++ case 0x6d7: ++ case 0x6d8: ++ case 0x6d9: ++ case 0x6da: ++ case 0x6db: ++ case 0x6dc: ++ return code - 0x6d6 + 70; ++ case 0x6df: ++ case 0x6e0: ++ case 0x6e1: ++ case 0x6e2: ++ return code - 0x6df + 77; ++ case 0x6e4: ++ return code - 0x6e4 + 81; ++ case 0x6e7: ++ case 0x6e8: ++ return code - 0x6e7 + 82; ++ case 0x6eb: ++ case 0x6ec: ++ return code - 0x6eb + 84; ++ case 0x730: ++ return code - 0x730 + 86; ++ case 0x732: ++ case 0x733: ++ return code - 0x732 + 87; ++ case 0x735: ++ case 0x736: ++ return code - 0x735 + 89; ++ case 0x73a: ++ return code - 0x73a + 91; ++ case 0x73d: ++ return code - 0x73d + 92; ++ case 0x73f: ++ case 0x740: ++ case 0x741: ++ return code - 0x73f + 93; ++ case 0x743: ++ return code - 0x743 + 96; ++ case 0x745: ++ return code - 0x745 + 97; ++ case 0x747: ++ return code - 0x747 + 98; ++ case 0x749: ++ case 0x74a: ++ return code - 0x749 + 99; ++ case 0x7eb: ++ case 0x7ec: ++ case 0x7ed: ++ case 0x7ee: ++ case 0x7ef: ++ case 0x7f0: ++ case 0x7f1: ++ return code - 0x7eb + 101; ++ case 0x7f3: ++ return code - 0x7f3 + 108; ++ case 0x816: ++ case 0x817: ++ case 0x818: ++ case 0x819: ++ return code - 0x816 + 109; ++ case 0x81b: ++ case 0x81c: ++ case 0x81d: ++ case 0x81e: ++ case 0x81f: ++ case 0x820: ++ case 0x821: ++ case 0x822: ++ case 0x823: ++ return code - 0x81b + 113; ++ case 0x825: ++ case 0x826: ++ case 0x827: ++ return code - 0x825 + 122; ++ case 0x829: ++ case 0x82a: ++ case 0x82b: ++ case 0x82c: ++ case 0x82d: ++ return code - 0x829 + 125; ++ case 0x951: ++ return code - 0x951 + 130; ++ case 0x953: ++ case 0x954: ++ return code - 0x953 + 131; ++ case 0xf82: ++ case 0xf83: ++ return code - 0xf82 + 133; ++ case 0xf86: ++ case 0xf87: ++ return code - 0xf86 + 135; ++ case 0x135d: ++ case 0x135e: ++ case 0x135f: ++ return code - 0x135d + 137; ++ case 0x17dd: ++ return code - 0x17dd + 140; ++ case 0x193a: ++ return code - 0x193a + 141; ++ case 0x1a17: ++ return code - 0x1a17 + 142; ++ case 0x1a75: ++ case 0x1a76: ++ case 0x1a77: ++ case 0x1a78: ++ case 0x1a79: ++ case 0x1a7a: ++ case 0x1a7b: ++ case 0x1a7c: ++ return code - 0x1a75 + 143; ++ case 0x1b6b: ++ return code - 0x1b6b + 151; ++ case 0x1b6d: ++ case 0x1b6e: ++ case 0x1b6f: ++ case 0x1b70: ++ case 0x1b71: ++ case 0x1b72: ++ case 0x1b73: ++ return code - 0x1b6d + 152; ++ case 0x1cd0: ++ case 0x1cd1: ++ case 0x1cd2: ++ return code - 0x1cd0 + 159; ++ case 0x1cda: ++ case 0x1cdb: ++ return code - 0x1cda + 162; ++ case 0x1ce0: ++ return code - 0x1ce0 + 164; ++ case 0x1dc0: ++ case 0x1dc1: ++ return code - 0x1dc0 + 165; ++ case 0x1dc3: ++ case 0x1dc4: ++ case 0x1dc5: ++ case 0x1dc6: ++ case 0x1dc7: ++ case 0x1dc8: ++ case 0x1dc9: ++ return code - 0x1dc3 + 167; ++ case 0x1dcb: ++ case 0x1dcc: ++ return code - 0x1dcb + 174; ++ case 0x1dd1: ++ case 0x1dd2: ++ case 0x1dd3: ++ case 0x1dd4: ++ case 0x1dd5: ++ case 0x1dd6: ++ case 0x1dd7: ++ case 0x1dd8: ++ case 0x1dd9: ++ case 0x1dda: ++ case 0x1ddb: ++ case 0x1ddc: ++ case 0x1ddd: ++ case 0x1dde: ++ case 0x1ddf: ++ case 0x1de0: ++ case 0x1de1: ++ case 0x1de2: ++ case 0x1de3: ++ case 0x1de4: ++ case 0x1de5: ++ case 0x1de6: ++ return code - 0x1dd1 + 176; ++ case 0x1dfe: ++ return code - 0x1dfe + 198; ++ case 0x20d0: ++ case 0x20d1: ++ return code - 0x20d0 + 199; ++ case 0x20d4: ++ case 0x20d5: ++ case 0x20d6: ++ case 0x20d7: ++ return code - 0x20d4 + 201; ++ case 0x20db: ++ case 0x20dc: ++ return code - 0x20db + 205; ++ case 0x20e1: ++ return code - 0x20e1 + 207; ++ case 0x20e7: ++ return code - 0x20e7 + 208; ++ case 0x20e9: ++ return code - 0x20e9 + 209; ++ case 0x20f0: ++ return code - 0x20f0 + 210; ++ case 0x2cef: ++ case 0x2cf0: ++ case 0x2cf1: ++ return code - 0x2cef + 211; ++ case 0x2de0: ++ case 0x2de1: ++ case 0x2de2: ++ case 0x2de3: ++ case 0x2de4: ++ case 0x2de5: ++ case 0x2de6: ++ case 0x2de7: ++ case 0x2de8: ++ case 0x2de9: ++ case 0x2dea: ++ case 0x2deb: ++ case 0x2dec: ++ case 0x2ded: ++ case 0x2dee: ++ case 0x2def: ++ case 0x2df0: ++ case 0x2df1: ++ case 0x2df2: ++ case 0x2df3: ++ case 0x2df4: ++ case 0x2df5: ++ case 0x2df6: ++ case 0x2df7: ++ case 0x2df8: ++ case 0x2df9: ++ case 0x2dfa: ++ case 0x2dfb: ++ case 0x2dfc: ++ case 0x2dfd: ++ case 0x2dfe: ++ case 0x2dff: ++ return code - 0x2de0 + 214; ++ case 0xa66f: ++ return code - 0xa66f + 246; ++ case 0xa67c: ++ case 0xa67d: ++ return code - 0xa67c + 247; ++ case 0xa6f0: ++ case 0xa6f1: ++ return code - 0xa6f0 + 249; ++ case 0xa8e0: ++ case 0xa8e1: ++ case 0xa8e2: ++ case 0xa8e3: ++ case 0xa8e4: ++ case 0xa8e5: ++ case 0xa8e6: ++ case 0xa8e7: ++ case 0xa8e8: ++ case 0xa8e9: ++ case 0xa8ea: ++ case 0xa8eb: ++ case 0xa8ec: ++ case 0xa8ed: ++ case 0xa8ee: ++ case 0xa8ef: ++ case 0xa8f0: ++ case 0xa8f1: ++ return code - 0xa8e0 + 251; ++ case 0xaab0: ++ return code - 0xaab0 + 269; ++ case 0xaab2: ++ case 0xaab3: ++ return code - 0xaab2 + 270; ++ case 0xaab7: ++ case 0xaab8: ++ return code - 0xaab7 + 272; ++ case 0xaabe: ++ case 0xaabf: ++ return code - 0xaabe + 274; ++ case 0xaac1: ++ return code - 0xaac1 + 276; ++ case 0xfe20: ++ case 0xfe21: ++ case 0xfe22: ++ case 0xfe23: ++ case 0xfe24: ++ case 0xfe25: ++ case 0xfe26: ++ return code - 0xfe20 + 277; ++ case 0x10a0f: ++ return code - 0x10a0f + 284; ++ case 0x10a38: ++ return code - 0x10a38 + 285; ++ case 0x1d185: ++ case 0x1d186: ++ case 0x1d187: ++ case 0x1d188: ++ case 0x1d189: ++ return code - 0x1d185 + 286; ++ case 0x1d1aa: ++ case 0x1d1ab: ++ case 0x1d1ac: ++ case 0x1d1ad: ++ return code - 0x1d1aa + 291; ++ case 0x1d242: ++ case 0x1d243: ++ case 0x1d244: ++ return code - 0x1d242 + 295; ++ } ++ return 0; ++} +diff --git a/st.c b/st.c +index 57c6e96..f1c5299 100644 +--- a/st.c ++++ b/st.c +@@ -19,6 +19,7 @@ + + #include "st.h" + #include "win.h" ++#include "graphics.h" + + #if defined(__linux) + #include +@@ -36,6 +37,10 @@ + #define STR_BUF_SIZ ESC_BUF_SIZ + #define STR_ARG_SIZ ESC_ARG_SIZ + ++/* PUA character used as an image placeholder */ ++#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE ++#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE ++ + /* macros */ + #define IS_SET(flag) ((term.mode & (flag)) != 0) + #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +@@ -113,6 +118,8 @@ typedef struct { + typedef struct { + int row; /* nb row */ + int col; /* nb col */ ++ int pixw; /* width of the text area in pixels */ ++ int pixh; /* height of the text area in pixels */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ +@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); + static char utf8encodebyte(Rune, size_t); + static size_t utf8validate(Rune *, size_t); + +-static char *base64dec(const char *); + static char base64dec_getc(const char **); + + static ssize_t xwrite(int, const char *, size_t); +@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; + static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; + static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + ++/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 ++ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ ++uint16_t diacritic_to_num(uint32_t code); ++ + ssize_t + xwrite(int fd, const char *s, size_t len) + { +@@ -616,6 +626,12 @@ getsel(void) + if (gp->mode & ATTR_WDUMMY) + continue; + ++ if (gp->mode & ATTR_IMAGE) { ++ // TODO: Copy diacritics as well ++ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); ++ continue; ++ } ++ + ptr += utf8encode(gp->u, ptr); + } + +@@ -819,7 +835,11 @@ ttyread(void) + { + static char buf[BUFSIZ]; + static int buflen = 0; +- int ret, written; ++ static int already_processing = 0; ++ int ret, written = 0; ++ ++ if (buflen >= LEN(buf)) ++ return 0; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); +@@ -831,7 +851,24 @@ ttyread(void) + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; +- written = twrite(buf, buflen, 0); ++ if (already_processing) { ++ /* Avoid recursive call to twrite() */ ++ return ret; ++ } ++ already_processing = 1; ++ while (1) { ++ int buflen_before_processing = buflen; ++ written += twrite(buf + written, buflen - written, 0); ++ // If buflen changed during the call to twrite, there is ++ // new data, and we need to keep processing, otherwise ++ // we can exit. This will not loop forever because the ++ // buffer is limited, and we don't clean it in this ++ // loop, so at some point ttywrite will have to drop ++ // some data. ++ if (buflen_before_processing == buflen) ++ break; ++ } ++ already_processing = 0; + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) +@@ -874,6 +911,7 @@ ttywriteraw(const char *s, size_t n) + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; ++ int retries_left = 100; + + /* + * Remember that we are using a pty, which might be a modem line. +@@ -882,6 +920,9 @@ ttywriteraw(const char *s, size_t n) + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { ++ if (retries_left-- <= 0) ++ goto too_many_retries; ++ + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); +@@ -923,11 +964,16 @@ ttywriteraw(const char *s, size_t n) + + write_error: + die("write error on tty: %s\n", strerror(errno)); ++too_many_retries: ++ fprintf(stderr, "Could not write %zu bytes to tty\n", n); + } + + void + ttyresize(int tw, int th) + { ++ term.pixw = tw; ++ term.pixh = th; ++ + struct winsize w; + + w.ws_row = term.row; +@@ -1015,7 +1061,8 @@ treset(void) + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, +- .bg = defaultbg ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); +@@ -1038,7 +1085,9 @@ treset(void) + void + tnew(int col, int row) + { +- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; ++ term = (Term){.c = {.attr = {.fg = defaultfg, ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR}}}; + tresize(col, row); + treset(); + } +@@ -1215,9 +1264,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + ++ if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE && ++ tgetisclassicplaceholder(&term.line[y][x])) { ++ // This is a workaround: don't overwrite classic placement ++ // placeholders with space symbols (unlike Unicode placeholders ++ // which must be overwritten by anything). ++ term.line[y][x].bg = attr->bg; ++ term.dirty[y] = 1; ++ return; ++ } ++ + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; ++ ++ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) { ++ term.line[y][x].u = 0; ++ term.line[y][x].mode |= ATTR_IMAGE; ++ } + } + + void +@@ -1244,12 +1308,104 @@ tclearregion(int x1, int y1, int x2, int y2) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; ++ gp->decor = term.c.attr.decor; + gp->mode = 0; + gp->u = ' '; + } + } + } + ++/// Fills a rectangle area with an image placeholder. The starting point is the ++/// cursor. Adds empty lines if needed. The placeholder will be marked as ++/// classic. ++void ++tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id, ++ int cols, int rows, char do_not_move_cursor) ++{ ++ for (int row = 0; row < rows; ++row) { ++ int y = term.c.y; ++ term.dirty[y] = 1; ++ for (int col = 0; col < cols; ++col) { ++ int x = term.c.x + col; ++ if (x >= term.col) ++ break; ++ Glyph *gp = &term.line[y][x]; ++ if (selected(x, y)) ++ selclear(); ++ gp->mode = ATTR_IMAGE; ++ gp->u = 0; ++ tsetimgrow(gp, row + 1); ++ tsetimgcol(gp, col + 1); ++ tsetimgid(gp, image_id); ++ tsetimgplacementid(gp, placement_id); ++ tsetimgdiacriticcount(gp, 3); ++ tsetisclassicplaceholder(gp, 1); ++ } ++ // If moving the cursor is not allowed and this is the last line ++ // of the terminal, we are done. ++ if (do_not_move_cursor && y == term.row - 1) ++ break; ++ // Move the cursor down, maybe creating a new line. The x is ++ // preserved (we never change term.c.x in the loop above). ++ if (row != rows - 1) ++ tnewline(/*first_col=*/0); ++ } ++ if (do_not_move_cursor) { ++ // Return the cursor to the original position. ++ tmoveto(term.c.x, term.c.y - rows + 1); ++ } else { ++ // Move the cursor beyond the last column, as required by the ++ // protocol. If the cursor goes beyond the screen edge, insert a ++ // newline to match the behavior of kitty. ++ if (term.c.x + cols >= term.col) ++ tnewline(/*first_col=*/1); ++ else ++ tmoveto(term.c.x + cols, term.c.y); ++ } ++} ++ ++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic), ++ void *data) { ++ for (int row = 0; row < term.row; ++row) { ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(gp); ++ uint32_t placement_id = tgetimgplacementid(gp); ++ int ret = ++ callback(data, tgetimgid(gp), ++ tgetimgplacementid(gp), ++ tgetimgcol(gp), tgetimgrow(gp), ++ tgetisclassicplaceholder(gp)); ++ if (ret == 1) { ++ term.dirty[row] = 1; ++ gp->mode = 0; ++ gp->u = ' '; ++ } ++ } ++ } ++ } ++} ++ ++void gr_schedule_image_redraw_by_id(uint32_t image_id) { ++ for (int row = 0; row < term.row; ++row) { ++ if (term.dirty[row]) ++ continue; ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ uint32_t cell_image_id = tgetimgid(gp); ++ if (cell_image_id == image_id) { ++ term.dirty[row] = 1; ++ break; ++ } ++ } ++ } ++ } ++} ++ + void + tdeletechar(int n) + { +@@ -1368,6 +1524,7 @@ tsetattr(const int *attr, int l) + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; ++ term.c.attr.decor = DECOR_DEFAULT_COLOR; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; +@@ -1380,6 +1537,20 @@ tsetattr(const int *attr, int l) + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; ++ if (i + 1 < l) { ++ idx = attr[++i]; ++ if (BETWEEN(idx, 1, 5)) { ++ tsetdecorstyle(&term.c.attr, idx); ++ } else if (idx == 0) { ++ term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); ++ } else { ++ fprintf(stderr, ++ "erresc: unknown underline " ++ "style %d\n", ++ idx); ++ } ++ } + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ +@@ -1403,6 +1574,7 @@ tsetattr(const int *attr, int l) + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; +@@ -1430,6 +1602,13 @@ tsetattr(const int *attr, int l) + case 49: + term.c.attr.bg = defaultbg; + break; ++ case 58: ++ if ((idx = tdefcolor(attr, &i, l)) >= 0) ++ tsetdecorcolor(&term.c.attr, idx); ++ break; ++ case 59: ++ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR); ++ break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; +@@ -1813,6 +1992,39 @@ csihandle(void) + goto unknown; + } + break; ++ case '>': ++ switch (csiescseq.mode[1]) { ++ case 'q': /* XTVERSION -- Print terminal name and version */ ++ len = snprintf(buf, sizeof(buf), ++ "\033P>|st-graphics(%s)\033\\", VERSION); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; ++ case 't': /* XTWINOPS -- Window manipulation */ ++ switch (csiescseq.arg[0]) { ++ case 14: /* Report text area size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[4;%i;%it", ++ term.pixh, term.pixw); ++ ttywrite(buf, len, 0); ++ break; ++ case 16: /* Report character cell size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[6;%i;%it", ++ term.pixh / term.row, ++ term.pixw / term.col); ++ ttywrite(buf, len, 0); ++ break; ++ case 18: /* Report the size of the text area in characters. */ ++ len = snprintf(buf, sizeof(buf), "\033[8;%i;%it", ++ term.row, term.col); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; + } + } + +@@ -1962,8 +2174,26 @@ strhandle(void) + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; +- case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ ++ if (gr_parse_command(strescseq.buf, strescseq.len)) { ++ GraphicsCommandResult *res = &graphics_command_result; ++ if (res->create_placeholder) { ++ tcreateimgplaceholder( ++ res->placeholder.image_id, ++ res->placeholder.placement_id, ++ res->placeholder.columns, ++ res->placeholder.rows, ++ res->placeholder.do_not_move_cursor); ++ } ++ if (res->response[0]) ++ ttywrite(res->response, strlen(res->response), ++ 0); ++ if (res->redraw) ++ tfulldirt(); ++ return; ++ } ++ return; ++ case 'P': /* DCS -- Device Control String */ + case '^': /* PM -- Privacy Message */ + return; + } +@@ -2469,6 +2699,33 @@ check_control_code: + if (selected(term.c.x, term.c.y)) + selclear(); + ++ if (width == 0) { ++ // It's probably a combining char. Combining characters are not ++ // supported, so we just ignore them, unless it denotes the row and ++ // column of an image character. ++ if (term.c.y <= 0 && term.c.x <= 0) ++ return; ++ else if (term.c.x == 0) ++ gp = &term.line[term.c.y-1][term.col-1]; ++ else if (term.c.state & CURSOR_WRAPNEXT) ++ gp = &term.line[term.c.y][term.c.x]; ++ else ++ gp = &term.line[term.c.y][term.c.x-1]; ++ uint16_t num = diacritic_to_num(u); ++ if (num && (gp->mode & ATTR_IMAGE)) { ++ unsigned diaccount = tgetimgdiacriticcount(gp); ++ if (diaccount == 0) ++ tsetimgrow(gp, num); ++ else if (diaccount == 1) ++ tsetimgcol(gp, num); ++ else if (diaccount == 2) ++ tsetimg4thbyteplus1(gp, num); ++ tsetimgdiacriticcount(gp, diaccount + 1); ++ } ++ term.lastc = u; ++ return; ++ } ++ + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; +@@ -2635,6 +2892,8 @@ drawregion(int x1, int y1, int x2, int y2) + { + int y; + ++ xstartimagedraw(term.dirty, term.row); ++ + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; +@@ -2642,6 +2901,8 @@ drawregion(int x1, int y1, int x2, int y2) + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } ++ ++ xfinishimagedraw(); + } + + void +@@ -2676,3 +2937,9 @@ redraw(void) + tfulldirt(); + draw(); + } ++ ++Glyph ++getglyphat(int col, int row) ++{ ++ return term.line[row][col]; ++} +diff --git a/st.h b/st.h +index fd3b0d8..c5dd731 100644 +--- a/st.h ++++ b/st.h +@@ -12,7 +12,7 @@ + #define DEFAULT(a, b) (a) = (a) ? (a) : (b) + #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) + #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ +- (a).bg != (b).bg) ++ (a).bg != (b).bg || (a).decor != (b).decor) + #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) + #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) +@@ -20,6 +20,10 @@ + #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) + #define IS_TRUECOL(x) (1 << 24 & (x)) + ++// This decor color indicates that the fg color should be used. Note that it's ++// not a 24-bit color because the 25-th bit is not set. ++#define DECOR_DEFAULT_COLOR 0x0ffffff ++ + enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, +@@ -34,6 +38,7 @@ enum glyph_attribute { + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, ++ ATTR_IMAGE = 1 << 14, + }; + + enum selection_mode { +@@ -52,6 +57,14 @@ enum selection_snap { + SNAP_LINE = 2 + }; + ++enum underline_style { ++ UNDERLINE_STRAIGHT = 1, ++ UNDERLINE_DOUBLE = 2, ++ UNDERLINE_CURLY = 3, ++ UNDERLINE_DOTTED = 4, ++ UNDERLINE_DASHED = 5, ++}; ++ + typedef unsigned char uchar; + typedef unsigned int uint; + typedef unsigned long ulong; +@@ -65,6 +78,7 @@ typedef struct { + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ ++ uint32_t decor; /* decoration (like underline) */ + } Glyph; + + typedef Glyph *Line; +@@ -105,6 +119,8 @@ void selextend(int, int, int, int); + int selected(int, int); + char *getsel(void); + ++Glyph getglyphat(int, int); ++ + size_t utf8encode(Rune, char *); + + void *xmalloc(size_t); +@@ -124,3 +140,69 @@ extern unsigned int tabspaces; + extern unsigned int defaultfg; + extern unsigned int defaultbg; + extern unsigned int defaultcs; ++ ++// Accessors to decoration properties stored in `decor`. ++// The 25-th bit is used to indicate if it's a 24-bit color. ++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; } ++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; } ++static inline void tsetdecorcolor(Glyph *g, uint32_t color) { ++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff); ++} ++static inline void tsetdecorstyle(Glyph *g, uint32_t style) { ++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25); ++} ++ ++ ++// Some accessors to image placeholder properties stored in `u`: ++// - row (1-base) - 9 bits ++// - column (1-base) - 9 bits ++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified, ++// don't forget to subtract 1). ++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits ++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit ++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; } ++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; } ++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; } ++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; } ++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; } ++static inline void tsetimgrow(Glyph *g, uint32_t row) { ++ g->u = (g->u & ~0x1ff) | (row & 0x1ff); ++} ++static inline void tsetimgcol(Glyph *g, uint32_t col) { ++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9); ++} ++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) { ++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18); ++} ++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) { ++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27); ++} ++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) { ++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29); ++} ++ ++/// Returns the full image id. This is a naive implementation, if the most ++/// significant byte is not specified, it's assumed to be 0 instead of inferring ++/// it from the cells to the left. ++static inline uint32_t tgetimgid(Glyph *g) { ++ uint32_t msb = tgetimgid4thbyteplus1(g); ++ if (msb != 0) ++ --msb; ++ return (msb << 24) | (g->fg & 0xFFFFFF); ++} ++ ++/// Sets the full image id. ++static inline void tsetimgid(Glyph *g, uint32_t id) { ++ g->fg = (id & 0xFFFFFF) | (1 << 24); ++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1); ++} ++ ++static inline uint32_t tgetimgplacementid(Glyph *g) { ++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR) ++ return 0; ++ return g->decor & 0xFFFFFF; ++} ++ ++static inline void tsetimgplacementid(Glyph *g, uint32_t id) { ++ g->decor = (id & 0xFFFFFF) | (1 << 24); ++} +diff --git a/st.info b/st.info +index efab2cf..ded76c1 100644 +--- a/st.info ++++ b/st.info +@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, ++ Smulx=\E[4:%p1%dm, + + st| simpleterm, + use=st-mono, +@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, ++# Underline colors ++ Su, ++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m, ++ Setulc1=\E[58:5:%p1%dm, ++ ol=\E[59m, + + st-meta| simpleterm with meta key, + use=st, +diff --git a/win.h b/win.h +index 6de960d..31b3fff 100644 +--- a/win.h ++++ b/win.h +@@ -39,3 +39,6 @@ void xsetpointermotion(int); + void xsetsel(char *); + int xstartdraw(void); + void xximspot(int, int); ++ ++void xstartimagedraw(int *dirty, int rows); ++void xfinishimagedraw(); +diff --git a/x.c b/x.c +index d73152b..6f1bf8c 100644 +--- a/x.c ++++ b/x.c +@@ -4,6 +4,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -19,6 +21,7 @@ char *argv0; + #include "arg.h" + #include "st.h" + #include "win.h" ++#include "graphics.h" + + /* types used in config.h */ + typedef struct { +@@ -59,6 +62,12 @@ static void zoom(const Arg *); + static void zoomabs(const Arg *); + static void zoomreset(const Arg *); + static void ttysend(const Arg *); ++static void previewimage(const Arg *); ++static void showimageinfo(const Arg *); ++static void togglegrdebug(const Arg *); ++static void dumpgrstate(const Arg *); ++static void unloadimages(const Arg *); ++static void toggleimages(const Arg *); + + /* config.h for applying patches and the configuration. */ + #include "config.h" +@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec; + typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ ++ int hborderpx, vborderpx; + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ +@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int); + static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); + static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); + static void xdrawglyph(Glyph, int, int); ++static void xdrawimages(Glyph, Line, int x1, int y1, int x2); ++static void xdrawoneimagecell(Glyph, int x, int y); + static void xclear(int, int, int, int); + static int xgeommasktogravity(int); + static int ximopen(Display *); +@@ -220,6 +232,7 @@ static DC dc; + static XWindow xw; + static XSelection xsel; + static TermWindow win; ++static unsigned int mouse_col = 0, mouse_row = 0; + + /* Font Ring Cache */ + enum { +@@ -328,10 +341,72 @@ ttysend(const Arg *arg) + ttywrite(arg->s, strlen(arg->s), 1); + } + ++void ++previewimage(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ gr_preview_image(image_id, arg->s); ++ } ++} ++ ++void ++showimageinfo(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ char stcommand[256] = {0}; ++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0); ++ if (len > sizeof(stcommand) - 1) { ++ fprintf(stderr, "Executable name too long: %s\n", ++ argv0); ++ return; ++ } ++ gr_show_image_info(image_id, tgetimgplacementid(&g), ++ tgetimgcol(&g), tgetimgrow(&g), ++ tgetisclassicplaceholder(&g), ++ tgetimgdiacriticcount(&g), argv0); ++ } ++} ++ ++void ++togglegrdebug(const Arg *arg) ++{ ++ graphics_debug_mode = (graphics_debug_mode + 1) % 3; ++ redraw(); ++} ++ ++void ++dumpgrstate(const Arg *arg) ++{ ++ gr_dump_state(); ++} ++ ++void ++unloadimages(const Arg *arg) ++{ ++ gr_unload_images_to_reduce_ram(); ++} ++ ++void ++toggleimages(const Arg *arg) ++{ ++ graphics_display_images = !graphics_display_images; ++ redraw(); ++} ++ + int + evcol(XEvent *e) + { +- int x = e->xbutton.x - borderpx; ++ int x = e->xbutton.x - win.hborderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; + } +@@ -339,7 +414,7 @@ evcol(XEvent *e) + int + evrow(XEvent *e) + { +- int y = e->xbutton.y - borderpx; ++ int y = e->xbutton.y - win.vborderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; + } +@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release) + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + ++ mouse_col = evcol(e); ++ mouse_row = evrow(e); ++ + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && +@@ -739,6 +817,9 @@ cresize(int width, int height) + col = MAX(1, col); + row = MAX(1, row); + ++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100; ++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100; ++ + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +@@ -869,8 +950,8 @@ xhints(void) + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; +- sizeh->height_inc = win.ch; +- sizeh->width_inc = win.cw; ++ sizeh->height_inc = 1; ++ sizeh->width_inc = 1; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; +@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } +- defaultfontsize = usedfontsize; ++ if (defaultfontsize <= 0) ++ defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) +@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; +- if (fontsize == 0) ++ if (defaultfontsize <= 0 && fontsize == 0) + defaultfontsize = fontval; + } + +@@ -1152,8 +1234,8 @@ xinit(int cols, int rows) + xloadcols(); + + /* adjust fixed window geometry */ +- win.w = 2 * borderpx + cols * win.cw; +- win.h = 2 * borderpx + rows * win.ch; ++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw; ++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) +@@ -1240,12 +1322,15 @@ xinit(int cols, int rows) + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; ++ ++ // Initialize the graphics (image display) module. ++ gr_init(xw.dpy, xw.vis, xw.cmap); + } + + int + xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) + { +- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; ++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; +@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + if (mode == ATTR_WDUMMY) + continue; + ++ /* Draw spaces for image placeholders (images will be drawn ++ * separately). */ ++ if (mode & ATTR_IMAGE) ++ rune = ' '; ++ + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; +@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + return numspecs; + } + ++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen` ++ * is the length of the dash plus the length of the gap. `fraction` is the ++ * fraction of the dash length compared to `wavelen`. */ ++static void ++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w, ++ int wavelen, float fraction, int thick) ++{ ++ int dashw = MAX(1, fraction * wavelen); ++ for (int i = x - x % wavelen; i < x + w; i += wavelen) { ++ int startx = MAX(i, x); ++ int endx = MIN(i + dashw, x + w); ++ if (startx < endx) ++ XftDrawRect(xw.draw, color, startx, y, endx - startx, ++ thick); ++ } ++} ++ ++/* Draws an undercurl. `h` is the total height, including line thickness. */ ++static void ++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) ++{ ++ XGCValues gcvals = {.foreground = color->pixel, ++ .line_width = thick, ++ .line_style = LineSolid, ++ .cap_style = CapRound}; ++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw), ++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle, ++ &gcvals); ++ ++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h}; ++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted); ++ ++ int yoffset = thick / 2; ++ int segh = MAX(1, h - thick); ++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't ++ * look good without antialiasing. */ ++ int segw = segh; ++ int wavelen = MAX(1, segw * 2); ++ ++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) { ++ XPoint points[3] = {{.x = i, .y = y + yoffset}, ++ {.x = i + segw, .y = y + yoffset + segh}, ++ {.x = i + wavelen, .y = y + yoffset}}; ++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3, ++ CoordModeOrigin); ++ } ++ ++ XFreeGC(xw.dpy, gc); ++} ++ + void + xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) + { + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); +- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, ++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; +@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { +- xclear(0, (y == 0)? 0 : winy, borderpx, ++ xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + +- ((winy + win.ch >= borderpx + win.th)? win.h : 0)); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } +- if (winx + width >= borderpx + win.tw) { ++ if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, +- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) +- xclear(winx, 0, winx + width, borderpx); +- if (winy + win.ch >= borderpx + win.th) ++ xclear(winx, 0, winx + width, win.vborderpx); ++ if (winy + win.ch >= win.vborderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ +@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + +- /* Render the glyphs. */ +- XftDrawGlyphFontSpec(xw.draw, fg, specs, len); +- +- /* Render underline and strikethrough. */ ++ /* Decoration color. */ ++ Color decor; ++ uint32_t decorcolor = tgetdecorcolor(&base); ++ if (decorcolor == DECOR_DEFAULT_COLOR) { ++ decor = *fg; ++ } else if (IS_TRUECOL(decorcolor)) { ++ colfg.alpha = 0xffff; ++ colfg.red = TRUERED(decorcolor); ++ colfg.green = TRUEGREEN(decorcolor); ++ colfg.blue = TRUEBLUE(decorcolor); ++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor); ++ } else { ++ decor = dc.col[decorcolor]; ++ } ++ decor.color.alpha = 0xffff; ++ decor.pixel |= 0xff << 24; ++ ++ /* Float thickness, used as a base to compute other values. */ ++ float fthick = dc.font.height / 18.0; ++ /* Integer thickness in pixels. Must not be 0. */ ++ int thick = MAX(1, roundf(fthick)); ++ /* The default gap between the baseline and a single underline. */ ++ int gap = roundf(fthick * 2); ++ /* The total thickness of a double underline. */ ++ int doubleh = thick * 2 + ceilf(fthick * 0.5); ++ /* The total thickness of an undercurl. */ ++ int curlh = thick * 2 + roundf(fthick * 0.75); ++ ++ /* Render the underline before the glyphs. */ + if (base.mode & ATTR_UNDERLINE) { +- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, +- width, 1); ++ uint32_t style = tgetdecorstyle(&base); ++ int liney = winy + dc.font.ascent + gap; ++ /* Adjust liney to guarantee that a single underline fits. */ ++ liney -= MAX(0, liney + thick - (winy + win.ch)); ++ if (style == UNDERLINE_DOUBLE) { ++ liney -= MAX(0, liney + doubleh - (winy + win.ch)); ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ XftDrawRect(xw.draw, &decor, winx, ++ liney + doubleh - thick, width, thick); ++ } else if (style == UNDERLINE_DOTTED) { ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ thick * 2, 0.5, thick); ++ } else if (style == UNDERLINE_DASHED) { ++ int wavelen = MAX(2, win.cw * 0.9); ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ wavelen, 0.65, thick); ++ } else if (style == UNDERLINE_CURLY) { ++ liney -= MAX(0, liney + curlh - (winy + win.ch)); ++ xdrawundercurl(xw.draw, &decor, winx, liney, width, ++ curlh, thick); ++ } else { ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ } + } + ++ /* Render the glyphs. */ ++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len); ++ ++ /* Render strikethrough. Alway use the fg color. */ + if (base.mode & ATTR_STRUCK) { +- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, +- width, 1); ++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, ++ width, thick); + } + + /* Reset clip to none. */ +@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y) + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); ++ if (g.mode & ATTR_IMAGE) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ xdrawoneimagecell(g, x, y); ++ gr_finish_drawing(xw.buf); ++ } + } + + void +@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + if (IS_SET(MODE_HIDE)) + return; + ++ // If it's an image, just draw a ballot box for simplicity. ++ if (g.mode & ATTR_IMAGE) ++ g.u = 0x2610; ++ + /* + * Select the right color for the right mode. + */ +@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - \ ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + (cx + 1) * win.cw - 1, +- borderpx + cy * win.ch, ++ win.hborderpx + (cx + 1) * win.cw - 1, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - 1, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } + } + ++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming ++ * that they have the same attributes (and thus the same lower 24 bits of the ++ * image ID and the same placement ID). */ ++void ++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) { ++ int y_pix = win.vborderpx + y1 * win.ch; ++ uint32_t image_id_24bits = base.fg & 0xFFFFFF; ++ uint32_t placement_id = tgetimgplacementid(&base); ++ // Columns and rows are 1-based, 0 means unspecified. ++ int last_col = 0; ++ int last_row = 0; ++ int last_start_col = 0; ++ int last_start_x = x1; ++ // The most significant byte is also 1-base, subtract 1 before use. ++ uint32_t last_id_4thbyteplus1 = 0; ++ // We may need to inherit row/column/4th byte from the previous cell. ++ Glyph *prev = &line[x1 - 1]; ++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) && ++ (prev->fg & 0xFFFFFF) == image_id_24bits && ++ prev->decor == base.decor) { ++ last_row = tgetimgrow(prev); ++ last_col = tgetimgcol(prev); ++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev); ++ last_start_col = last_col + 1; ++ } ++ for (int x = x1; x < x2; ++x) { ++ Glyph *g = &line[x]; ++ uint32_t cur_row = tgetimgrow(g); ++ uint32_t cur_col = tgetimgcol(g); ++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g); ++ uint32_t num_diacritics = tgetimgdiacriticcount(g); ++ // If the row is not specified, assume it's the same as the row ++ // of the previous cell. Note that `cur_row` may contain a ++ // value imputed earlier, which will be preserved if `last_row` ++ // is zero (i.e. we don't know the row of the previous cell). ++ if (last_row && (num_diacritics == 0 || !cur_row)) ++ cur_row = last_row; ++ // If the column is not specified and the row is the same as the ++ // row of the previous cell, then assume that the column is the ++ // next one. ++ if (last_col && (num_diacritics <= 1 || !cur_col) && ++ cur_row == last_row) ++ cur_col = last_col + 1; ++ // If the additional id byte is not specified and the ++ // coordinates are consecutive, assume the byte is also the ++ // same. ++ if (last_id_4thbyteplus1 && ++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) && ++ cur_row == last_row && cur_col == last_col + 1) ++ cur_id_4thbyteplus1 = last_id_4thbyteplus1; ++ // If we couldn't infer row and column, start from the top left ++ // corner. ++ if (cur_row == 0) ++ cur_row = 1; ++ if (cur_col == 0) ++ cur_col = 1; ++ // If this cell breaks a contiguous stripe of image cells, draw ++ // that line and start a new one. ++ if (cur_col != last_col + 1 || cur_row != last_row || ++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) { ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ if (last_row != 0) { ++ int x_pix = ++ win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect( ++ xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, ++ last_row - 1, last_row, last_start_x, ++ y1, x_pix, y_pix, win.cw, win.ch, ++ base.mode & ATTR_REVERSE); ++ } ++ last_start_col = cur_col; ++ last_start_x = x; ++ } ++ last_row = cur_row; ++ last_col = cur_col; ++ last_id_4thbyteplus1 = cur_id_4thbyteplus1; ++ // Populate the missing glyph data to enable inheritance between ++ // runs and support the naive implementation of tgetimgid. ++ if (!tgetimgrow(g)) ++ tsetimgrow(g, cur_row); ++ // We cannot save this information if there are > 511 cols. ++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0) ++ tsetimgcol(g, cur_col); ++ if (!tgetimgid4thbyteplus1(g)) ++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1); ++ } ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ // Draw the last contiguous stripe. ++ if (last_row != 0) { ++ int x_pix = win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect(xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, last_row - 1, ++ last_row, last_start_x, y1, x_pix, y_pix, ++ win.cw, win.ch, base.mode & ATTR_REVERSE); ++ } ++} ++ ++/* Draw just one image cell without inheriting attributes from the left. */ ++void xdrawoneimagecell(Glyph g, int x, int y) { ++ if (!(g.mode & ATTR_IMAGE)) ++ return; ++ int x_pix = win.hborderpx + x * win.cw; ++ int y_pix = win.vborderpx + y * win.ch; ++ uint32_t row = tgetimgrow(&g) - 1; ++ uint32_t col = tgetimgcol(&g) - 1; ++ uint32_t placement_id = tgetimgplacementid(&g); ++ uint32_t image_id = tgetimgid(&g); ++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row, ++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch, ++ g.mode & ATTR_REVERSE); ++} ++ ++/* Prepare for image drawing. */ ++void xstartimagedraw(int *dirty, int rows) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ gr_mark_dirty_animations(dirty, rows); ++} ++ ++/* Draw all queued image cells. */ ++void xfinishimagedraw() { ++ gr_finish_drawing(xw.buf); ++} ++ + void + xsetenv(void) + { +@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + specs += i; + numspecs -= i; + i = 0; +@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2) + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (i > 0 && base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + } + + void +@@ -1907,6 +2238,7 @@ cmessage(XEvent *e) + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); ++ gr_deinit(); + exit(0); + } + } +@@ -1957,6 +2289,13 @@ run(void) + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + ++ /* Decrease the timeout if there are active animations. */ ++ if (graphics_next_redraw_delay != INT_MAX && ++ IS_SET(MODE_VISIBLE)) ++ timeout = timeout < 0 ? graphics_next_redraw_delay ++ : MIN(timeout, ++ graphics_next_redraw_delay); ++ + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; +-- +2.43.0 + diff --git a/patches/st-scrollback.diff b/patches/st-scrollback.diff new file mode 100644 index 0000000..f9782e8 --- /dev/null +++ b/patches/st-scrollback.diff @@ -0,0 +1,351 @@ +diff --git a/config.def.h b/config.def.h +index 2cd740a..40b7d93 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -201,6 +201,8 @@ static Shortcut shortcuts[] = { + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, ++ { ShiftMask, XK_Page_Up, kscrollup, {.i = -1} }, ++ { ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} }, + }; + + /* +diff --git a/st.c b/st.c +index b9f66e7..2478942 100644 +--- a/st.c ++++ b/st.c +@@ -35,6 +35,7 @@ + #define ESC_ARG_SIZ 16 + #define STR_BUF_SIZ ESC_BUF_SIZ + #define STR_ARG_SIZ ESC_ARG_SIZ ++#define HISTSIZE 2000 + + /* macros */ + #define IS_SET(flag) ((term.mode & (flag)) != 0) +@@ -42,6 +43,9 @@ + #define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f)) + #define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c)) + #define ISDELIM(u) (u && wcschr(worddelimiters, u)) ++#define TLINE(y) ((y) < term.scr ? term.hist[((y) + term.histi - \ ++ term.scr + HISTSIZE + 1) % HISTSIZE] : \ ++ term.line[(y) - term.scr]) + + enum term_mode { + MODE_WRAP = 1 << 0, +@@ -115,6 +119,9 @@ typedef struct { + int col; /* nb col */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ ++ Line hist[HISTSIZE]; /* history buffer */ ++ int histi; /* history index */ ++ int scr; /* scroll back */ + int *dirty; /* dirtyness of lines */ + TCursor c; /* cursor */ + int ocx; /* old cursor col */ +@@ -185,8 +192,8 @@ static void tnewline(int); + static void tputtab(int); + static void tputc(Rune); + static void treset(void); +-static void tscrollup(int, int); +-static void tscrolldown(int, int); ++static void tscrollup(int, int, int); ++static void tscrolldown(int, int, int); + static void tsetattr(const int *, int); + static void tsetchar(Rune, const Glyph *, int, int); + static void tsetdirt(int, int); +@@ -409,10 +416,10 @@ tlinelen(int y) + { + int i = term.col; + +- if (term.line[y][i - 1].mode & ATTR_WRAP) ++ if (TLINE(y)[i - 1].mode & ATTR_WRAP) + return i; + +- while (i > 0 && term.line[y][i - 1].u == ' ') ++ while (i > 0 && TLINE(y)[i - 1].u == ' ') + --i; + + return i; +@@ -521,7 +528,7 @@ selsnap(int *x, int *y, int direction) + * Snap around if the word wraps around at the end or + * beginning of a line. + */ +- prevgp = &term.line[*y][*x]; ++ prevgp = &TLINE(*y)[*x]; + prevdelim = ISDELIM(prevgp->u); + for (;;) { + newx = *x + direction; +@@ -536,14 +543,14 @@ selsnap(int *x, int *y, int direction) + yt = *y, xt = *x; + else + yt = newy, xt = newx; +- if (!(term.line[yt][xt].mode & ATTR_WRAP)) ++ if (!(TLINE(yt)[xt].mode & ATTR_WRAP)) + break; + } + + if (newx >= tlinelen(newy)) + break; + +- gp = &term.line[newy][newx]; ++ gp = &TLINE(newy)[newx]; + delim = ISDELIM(gp->u); + if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim + || (delim && gp->u != prevgp->u))) +@@ -564,14 +571,14 @@ selsnap(int *x, int *y, int direction) + *x = (direction < 0) ? 0 : term.col - 1; + if (direction < 0) { + for (; *y > 0; *y += direction) { +- if (!(term.line[*y-1][term.col-1].mode ++ if (!(TLINE(*y-1)[term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } else if (direction > 0) { + for (; *y < term.row-1; *y += direction) { +- if (!(term.line[*y][term.col-1].mode ++ if (!(TLINE(*y)[term.col-1].mode + & ATTR_WRAP)) { + break; + } +@@ -602,13 +609,13 @@ getsel(void) + } + + if (sel.type == SEL_RECTANGULAR) { +- gp = &term.line[y][sel.nb.x]; ++ gp = &TLINE(y)[sel.nb.x]; + lastx = sel.ne.x; + } else { +- gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0]; ++ gp = &TLINE(y)[sel.nb.y == y ? sel.nb.x : 0]; + lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; + } +- last = &term.line[y][MIN(lastx, linelen-1)]; ++ last = &TLINE(y)[MIN(lastx, linelen-1)]; + while (last >= gp && last->u == ' ') + --last; + +@@ -844,6 +851,9 @@ void + ttywrite(const char *s, size_t n, int may_echo) + { + const char *next; ++ Arg arg = (Arg) { .i = term.scr }; ++ ++ kscrolldown(&arg); + + if (may_echo && IS_SET(MODE_ECHO)) + twrite(s, n, 1); +@@ -1055,13 +1065,53 @@ tswapscreen(void) + } + + void +-tscrolldown(int orig, int n) ++kscrolldown(const Arg* a) ++{ ++ int n = a->i; ++ ++ if (n < 0) ++ n = term.row + n; ++ ++ if (n > term.scr) ++ n = term.scr; ++ ++ if (term.scr > 0) { ++ term.scr -= n; ++ selscroll(0, -n); ++ tfulldirt(); ++ } ++} ++ ++void ++kscrollup(const Arg* a) ++{ ++ int n = a->i; ++ ++ if (n < 0) ++ n = term.row + n; ++ ++ if (term.scr <= HISTSIZE-n) { ++ term.scr += n; ++ selscroll(0, n); ++ tfulldirt(); ++ } ++} ++ ++void ++tscrolldown(int orig, int n, int copyhist) + { + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + ++ if (copyhist) { ++ term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE; ++ temp = term.hist[term.histi]; ++ term.hist[term.histi] = term.line[term.bot]; ++ term.line[term.bot] = temp; ++ } ++ + tsetdirt(orig, term.bot-n); + tclearregion(0, term.bot-n+1, term.col-1, term.bot); + +@@ -1071,17 +1121,28 @@ tscrolldown(int orig, int n) + term.line[i-n] = temp; + } + +- selscroll(orig, n); ++ if (term.scr == 0) ++ selscroll(orig, n); + } + + void +-tscrollup(int orig, int n) ++tscrollup(int orig, int n, int copyhist) + { + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + ++ if (copyhist) { ++ term.histi = (term.histi + 1) % HISTSIZE; ++ temp = term.hist[term.histi]; ++ term.hist[term.histi] = term.line[orig]; ++ term.line[orig] = temp; ++ } ++ ++ if (term.scr > 0 && term.scr < HISTSIZE) ++ term.scr = MIN(term.scr + n, HISTSIZE-1); ++ + tclearregion(0, orig, term.col-1, orig+n-1); + tsetdirt(orig+n, term.bot); + +@@ -1091,7 +1152,8 @@ tscrollup(int orig, int n) + term.line[i+n] = temp; + } + +- selscroll(orig, -n); ++ if (term.scr == 0) ++ selscroll(orig, -n); + } + + void +@@ -1120,7 +1182,7 @@ tnewline(int first_col) + int y = term.c.y; + + if (y == term.bot) { +- tscrollup(term.top, 1); ++ tscrollup(term.top, 1, 1); + } else { + y++; + } +@@ -1285,14 +1347,14 @@ void + tinsertblankline(int n) + { + if (BETWEEN(term.c.y, term.top, term.bot)) +- tscrolldown(term.c.y, n); ++ tscrolldown(term.c.y, n, 0); + } + + void + tdeleteline(int n) + { + if (BETWEEN(term.c.y, term.top, term.bot)) +- tscrollup(term.c.y, n); ++ tscrollup(term.c.y, n, 0); + } + + int32_t +@@ -1730,11 +1792,11 @@ csihandle(void) + case 'S': /* SU -- Scroll line up */ + if (csiescseq.priv) break; + DEFAULT(csiescseq.arg[0], 1); +- tscrollup(term.top, csiescseq.arg[0]); ++ tscrollup(term.top, csiescseq.arg[0], 0); + break; + case 'T': /* SD -- Scroll line down */ + DEFAULT(csiescseq.arg[0], 1); +- tscrolldown(term.top, csiescseq.arg[0]); ++ tscrolldown(term.top, csiescseq.arg[0], 0); + break; + case 'L': /* IL -- Insert blank lines */ + DEFAULT(csiescseq.arg[0], 1); +@@ -2306,7 +2368,7 @@ eschandle(uchar ascii) + return 0; + case 'D': /* IND -- Linefeed */ + if (term.c.y == term.bot) { +- tscrollup(term.top, 1); ++ tscrollup(term.top, 1, 1); + } else { + tmoveto(term.c.x, term.c.y+1); + } +@@ -2319,7 +2381,7 @@ eschandle(uchar ascii) + break; + case 'M': /* RI -- Reverse index */ + if (term.c.y == term.top) { +- tscrolldown(term.top, 1); ++ tscrolldown(term.top, 1, 1); + } else { + tmoveto(term.c.x, term.c.y-1); + } +@@ -2542,7 +2604,7 @@ twrite(const char *buf, int buflen, int show_ctrl) + void + tresize(int col, int row) + { +- int i; ++ int i, j; + int minrow = MIN(row, term.row); + int mincol = MIN(col, term.col); + int *bp; +@@ -2579,6 +2641,14 @@ tresize(int col, int row) + term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty)); + term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs)); + ++ for (i = 0; i < HISTSIZE; i++) { ++ term.hist[i] = xrealloc(term.hist[i], col * sizeof(Glyph)); ++ for (j = mincol; j < col; j++) { ++ term.hist[i][j] = term.c.attr; ++ term.hist[i][j].u = ' '; ++ } ++ } ++ + /* resize each row to new width, zero-pad if needed */ + for (i = 0; i < minrow; i++) { + term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph)); +@@ -2637,7 +2707,7 @@ drawregion(int x1, int y1, int x2, int y2) + continue; + + term.dirty[y] = 0; +- xdrawline(term.line[y], x1, y, x2); ++ xdrawline(TLINE(y), x1, y, x2); + } + } + +@@ -2658,8 +2728,9 @@ draw(void) + cx--; + + drawregion(0, 0, term.col, term.row); +- xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], +- term.ocx, term.ocy, term.line[term.ocy][term.ocx]); ++ if (term.scr == 0) ++ xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], ++ term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; + term.ocy = term.c.y; + xfinishdraw(); +diff --git a/st.h b/st.h +index fd3b0d8..818a6f8 100644 +--- a/st.h ++++ b/st.h +@@ -81,6 +81,8 @@ void die(const char *, ...); + void redraw(void); + void draw(void); + ++void kscrolldown(const Arg *); ++void kscrollup(const Arg *); + void printscreen(const Arg *); + void printsel(const Arg *); + void sendbreak(const Arg *); diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c new file mode 100644 index 0000000..829c0fc --- /dev/null +++ b/rowcolumn_diacritics_helpers.c @@ -0,0 +1,391 @@ +#include + +uint16_t diacritic_to_num(uint32_t code) +{ + switch (code) { + case 0x305: + return code - 0x305 + 1; + case 0x30d: + case 0x30e: + return code - 0x30d + 2; + case 0x310: + return code - 0x310 + 4; + case 0x312: + return code - 0x312 + 5; + case 0x33d: + case 0x33e: + case 0x33f: + return code - 0x33d + 6; + case 0x346: + return code - 0x346 + 9; + case 0x34a: + case 0x34b: + case 0x34c: + return code - 0x34a + 10; + case 0x350: + case 0x351: + case 0x352: + return code - 0x350 + 13; + case 0x357: + return code - 0x357 + 16; + case 0x35b: + return code - 0x35b + 17; + case 0x363: + case 0x364: + case 0x365: + case 0x366: + case 0x367: + case 0x368: + case 0x369: + case 0x36a: + case 0x36b: + case 0x36c: + case 0x36d: + case 0x36e: + case 0x36f: + return code - 0x363 + 18; + case 0x483: + case 0x484: + case 0x485: + case 0x486: + case 0x487: + return code - 0x483 + 31; + case 0x592: + case 0x593: + case 0x594: + case 0x595: + return code - 0x592 + 36; + case 0x597: + case 0x598: + case 0x599: + return code - 0x597 + 40; + case 0x59c: + case 0x59d: + case 0x59e: + case 0x59f: + case 0x5a0: + case 0x5a1: + return code - 0x59c + 43; + case 0x5a8: + case 0x5a9: + return code - 0x5a8 + 49; + case 0x5ab: + case 0x5ac: + return code - 0x5ab + 51; + case 0x5af: + return code - 0x5af + 53; + case 0x5c4: + return code - 0x5c4 + 54; + case 0x610: + case 0x611: + case 0x612: + case 0x613: + case 0x614: + case 0x615: + case 0x616: + case 0x617: + return code - 0x610 + 55; + case 0x657: + case 0x658: + case 0x659: + case 0x65a: + case 0x65b: + return code - 0x657 + 63; + case 0x65d: + case 0x65e: + return code - 0x65d + 68; + case 0x6d6: + case 0x6d7: + case 0x6d8: + case 0x6d9: + case 0x6da: + case 0x6db: + case 0x6dc: + return code - 0x6d6 + 70; + case 0x6df: + case 0x6e0: + case 0x6e1: + case 0x6e2: + return code - 0x6df + 77; + case 0x6e4: + return code - 0x6e4 + 81; + case 0x6e7: + case 0x6e8: + return code - 0x6e7 + 82; + case 0x6eb: + case 0x6ec: + return code - 0x6eb + 84; + case 0x730: + return code - 0x730 + 86; + case 0x732: + case 0x733: + return code - 0x732 + 87; + case 0x735: + case 0x736: + return code - 0x735 + 89; + case 0x73a: + return code - 0x73a + 91; + case 0x73d: + return code - 0x73d + 92; + case 0x73f: + case 0x740: + case 0x741: + return code - 0x73f + 93; + case 0x743: + return code - 0x743 + 96; + case 0x745: + return code - 0x745 + 97; + case 0x747: + return code - 0x747 + 98; + case 0x749: + case 0x74a: + return code - 0x749 + 99; + case 0x7eb: + case 0x7ec: + case 0x7ed: + case 0x7ee: + case 0x7ef: + case 0x7f0: + case 0x7f1: + return code - 0x7eb + 101; + case 0x7f3: + return code - 0x7f3 + 108; + case 0x816: + case 0x817: + case 0x818: + case 0x819: + return code - 0x816 + 109; + case 0x81b: + case 0x81c: + case 0x81d: + case 0x81e: + case 0x81f: + case 0x820: + case 0x821: + case 0x822: + case 0x823: + return code - 0x81b + 113; + case 0x825: + case 0x826: + case 0x827: + return code - 0x825 + 122; + case 0x829: + case 0x82a: + case 0x82b: + case 0x82c: + case 0x82d: + return code - 0x829 + 125; + case 0x951: + return code - 0x951 + 130; + case 0x953: + case 0x954: + return code - 0x953 + 131; + case 0xf82: + case 0xf83: + return code - 0xf82 + 133; + case 0xf86: + case 0xf87: + return code - 0xf86 + 135; + case 0x135d: + case 0x135e: + case 0x135f: + return code - 0x135d + 137; + case 0x17dd: + return code - 0x17dd + 140; + case 0x193a: + return code - 0x193a + 141; + case 0x1a17: + return code - 0x1a17 + 142; + case 0x1a75: + case 0x1a76: + case 0x1a77: + case 0x1a78: + case 0x1a79: + case 0x1a7a: + case 0x1a7b: + case 0x1a7c: + return code - 0x1a75 + 143; + case 0x1b6b: + return code - 0x1b6b + 151; + case 0x1b6d: + case 0x1b6e: + case 0x1b6f: + case 0x1b70: + case 0x1b71: + case 0x1b72: + case 0x1b73: + return code - 0x1b6d + 152; + case 0x1cd0: + case 0x1cd1: + case 0x1cd2: + return code - 0x1cd0 + 159; + case 0x1cda: + case 0x1cdb: + return code - 0x1cda + 162; + case 0x1ce0: + return code - 0x1ce0 + 164; + case 0x1dc0: + case 0x1dc1: + return code - 0x1dc0 + 165; + case 0x1dc3: + case 0x1dc4: + case 0x1dc5: + case 0x1dc6: + case 0x1dc7: + case 0x1dc8: + case 0x1dc9: + return code - 0x1dc3 + 167; + case 0x1dcb: + case 0x1dcc: + return code - 0x1dcb + 174; + case 0x1dd1: + case 0x1dd2: + case 0x1dd3: + case 0x1dd4: + case 0x1dd5: + case 0x1dd6: + case 0x1dd7: + case 0x1dd8: + case 0x1dd9: + case 0x1dda: + case 0x1ddb: + case 0x1ddc: + case 0x1ddd: + case 0x1dde: + case 0x1ddf: + case 0x1de0: + case 0x1de1: + case 0x1de2: + case 0x1de3: + case 0x1de4: + case 0x1de5: + case 0x1de6: + return code - 0x1dd1 + 176; + case 0x1dfe: + return code - 0x1dfe + 198; + case 0x20d0: + case 0x20d1: + return code - 0x20d0 + 199; + case 0x20d4: + case 0x20d5: + case 0x20d6: + case 0x20d7: + return code - 0x20d4 + 201; + case 0x20db: + case 0x20dc: + return code - 0x20db + 205; + case 0x20e1: + return code - 0x20e1 + 207; + case 0x20e7: + return code - 0x20e7 + 208; + case 0x20e9: + return code - 0x20e9 + 209; + case 0x20f0: + return code - 0x20f0 + 210; + case 0x2cef: + case 0x2cf0: + case 0x2cf1: + return code - 0x2cef + 211; + case 0x2de0: + case 0x2de1: + case 0x2de2: + case 0x2de3: + case 0x2de4: + case 0x2de5: + case 0x2de6: + case 0x2de7: + case 0x2de8: + case 0x2de9: + case 0x2dea: + case 0x2deb: + case 0x2dec: + case 0x2ded: + case 0x2dee: + case 0x2def: + case 0x2df0: + case 0x2df1: + case 0x2df2: + case 0x2df3: + case 0x2df4: + case 0x2df5: + case 0x2df6: + case 0x2df7: + case 0x2df8: + case 0x2df9: + case 0x2dfa: + case 0x2dfb: + case 0x2dfc: + case 0x2dfd: + case 0x2dfe: + case 0x2dff: + return code - 0x2de0 + 214; + case 0xa66f: + return code - 0xa66f + 246; + case 0xa67c: + case 0xa67d: + return code - 0xa67c + 247; + case 0xa6f0: + case 0xa6f1: + return code - 0xa6f0 + 249; + case 0xa8e0: + case 0xa8e1: + case 0xa8e2: + case 0xa8e3: + case 0xa8e4: + case 0xa8e5: + case 0xa8e6: + case 0xa8e7: + case 0xa8e8: + case 0xa8e9: + case 0xa8ea: + case 0xa8eb: + case 0xa8ec: + case 0xa8ed: + case 0xa8ee: + case 0xa8ef: + case 0xa8f0: + case 0xa8f1: + return code - 0xa8e0 + 251; + case 0xaab0: + return code - 0xaab0 + 269; + case 0xaab2: + case 0xaab3: + return code - 0xaab2 + 270; + case 0xaab7: + case 0xaab8: + return code - 0xaab7 + 272; + case 0xaabe: + case 0xaabf: + return code - 0xaabe + 274; + case 0xaac1: + return code - 0xaac1 + 276; + case 0xfe20: + case 0xfe21: + case 0xfe22: + case 0xfe23: + case 0xfe24: + case 0xfe25: + case 0xfe26: + return code - 0xfe20 + 277; + case 0x10a0f: + return code - 0x10a0f + 284; + case 0x10a38: + return code - 0x10a38 + 285; + case 0x1d185: + case 0x1d186: + case 0x1d187: + case 0x1d188: + case 0x1d189: + return code - 0x1d185 + 286; + case 0x1d1aa: + case 0x1d1ab: + case 0x1d1ac: + case 0x1d1ad: + return code - 0x1d1aa + 291; + case 0x1d242: + case 0x1d243: + case 0x1d244: + return code - 0x1d242 + 295; + } + return 0; +} diff --git a/rowcolumn_diacritics_helpers.o b/rowcolumn_diacritics_helpers.o new file mode 100644 index 0000000..e3b6b55 Binary files /dev/null and b/rowcolumn_diacritics_helpers.o differ -- cgit v1.2.3