diff options
author | ZachIR <zachir@librem.one> | 2025-07-08 21:48:39 -0500 |
---|---|---|
committer | ZachIR <zachir@librem.one> | 2025-07-08 21:48:39 -0500 |
commit | cbffff166748e7dada4b66ed1c4f10f495eefa97 (patch) | |
tree | e5b15a872bb79b4beb56c8177b229c58b356bdb5 | |
parent | d79cc4cea16572f7f80c3842031406172630f697 (diff) |
Add in st-graphics repo
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | README.md | 183 | ||||
-rw-r--r-- | boxdraw.c | 194 | ||||
-rw-r--r-- | boxdraw_data.h | 214 | ||||
-rw-r--r-- | config.def.h | 71 | ||||
-rw-r--r-- | config.mk | 8 | ||||
-rwxr-xr-x | generate-rowcolumn-helpers.py | 111 | ||||
-rw-r--r-- | graphics.c | 4063 | ||||
-rw-r--r-- | graphics.h | 112 | ||||
-rw-r--r-- | hb.c | 3 | ||||
-rwxr-xr-x | icat-mini.sh | 875 | ||||
-rw-r--r-- | khash.h | 627 | ||||
-rw-r--r-- | kvec.h | 90 | ||||
-rw-r--r-- | patches/st-kitty-graphics-20240922-a0274bc.diff | 7324 | ||||
-rw-r--r-- | rowcolumn-diacritics.txt | 310 | ||||
-rw-r--r-- | rowcolumn_diacritics.sh | 1 | ||||
-rw-r--r-- | rowcolumn_diacritics_helpers.c | 391 | ||||
-rw-r--r-- | st.c | 777 | ||||
-rw-r--r-- | st.h | 105 | ||||
-rw-r--r-- | st.info | 6 | ||||
-rw-r--r-- | win.h | 3 | ||||
-rw-r--r-- | x.c | 768 |
22 files changed, 15854 insertions, 390 deletions
@@ -4,7 +4,7 @@ include config.mk -SRC = st.c x.c hb.c +SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c boxdraw.c hb.c OBJ = $(SRC:.c=.o) all: st @@ -15,8 +15,10 @@ config.h: .c.o: $(CC) $(STCFLAGS) -c $< -st.o: config.h st.h win.h -x.o: arg.h config.h st.h win.h hb.h +st.o: config.h st.h win.h graphics.h +x.o: arg.h config.h st.h win.h graphics.h hb.h +graphics.c: graphics.h khash.h kvec.h st.h +boxdraw.o: config.h st.h boxdraw_data.h hb.o: st.h $(OBJ): config.h config.mk diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cd3113 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# st-graphics + +This is a fork of [st](https://st.suckless.org/) that implements a subset of +[kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/). + +If you want this formatted as a single patch, take the last commit from this +branch: [graphics-squashed](https://github.com/sergei-grechanik/st-graphics/tree/graphics-squashed) +(may be slightly outdated). + +If you want to combine this with other patches, check out the +[graphics-with-patches branch](https://github.com/sergei-grechanik/st-graphics/tree/graphics-with-patches) +(see also [patch compatibility](#patch-compatibility)). + + + + +This repository also includes a simple script to display images `icat-mini.sh`. +Note: to make it work in tmux you need to enable pass-through sequences, i.e. +add something like this to your `.tmux.conf`: + + set -gq allow-passthrough all + +You also need to make sure that tmux supports 24-bit colors and that it knows +that the client terminal supports 24-bit colors (you may need to install the st +terminfo entry on all systems that you use, including remote ones). + +## Installation + +As usual, copy `config.def.h` to `config.h`, modify it according to your needs, +run `make install` or `DESTDIR=/some/prefix make install`. + +In addition to the standard st dependencies (X11, fontconfig, freetype2), +you will need imlib2 and zlib for the graphics module. + +## Configuration + +**This fork includes some seemingly unrelated changes, like anysize and +underline color and style. You may want to tweak them too if you don't like the +defaults. See also [patch compatibility](#patch-compatibility).** + +You may want to change the graphics-related shortcuts and image size limits (see +`config.def.h`). + +Default shortcuts: +- `Ctrl+Shift+RightClick` to preview the clicked image in feh. +- `Ctrl+Shift+MiddleClick` to see debug info (image id, placement id, etc). +- `Ctrl+Shift+F1` to toggle graphics debug mode. It has three states: 1) no + debugging; 2) show general info and print logs to stderr; 3) print logs and + show bounding boxes. +- `Ctrl+Shift+F6` to dump the state of all images to stderr. +- `Ctrl+Shift+F7` to unload all images from ram (but the cache in `/tmp` will be + preserved). +- `Ctrl+Shift+F8` to toggle image display. + +## Features + +Originally I implemented it to prototype the Unicode placeholder feature (which +is now included in the kitty graphics protocol). Classic placements were +retrofitted later, and under the hood they are implemented via the same +placeholder mechanism. This means that a cell may be occupied only by one +placement. It is the main reason why some things don't work or work a bit +differently. + +Here is the list of supported (✅ ), unsupported (❌), and non-standard (⚡) +features. + +- Uploading: + - Formats: + - ✅ PNG (`f=100`) + - ✅ RGB, RGBA (`f=24`, `f=32`) + - ✅ Compression with zlib (`o=z`) + - ⚡ jpeg. Actually any format supported by imlib2 should work. The key + value is the same as for png (`f=100`). + - Transmission mediums: + - ✅ Direct (`t=d`) + - ✅ File (`t=f`) + - ✅ Temporary file (`t=t`) + - ✅ Shared memory object (`t=s`) + - ✅ Size and offset specification (`S` and `O` keys) for shared memory + - ❌ Not fully supported for files + - ✅ Image numbers + - ✅ Responses + - ✅ Transmit and display (`a=T`) +- Placement: + - ✅ Classic placements (but see the note above) + - ✅ Unicode placeholders + - ✅ Placement IDs + - ❌ NOTE: Placement IDs must be 24-bit (between 1 and 16777215). + - ✅ Cursor movement policies `C=1` and `C=0` + - ✅ Source rectangle (`x, y, w, h`) + - ✅ The number of rows/columns (`r, c`) + - ❌ Cell offsets (`X, Y`) + - ❌ z-index. Classic placements will erase old placements and the text on + overlap. + - ❌ Relative placements (`P, Q, H, V`) +- Deletion: + - ✅ Deletion of image data when the specifier is uppercase + - ✅ All visible classic placements (`d=a`) + - ✅ By image id/number and placement id (`d=i`, `d=n`) + - ❌ By position (specifiers `c, p, q, x, y, z`) + - ❌ Animation frames (`d=f`) +- Animation - experimental + - ✅ Transferring animation frames (`a=f`) + - ✅ Transferring part of the image (`x, y, s, v`) + - ✅ Background colors (`Y`) + - ✅ Background frames (`c`) + - ❌ Editing the existing frame (`r`) + - ✅ Delays (`z`) + - ✅ Controlling animations (`a=a`) + - ✅ Stop, run, and loading modes (`s`) + - ❌ Number of loop (`v`) + - ✅ Setting the delay (`z`) + - ❌ Animation frame composition (`a=c`) + - ❌ Frame deletion (`d=f`) + - ✅ Kitty's icat kitten is supported. You can also use `icat-mini.sh` + (slower). + +## Things I have tested + +### Apps that seem to work +- Kitty's icat (including from within tmux). +- [termpdf](https://github.com/dsanson/termpdf.py) +- [ranger](https://github.com/ranger/ranger) - I had to explicitly set + `TERM=kitty`. +- [tpix](https://github.com/jesvedberg/tpix) +- [pixcat](https://github.com/mirukana/pixcat) (modulo some unsupported keys + that are currently ignored). +- [viu](https://github.com/atanunq/viu) - I had to explicitly set + `TERM=kitty`. +- [timg](https://github.com/hzeller/timg) - I had to explicitly pass `-pk` + (i.e. `timg -pk <image>`). If your timg is fresh enough, it even works in + tmux! +- [yazi](https://github.com/sxyazi/yazi) - works if you run it as + `TERM="xterm-kitty" yazi`. + +### Apps that sort of work +- [hologram.nvim](https://github.com/edluffy/hologram.nvim) - There are some + glitches, like erasure of parts of the status line. +- [mpv](https://github.com/mpv-player/mpv) - shared memory works + (`mpv --vo=kitty --vo-kitty-use-shm=yes`), but slower than kitty, and there is + an annoying flickering status line at the top (unsupported z-index). There are + also some artifacts on high resolutions. + +### Apps that don't work +- [termvisage](https://github.com/AnonymouX47/termvisage) - seems to erase + cells containing the image after image placement. In kitty this has no effect + on classic placements because they aren't attached to cells, but in + st-graphics classic placements are implemented on top of Unicode placements, + so they get erased. + +## Patch compatibility + +The [graphics-with-patches branch](https://github.com/sergei-grechanik/st-graphics/tree/graphics-with-patches) +contains several patches applied on top of the graphics branch. The patches +include: + +- [Boxdraw](https://st.suckless.org/patches/boxdraw) - applied with minimal + conflicts. +- [Scrollback](https://st.suckless.org/patches/scrollback) - quite a few + conflicts, but easy to resolve. +- [Wide glyph support](https://st.suckless.org/patches/glyph_wide_support/) +- [Ligatures](https://st.suckless.org/patches/ligatures) - + required some modifications. +- [Alpha](https://st.suckless.org/patches/alpha) +- [External pipe](https://st.suckless.org/patches/externalpipe) + +If you run into any problems with other patches, feel free to open an issue. + +### Patches and changes that are included in the graphics implementation + +This fork includes some patches and features that are not graphics-related +per se, but are hard to disentangle from the graphics implementation: +- [Anysize](https://st.suckless.org/patches/anysize/) - this patch is applied + and on by default. If you want the "expected" anysize behavior (no centering), + set `anysize_halign` and `anysize_valign` to zero in `config.h`. +- Support for several XTWINOPS control sequences to query information that is + sometimes required for image display (like cell size in pixels). +- Support for decoration (underline) color and style. The decoration color is + used to specify the placement id in Unicode placeholders, so it's hard to make + them separate. The behavior of the underline is different from the upstream + st: it's drawn behind the text and the thickness depends on the font size. You + may need to tweak the code in `xdrawglyphfontspecs` in `x.c` if you don't like + it. diff --git a/boxdraw.c b/boxdraw.c new file mode 100644 index 0000000..28a92d0 --- /dev/null +++ b/boxdraw.c @@ -0,0 +1,194 @@ +/* + * Copyright 2018 Avi Halachmi (:avih) avihpit@yahoo.com https://github.com/avih + * MIT/X Consortium License + */ + +#include <X11/Xft/Xft.h> +#include "st.h" +#include "boxdraw_data.h" + +/* Rounded non-negative integers division of n / d */ +#define DIV(n, d) (((n) + (d) / 2) / (d)) + +static Display *xdpy; +static Colormap xcmap; +static XftDraw *xd; +static Visual *xvis; + +static void drawbox(int, int, int, int, XftColor *, XftColor *, ushort); +static void drawboxlines(int, int, int, int, XftColor *, ushort); + +/* public API */ + +void +boxdraw_xinit(Display *dpy, Colormap cmap, XftDraw *draw, Visual *vis) +{ + xdpy = dpy; xcmap = cmap; xd = draw, xvis = vis; +} + +int +isboxdraw(Rune u) +{ + Rune block = u & ~0xff; + return (boxdraw && block == 0x2500 && boxdata[(uint8_t)u]) || + (boxdraw_braille && block == 0x2800); +} + +/* the "index" is actually the entire shape data encoded as ushort */ +ushort +boxdrawindex(const Glyph *g) +{ + if (boxdraw_braille && (g->u & ~0xff) == 0x2800) + return BRL | (uint8_t)g->u; + if (boxdraw_bold && (g->mode & ATTR_BOLD)) + return BDB | boxdata[(uint8_t)g->u]; + return boxdata[(uint8_t)g->u]; +} + +void +drawboxes(int x, int y, int cw, int ch, XftColor *fg, XftColor *bg, + const XftGlyphFontSpec *specs, int len) +{ + for ( ; len-- > 0; x += cw, specs++) + drawbox(x, y, cw, ch, fg, bg, (ushort)specs->glyph); +} + +/* implementation */ + +void +drawbox(int x, int y, int w, int h, XftColor *fg, XftColor *bg, ushort bd) +{ + ushort cat = bd & ~(BDB | 0xff); /* mask out bold and data */ + if (bd & (BDL | BDA)) { + /* lines (light/double/heavy/arcs) */ + drawboxlines(x, y, w, h, fg, bd); + + } else if (cat == BBD) { + /* lower (8-X)/8 block */ + int d = DIV((uint8_t)bd * h, 8); + XftDrawRect(xd, fg, x, y + d, w, h - d); + + } else if (cat == BBU) { + /* upper X/8 block */ + XftDrawRect(xd, fg, x, y, w, DIV((uint8_t)bd * h, 8)); + + } else if (cat == BBL) { + /* left X/8 block */ + XftDrawRect(xd, fg, x, y, DIV((uint8_t)bd * w, 8), h); + + } else if (cat == BBR) { + /* right (8-X)/8 block */ + int d = DIV((uint8_t)bd * w, 8); + XftDrawRect(xd, fg, x + d, y, w - d, h); + + } else if (cat == BBQ) { + /* Quadrants */ + int w2 = DIV(w, 2), h2 = DIV(h, 2); + if (bd & TL) + XftDrawRect(xd, fg, x, y, w2, h2); + if (bd & TR) + XftDrawRect(xd, fg, x + w2, y, w - w2, h2); + if (bd & BL) + XftDrawRect(xd, fg, x, y + h2, w2, h - h2); + if (bd & BR) + XftDrawRect(xd, fg, x + w2, y + h2, w - w2, h - h2); + + } else if (bd & BBS) { + /* Shades - data is 1/2/3 for 25%/50%/75% alpha, respectively */ + int d = (uint8_t)bd; + XftColor xfc; + XRenderColor xrc = { .alpha = 0xffff }; + + xrc.red = DIV(fg->color.red * d + bg->color.red * (4 - d), 4); + xrc.green = DIV(fg->color.green * d + bg->color.green * (4 - d), 4); + xrc.blue = DIV(fg->color.blue * d + bg->color.blue * (4 - d), 4); + + XftColorAllocValue(xdpy, xvis, xcmap, &xrc, &xfc); + XftDrawRect(xd, &xfc, x, y, w, h); + XftColorFree(xdpy, xvis, xcmap, &xfc); + + } else if (cat == BRL) { + /* braille, each data bit corresponds to one dot at 2x4 grid */ + int w1 = DIV(w, 2); + int h1 = DIV(h, 4), h2 = DIV(h, 2), h3 = DIV(3 * h, 4); + + if (bd & 1) XftDrawRect(xd, fg, x, y, w1, h1); + if (bd & 2) XftDrawRect(xd, fg, x, y + h1, w1, h2 - h1); + if (bd & 4) XftDrawRect(xd, fg, x, y + h2, w1, h3 - h2); + if (bd & 8) XftDrawRect(xd, fg, x + w1, y, w - w1, h1); + if (bd & 16) XftDrawRect(xd, fg, x + w1, y + h1, w - w1, h2 - h1); + if (bd & 32) XftDrawRect(xd, fg, x + w1, y + h2, w - w1, h3 - h2); + if (bd & 64) XftDrawRect(xd, fg, x, y + h3, w1, h - h3); + if (bd & 128) XftDrawRect(xd, fg, x + w1, y + h3, w - w1, h - h3); + + } +} + +void +drawboxlines(int x, int y, int w, int h, XftColor *fg, ushort bd) +{ + /* s: stem thickness. width/8 roughly matches underscore thickness. */ + /* We draw bold as 1.5 * normal-stem and at least 1px thicker. */ + /* doubles draw at least 3px, even when w or h < 3. bold needs 6px. */ + int mwh = MIN(w, h); + int base_s = MAX(1, DIV(mwh, 8)); + int bold = (bd & BDB) && mwh >= 6; /* possibly ignore boldness */ + int s = bold ? MAX(base_s + 1, DIV(3 * base_s, 2)) : base_s; + int w2 = DIV(w - s, 2), h2 = DIV(h - s, 2); + /* the s-by-s square (x + w2, y + h2, s, s) is the center texel. */ + /* The base length (per direction till edge) includes this square. */ + + int light = bd & (LL | LU | LR | LD); + int double_ = bd & (DL | DU | DR | DD); + + if (light) { + /* d: additional (negative) length to not-draw the center */ + /* texel - at arcs and avoid drawing inside (some) doubles */ + int arc = bd & BDA; + int multi_light = light & (light - 1); + int multi_double = double_ & (double_ - 1); + /* light crosses double only at DH+LV, DV+LH (ref. shapes) */ + int d = arc || (multi_double && !multi_light) ? -s : 0; + + if (bd & LL) + XftDrawRect(xd, fg, x, y + h2, w2 + s + d, s); + if (bd & LU) + XftDrawRect(xd, fg, x + w2, y, s, h2 + s + d); + if (bd & LR) + XftDrawRect(xd, fg, x + w2 - d, y + h2, w - w2 + d, s); + if (bd & LD) + XftDrawRect(xd, fg, x + w2, y + h2 - d, s, h - h2 + d); + } + + /* double lines - also align with light to form heavy when combined */ + if (double_) { + /* + * going clockwise, for each double-ray: p is additional length + * to the single-ray nearer to the previous direction, and n to + * the next. p and n adjust from the base length to lengths + * which consider other doubles - shorter to avoid intersections + * (p, n), or longer to draw the far-corner texel (n). + */ + int dl = bd & DL, du = bd & DU, dr = bd & DR, dd = bd & DD; + if (dl) { + int p = dd ? -s : 0, n = du ? -s : dd ? s : 0; + XftDrawRect(xd, fg, x, y + h2 + s, w2 + s + p, s); + XftDrawRect(xd, fg, x, y + h2 - s, w2 + s + n, s); + } + if (du) { + int p = dl ? -s : 0, n = dr ? -s : dl ? s : 0; + XftDrawRect(xd, fg, x + w2 - s, y, s, h2 + s + p); + XftDrawRect(xd, fg, x + w2 + s, y, s, h2 + s + n); + } + if (dr) { + int p = du ? -s : 0, n = dd ? -s : du ? s : 0; + XftDrawRect(xd, fg, x + w2 - p, y + h2 - s, w - w2 + p, s); + XftDrawRect(xd, fg, x + w2 - n, y + h2 + s, w - w2 + n, s); + } + if (dd) { + int p = dr ? -s : 0, n = dl ? -s : dr ? s : 0; + XftDrawRect(xd, fg, x + w2 + s, y + h2 - p, s, h - h2 + p); + XftDrawRect(xd, fg, x + w2 - s, y + h2 - n, s, h - h2 + n); + } + } +} diff --git a/boxdraw_data.h b/boxdraw_data.h new file mode 100644 index 0000000..7890500 --- /dev/null +++ b/boxdraw_data.h @@ -0,0 +1,214 @@ +/* + * Copyright 2018 Avi Halachmi (:avih) avihpit@yahoo.com https://github.com/avih + * MIT/X Consortium License + */ + +/* + * U+25XX codepoints data + * + * References: + * http://www.unicode.org/charts/PDF/U2500.pdf + * http://www.unicode.org/charts/PDF/U2580.pdf + * + * Test page: + * https://github.com/GNOME/vte/blob/master/doc/boxes.txt + */ + +/* Each shape is encoded as 16-bits. Higher bits are category, lower are data */ +/* Categories (mutually exclusive except BDB): */ +/* For convenience, BDL/BDA/BBS/BDB are 1 bit each, the rest are enums */ +#define BDL (1<<8) /* Box Draw Lines (light/double/heavy) */ +#define BDA (1<<9) /* Box Draw Arc (light) */ + +#define BBD (1<<10) /* Box Block Down (lower) X/8 */ +#define BBL (2<<10) /* Box Block Left X/8 */ +#define BBU (3<<10) /* Box Block Upper X/8 */ +#define BBR (4<<10) /* Box Block Right X/8 */ +#define BBQ (5<<10) /* Box Block Quadrants */ +#define BRL (6<<10) /* Box Braille (data is lower byte of U28XX) */ + +#define BBS (1<<14) /* Box Block Shades */ +#define BDB (1<<15) /* Box Draw is Bold */ + +/* (BDL/BDA) Light/Double/Heavy x Left/Up/Right/Down/Horizontal/Vertical */ +/* Heavy is light+double (literally drawing light+double align to form heavy) */ +#define LL (1<<0) +#define LU (1<<1) +#define LR (1<<2) +#define LD (1<<3) +#define LH (LL+LR) +#define LV (LU+LD) + +#define DL (1<<4) +#define DU (1<<5) +#define DR (1<<6) +#define DD (1<<7) +#define DH (DL+DR) +#define DV (DU+DD) + +#define HL (LL+DL) +#define HU (LU+DU) +#define HR (LR+DR) +#define HD (LD+DD) +#define HH (HL+HR) +#define HV (HU+HD) + +/* (BBQ) Quadrants Top/Bottom x Left/Right */ +#define TL (1<<0) +#define TR (1<<1) +#define BL (1<<2) +#define BR (1<<3) + +/* Data for U+2500 - U+259F except dashes/diagonals */ +static const unsigned short boxdata[256] = { + /* light lines */ + [0x00] = BDL + LH, /* light horizontal */ + [0x02] = BDL + LV, /* light vertical */ + [0x0c] = BDL + LD + LR, /* light down and right */ + [0x10] = BDL + LD + LL, /* light down and left */ + [0x14] = BDL + LU + LR, /* light up and right */ + [0x18] = BDL + LU + LL, /* light up and left */ + [0x1c] = BDL + LV + LR, /* light vertical and right */ + [0x24] = BDL + LV + LL, /* light vertical and left */ + [0x2c] = BDL + LH + LD, /* light horizontal and down */ + [0x34] = BDL + LH + LU, /* light horizontal and up */ + [0x3c] = BDL + LV + LH, /* light vertical and horizontal */ + [0x74] = BDL + LL, /* light left */ + [0x75] = BDL + LU, /* light up */ + [0x76] = BDL + LR, /* light right */ + [0x77] = BDL + LD, /* light down */ + + /* heavy [+light] lines */ + [0x01] = BDL + HH, + [0x03] = BDL + HV, + [0x0d] = BDL + HR + LD, + [0x0e] = BDL + HD + LR, + [0x0f] = BDL + HD + HR, + [0x11] = BDL + HL + LD, + [0x12] = BDL + HD + LL, + [0x13] = BDL + HD + HL, + [0x15] = BDL + HR + LU, + [0x16] = BDL + HU + LR, + [0x17] = BDL + HU + HR, + [0x19] = BDL + HL + LU, + [0x1a] = BDL + HU + LL, + [0x1b] = BDL + HU + HL, + [0x1d] = BDL + HR + LV, + [0x1e] = BDL + HU + LD + LR, + [0x1f] = BDL + HD + LR + LU, + [0x20] = BDL + HV + LR, + [0x21] = BDL + HU + HR + LD, + [0x22] = BDL + HD + HR + LU, + [0x23] = BDL + HV + HR, + [0x25] = BDL + HL + LV, + [0x26] = BDL + HU + LD + LL, + [0x27] = BDL + HD + LU + LL, + [0x28] = BDL + HV + LL, + [0x29] = BDL + HU + HL + LD, + [0x2a] = BDL + HD + HL + LU, + [0x2b] = BDL + HV + HL, + [0x2d] = BDL + HL + LD + LR, + [0x2e] = BDL + HR + LL + LD, + [0x2f] = BDL + HH + LD, + [0x30] = BDL + HD + LH, + [0x31] = BDL + HD + HL + LR, + [0x32] = BDL + HR + HD + LL, + [0x33] = BDL + HH + HD, + [0x35] = BDL + HL + LU + LR, + [0x36] = BDL + HR + LU + LL, + [0x37] = BDL + HH + LU, + [0x38] = BDL + HU + LH, + [0x39] = BDL + HU + HL + LR, + [0x3a] = BDL + HU + HR + LL, + [0x3b] = BDL + HH + HU, + [0x3d] = BDL + HL + LV + LR, + [0x3e] = BDL + HR + LV + LL, + [0x3f] = BDL + HH + LV, + [0x40] = BDL + HU + LH + LD, + [0x41] = BDL + HD + LH + LU, + [0x42] = BDL + HV + LH, + [0x43] = BDL + HU + HL + LD + LR, + [0x44] = BDL + HU + HR + LD + LL, + [0x45] = BDL + HD + HL + LU + LR, + [0x46] = BDL + HD + HR + LU + LL, + [0x47] = BDL + HH + HU + LD, + [0x48] = BDL + HH + HD + LU, + [0x49] = BDL + HV + HL + LR, + [0x4a] = BDL + HV + HR + LL, + [0x4b] = BDL + HV + HH, + [0x78] = BDL + HL, + [0x79] = BDL + HU, + [0x7a] = BDL + HR, + [0x7b] = BDL + HD, + [0x7c] = BDL + HR + LL, + [0x7d] = BDL + HD + LU, + [0x7e] = BDL + HL + LR, + [0x7f] = BDL + HU + LD, + + /* double [+light] lines */ + [0x50] = BDL + DH, + [0x51] = BDL + DV, + [0x52] = BDL + DR + LD, + [0x53] = BDL + DD + LR, + [0x54] = BDL + DR + DD, + [0x55] = BDL + DL + LD, + [0x56] = BDL + DD + LL, + [0x57] = BDL + DL + DD, + [0x58] = BDL + DR + LU, + [0x59] = BDL + DU + LR, + [0x5a] = BDL + DU + DR, + [0x5b] = BDL + DL + LU, + [0x5c] = BDL + DU + LL, + [0x5d] = BDL + DL + DU, + [0x5e] = BDL + DR + LV, + [0x5f] = BDL + DV + LR, + [0x60] = BDL + DV + DR, + [0x61] = BDL + DL + LV, + [0x62] = BDL + DV + LL, + [0x63] = BDL + DV + DL, + [0x64] = BDL + DH + LD, + [0x65] = BDL + DD + LH, + [0x66] = BDL + DD + DH, + [0x67] = BDL + DH + LU, + [0x68] = BDL + DU + LH, + [0x69] = BDL + DH + DU, + [0x6a] = BDL + DH + LV, + [0x6b] = BDL + DV + LH, + [0x6c] = BDL + DH + DV, + + /* (light) arcs */ + [0x6d] = BDA + LD + LR, + [0x6e] = BDA + LD + LL, + [0x6f] = BDA + LU + LL, + [0x70] = BDA + LU + LR, + + /* Lower (Down) X/8 block (data is 8 - X) */ + [0x81] = BBD + 7, [0x82] = BBD + 6, [0x83] = BBD + 5, [0x84] = BBD + 4, + [0x85] = BBD + 3, [0x86] = BBD + 2, [0x87] = BBD + 1, [0x88] = BBD + 0, + + /* Left X/8 block (data is X) */ + [0x89] = BBL + 7, [0x8a] = BBL + 6, [0x8b] = BBL + 5, [0x8c] = BBL + 4, + [0x8d] = BBL + 3, [0x8e] = BBL + 2, [0x8f] = BBL + 1, + + /* upper 1/2 (4/8), 1/8 block (X), right 1/2, 1/8 block (8-X) */ + [0x80] = BBU + 4, [0x94] = BBU + 1, + [0x90] = BBR + 4, [0x95] = BBR + 7, + + /* Quadrants */ + [0x96] = BBQ + BL, + [0x97] = BBQ + BR, + [0x98] = BBQ + TL, + [0x99] = BBQ + TL + BL + BR, + [0x9a] = BBQ + TL + BR, + [0x9b] = BBQ + TL + TR + BL, + [0x9c] = BBQ + TL + TR + BR, + [0x9d] = BBQ + TR, + [0x9e] = BBQ + BL + TR, + [0x9f] = BBQ + BL + TR + BR, + + /* Shades, data is an alpha value in 25% units (1/4, 1/2, 3/4) */ + [0x91] = BBS + 1, [0x92] = BBS + 2, [0x93] = BBS + 3, + + /* U+2504 - U+250B, U+254C - U+254F: unsupported (dashes) */ + /* U+2571 - U+2573: unsupported (diagonals) */ +}; diff --git a/config.def.h b/config.def.h index 49d3dda..ee892e4 100644 --- a/config.def.h +++ b/config.def.h @@ -8,6 +8,13 @@ static char *font = "FiraCode Nerd Font Mono:pixelsize=18: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; @@ -68,6 +76,18 @@ static unsigned int blinktimeout = 800; static unsigned int cursorthickness = 2; /* + * 1: render most of the lines/blocks characters without using the font for + * perfect alignment between cells (U2500 - U259F except dashes/diagonals). + * Bold affects lines thickness if boxdraw_bold is not 0. Italic is ignored. + * 0: disable (render all U25XX glyphs normally from the font). + */ +const int boxdraw = 1; +const int boxdraw_bold = 1; + +/* braille (U28XX): 1: render as adjacent "pixels", 0: use font */ +const int boxdraw_braille = 0; + +/* * bell volume. It must be a value between -100 and 100. Use 0 for disabling * it */ @@ -167,18 +187,46 @@ 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 * modifier, set to 0 to not use it. */ 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"} }, @@ -186,9 +234,7 @@ static MouseShortcut mshortcuts[] = { { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, }; -/* Internal keyboard shortcuts. */ -#define MODKEY Mod1Mask -#define TERMMOD (ControlMask|ShiftMask) +static const char *writetofilecmd[] = {"/bin/sh", "-c", "cat > $(mktemp /tmp/st-screen.XXXXXX)"}; static Shortcut shortcuts[] = { /* mask keysym function argument */ @@ -196,18 +242,23 @@ static Shortcut shortcuts[] = { { ControlMask, XK_Print, toggleprinter, {.i = 0} }, { ShiftMask, XK_Print, printscreen, {.i = 0} }, { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, - { TERMMOD, XK_plus, zoom, {.f = +1} }, - { TERMMOD, XK_underscore, zoom, {.f = -1} }, + { TERMMOD, XK_Prior, zoom, {.f = +1} }, + { TERMMOD, XK_Next, zoom, {.f = -1} }, { TERMMOD, XK_Home, zoomreset, {.f = 0} }, { TERMMOD, XK_C, clipcopy, {.i = 0} }, { TERMMOD, XK_V, clippaste, {.i = 0} }, { TERMMOD, XK_Y, selpaste, {.i = 0} }, { ShiftMask, XK_Insert, selpaste, {.i = 0} }, { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, - { TERMMOD, XK_Up, kscrollup, {.i = +1} }, - { TERMMOD, XK_Down, kscrolldown, {.i = +1} }, - { TERMMOD, XK_Page_Up, kscrollup, {.i = -1} }, - { TERMMOD, XK_Page_Down, kscrolldown, {.i = -1} }, + { TERMMOD, XK_Up, numlock, {.i = +1} }, + { TERMMOD, XK_Down, numlock, {.i = +1} }, + { ShiftMask, XK_Page_Up, kscrollup, {.i = -1} }, + { ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} }, + { TERMMOD, XK_F1, togglegrdebug, {.i = 0} }, + { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, + { TERMMOD, XK_F7, unloadimages, {.i = 0} }, + { TERMMOD, XK_F8, toggleimages, {.i = 0} }, + { TERMMOD, XK_F9, externalpipe, { .v = writetofilecmd } }, }; /* @@ -14,10 +14,13 @@ PKG_CONFIG = pkg-config # includes and libs INCS = -I$(X11INC) \ + `$(PKG_CONFIG) --cflags imlib2` \ `$(PKG_CONFIG) --cflags fontconfig` \ `$(PKG_CONFIG) --cflags freetype2` \ `$(PKG_CONFIG) --cflags harfbuzz` -LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender\ +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` \ `$(PKG_CONFIG) --libs harfbuzz` @@ -31,8 +34,7 @@ STLDFLAGS = $(LIBS) $(LDFLAGS) #CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE #LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \ # `$(PKG_CONFIG) --libs fontconfig` \ -# `$(PKG_CONFIG) --libs freetype2` \ -# `$(PKG_CONFIG) --libs harfbuzz` +# `$(PKG_CONFIG) --libs freetype2` #MANPREFIX = ${PREFIX}/man # compiler and linker diff --git a/generate-rowcolumn-helpers.py b/generate-rowcolumn-helpers.py new file mode 100755 index 0000000..fd8d48c --- /dev/null +++ b/generate-rowcolumn-helpers.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +# This script generates functions to convert row/column numbers encoded as +# diacritics to actual numbers. +# It reads the file rowcolumn-diacritics.txt from the currend directory and +# produces the following files: +# - rowcolumn_diacritics_helpers.c - contains a helper function to convert from +# diacritics to row/column numbers. +# - rowcolumn_diacritics.sh - contains an array of row/column diacritics (can be +# used by shell scripts to generate image placeholders). +# +# The script also checks some desirable properties of row/column diacritics, +# e.g. that image placeholders are in normal form. + +import unicodedata +import sys + +# codes of all row/column diacritics +codes = [] + +with open("./rowcolumn-diacritics.txt", "r") as file: + for line in file.readlines(): + if line.startswith('#'): + continue + code = int(line.split(";")[0], 16) + char = chr(code) + assert unicodedata.combining(char) == 230 + codes.append(code) + +print("Generating ./rowcolumn_diacritics_helpers.c") +with open("./rowcolumn_diacritics_helpers.c", "w") as file: + range_start_num = 1 + range_start = 0 + range_end = 0 + + def print_range(): + if range_start >= range_end: + return + for code in range(range_start, range_end): + print("\tcase " + hex(code) + ":", file=file) + print("\t\treturn code - " + hex(range_start) + " + " + + str(range_start_num) + ";", + file=file) + + print("#include <stdint.h>\n", file=file) + print("uint16_t diacritic_to_num(uint32_t code)\n{", file=file) + print("\tswitch (code) {", file=file) + + for code in codes: + if range_end == code: + range_end += 1 + else: + print_range() + range_start_num += range_end - range_start + range_start = code + range_end = code + 1 + print_range() + + print("\t}", file=file) + print("\treturn 0;", file=file) + print("}", file=file) + +print("Generating ./rowcolumn_diacritics.sh") +with open("./rowcolumn_diacritics.sh", "w") as file: + print("ROWCOLUMN_DIACRITICS=(", file=file, end="") + for code in codes: + print('"\\U' + format(code, 'x') + '" ', file=file, end="") + print(")", file=file) + +print("Checking that image placeholder cannot be normalized further") + +img_char = chr(0x10EEEE) +for row_code in codes: + row_char = chr(row_code) + for col_code in codes: + col_char = chr(col_code) + cell = img_char + row_char + col_char + for nf in ["NFC", "NFKC", "NFD", "NFKD"]: + if not unicodedata.is_normalized(nf, cell): + print(cell) + print("unnormalized!", nf, [hex(ord(img_char)), hex(row_code), hex(col_code)]) + normalized = unicodedata.normalize(nf, cell) + print("normalized:", [hex(ord(c)) for c in normalized]) + exit(1) + +print("Checking that the row/column marks are not fused with anything " + "letter-like during normalization") + +# Collect somewhat normal characters. +normal_symbols = [] +for i in range(sys.maxunicode): + string = chr(i) + if unicodedata.category(string)[0] not in ['L', 'P', 'N', 'S']: + continue + is_normalized = True + for nf in ["NFC", "NFKC", "NFD", "NFKD"]: + if not unicodedata.is_normalized(nf, string): + is_normalized = False + if is_normalized: + normal_symbols.append(i) + +for code in codes: + print("Checking " + hex(code), end="\r") + for num in normal_symbols: + string = chr(num) + chr(code) + for nf in ["NFC", "NFKC", "NFD", "NFKD"]: + if not unicodedata.is_normalized(nf, string): + normalized = unicodedata.normalize(nf, string) + print("WARNING: " + hex(num) + " + " + hex(code) + + " is normalized to " + normalized, + " ".join(hex(ord(c)) for c in normalized)) diff --git a/graphics.c b/graphics.c new file mode 100644 index 0000000..98e20f4 --- /dev/null +++ b/graphics.c @@ -0,0 +1,4063 @@ +/* The MIT License + + Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com> + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// This file implements a subset of the kitty graphics protocol. +// +//////////////////////////////////////////////////////////////////////////////// + +// A workaround for mac os to enable mkdtemp. +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif + +#define _POSIX_C_SOURCE 200809L + +#include <zlib.h> +#include <Imlib2.h> +#include <X11/Xlib.h> +#include <X11/extensions/Xrender.h> + +#include <assert.h> +#include <ctype.h> +#include <fcntl.h> +#include <spawn.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include <errno.h> + +#include "khash.h" +#include "kvec.h" + +#include "st.h" +#include "graphics.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, + ERROR_CANNOT_OPEN_SHM = 5, +}; + +const char *image_uploading_failure_strings[6] = { + "NO_ERROR", + "ERROR_OVER_SIZE_LIMIT", + "ERROR_CANNOT_OPEN_CACHED_FILE", + "ERROR_UNEXPECTED_SIZE", + "ERROR_CANNOT_COPY_FILE", + "ERROR_CANNOT_OPEN_SHM", +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// 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; + /// The text underneath this placement, valid only for classic + /// placements. On deletion, the text is restored. This is a malloced + /// array of rows*cols Glyphs. + Glyph *text_underneath; +} 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_erase_placement(ImagePlacement *placement); +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 or command execution. +static int debug_loaded_files_counter = 0; +/// The number of pixmaps loaded in the current redraw cycle or command execution. +static int debug_loaded_pixmaps_counter = 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); +} + +/// Returns a pointer to the glyph under the classic placement with `image_id` +/// and `placement_id` at `col` and `row` (1-based). May return NULL if the +/// underneath text is unknown. +Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, + int col, int row) { + ImagePlacement *placement = + gr_find_image_and_placement(image_id, placement_id); + if (!placement || !placement->text_underneath) + return NULL; + col--; + row--; + if (col < 0 || col >= placement->cols || row < 0 || + row >= placement->rows) + return NULL; + return &placement->text_underneath[row * placement->cols + col]; +} + +/// 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, erases it from the +/// screen in the classic case, 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); + // Erase the placement from the screen if it's classic and there is some + // saved text underneath. + if (placement->text_underneath && !placement->virtual) + gr_erase_placement(placement); + gr_unload_placement(placement); + kv_destroy(placement->pixmaps_beyond_the_first); + free(placement->text_underneath); + 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, erases from the +/// screen (in the classic case), 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) { + 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) { + Milliseconds end = gr_now_ms(); + GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " + "img count: %d placement count: %d Took %ld ms\n", + images_ram_size / 1024, images_disk_size / 1024, + kh_size(images), total_placement_count, end - now); + } + 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(); + + if (frame->compression == 0) { + gr_load_raw_pixel_data_uncompressed(data, file, frame->format, + total_pixels); + } else { + int ret = gr_load_raw_pixel_data_compressed( + data, file, frame->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; + } + } + + // We exclude background frames from the time to load the frame. + Milliseconds loading_start = gr_now_ms(); + + // 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_data_image = imlib_load_image(filename); + if (frame->format == 32 || frame->format == 24) + frame_data_image = gr_load_raw_pixel_data(frame, filename); + debug_loaded_files_counter++; + + 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; + + Milliseconds loading_end = gr_now_ms(); + GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB) Took " + "%ld ms\n", + frame->image->image_id, frame->index, images_ram_size / 1024, + gr_frame_current_ram_size(frame) / 1024, + loading_end - loading_start); +} + +/// 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) { + Milliseconds loading_start = gr_now_ms(); + 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 = {0}; + Status visual_found = XMatchVisualInfo(disp, DefaultScreen(disp), 32, + TrueColor, &visinfo) || + XMatchVisualInfo(disp, DefaultScreen(disp), 24, + TrueColor, &visinfo); + if (!visual_found) { + fprintf(stderr, + "error: could not find 32-bit TrueColor visual\n"); + // Proceed anyway. + visinfo.visual = NULL; + } + XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, + (char *)data, scaled_w, scaled_h, 32, 0); + if (!ximage) { + fprintf(stderr, "error: could not create XImage\n"); + imlib_image_put_back_data(data); + imlib_free_image(); + return 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); + debug_loaded_pixmaps_counter++; + + Milliseconds loading_end = gr_now_ms(); + GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " + "KiB) Took %ld ms\n", + frame->image->image_id, placement->placement_id, frame->index, + images_ram_size / 1024, + gr_placement_single_frame_ram_size(placement) / 1024, + loading_end - loading_start); + + // Free up ram if needed, but keep the pixmap we've loaded no matter + // what. + placement->protected_frame = frameidx; + gr_check_limits(); + placement->protected_frame = 0; + + return pixmap; +} + +//////////////////////////////////////////////////////////////////////////////// +// Initialization and deinitialization. +//////////////////////////////////////////////////////////////////////////////// + +/// Creates a temporary directory. +static int gr_create_cache_dir() { + strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); + if (!mkdtemp(cache_dir)) { + fprintf(stderr, + "error: could not create temporary dir from template " + "%s\n", + sanitized_filename(cache_dir)); + return 0; + } + fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); + return 1; +} + +/// Checks whether `tmp_dir` exists and recreates it if it doesn't. +static void gr_make_sure_tmpdir_exists() { + struct stat st; + if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) + return; + fprintf(stderr, + "error: %s is not a directory, will need to create a new " + "graphics cache directory\n", + sanitized_filename(cache_dir)); + gr_create_cache_dir(); +} + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm) { + // Set the initialization time. + clock_gettime(CLOCK_MONOTONIC, &initialization_time); + + // Create the temporary dir. + if (!gr_create_cache_dir()) + abort(); + + // Initialize imlib. + imlib_context_set_display(disp); + imlib_context_set_visual(vis); + imlib_context_set_colormap(cm); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + // Imlib2 checks only the file name when caching, which is not enough + // for us since we reuse file names. Disable caching. + imlib_set_cache_size(0); + + // Prepare for color inversion. + for (size_t i = 0; i < 256; ++i) + reverse_table[i] = 255 - i; + + // Create data structures. + images = kh_init(id2image); + kv_init(next_redraw_times); + + atexit(gr_deinit); +} + +/// Deinitialize the graphics module. +void gr_deinit() { + // Remove the cache dir. + remove(cache_dir); + kv_destroy(next_redraw_times); + if (images) { + // Delete all images. + gr_delete_all_images(); + // Destroy the data structures. + kh_destroy(id2image, images); + images = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Dumping, debugging, and image preview. +//////////////////////////////////////////////////////////////////////////////// + +/// Returns a string containing a time difference in a human-readable format. +/// Uses a static buffer, so be careful. +static const char *gr_ago(Milliseconds diff) { + static char result[32]; + double seconds = (double)diff / 1000.0; + if (seconds < 1) + snprintf(result, sizeof(result), "%.2f sec ago", seconds); + else if (seconds < 60) + snprintf(result, sizeof(result), "%d sec ago", (int)seconds); + else if (seconds < 3600) + snprintf(result, sizeof(result), "%d min %d sec ago", + (int)(seconds / 60), (int)(seconds) % 60); + else { + snprintf(result, sizeof(result), "%d hr %d min %d sec ago", + (int)(seconds / 3600), (int)(seconds) % 3600 / 60, + (int)(seconds) % 60); + } + return result; +} + +/// Prints to `file` with an indentation of `ind` spaces. +static void fprintf_ind(FILE *file, int ind, const char *format, ...) { + fprintf(file, "%*s", ind, ""); + va_list args; + va_start(args, format); + vfprintf(file, format, args); + va_end(args); +} + +/// Dumps the image info to `file` with an indentation of `ind` spaces. +static void gr_dump_image_info(FILE *file, Image *img, int ind) { + if (!img) { + fprintf_ind(file, ind, "Image is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Image %u\n", img->image_id); + ind += 4; + fprintf_ind(file, ind, "number: %u\n", img->image_number); + fprintf_ind(file, ind, "global command index: %lu\n", + img->global_command_index); + fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, + gr_ago(now - img->atime)); + fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, + img->pix_height); + fprintf_ind(file, ind, "cur frame start time: %ld %s\n", + img->current_frame_time, + gr_ago(now - img->current_frame_time)); + if (img->next_redraw) + fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", + img->next_redraw, img->next_redraw - now); + fprintf_ind(file, ind, "total disk size: %u KiB\n", + img->total_disk_size / 1024); + fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); + fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); + fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); + fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); + fprintf_ind(file, ind, "default_placement: %u\n", + img->default_placement); +} + +/// Dumps the frame info to `file` with an indentation of `ind` spaces. +static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { + if (!frame) { + fprintf_ind(file, ind, "Frame is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Frame %d\n", frame->index); + ind += 4; + if (frame->index == 0) { + fprintf_ind(file, ind, "NOT INITIALIZED\n"); + return; + } + if (frame->uploading_failure) + fprintf_ind(file, ind, "uploading failure: %s\n", + image_uploading_failure_strings + [frame->uploading_failure]); + fprintf_ind(file, ind, "gap: %d\n", frame->gap); + fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, + gr_ago(now - frame->atime)); + fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, + frame->data_pix_height); + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (access(filename, F_OK) != -1) + fprintf_ind(file, ind, "file: %s\n", + sanitized_filename(filename)); + else + fprintf_ind(file, ind, "not on disk\n"); + fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); + if (frame->imlib_object) { + unsigned ram_size = gr_frame_current_ram_size(frame); + fprintf_ind(file, ind, + "loaded into ram, size: %d " + "KiB\n", + ram_size / 1024); + } else { + fprintf_ind(file, ind, "not loaded into ram\n"); + } +} + +/// Dumps the placement info to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) { + fprintf_ind(file, ind, "Placement is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); + ind += 4; + fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, + gr_ago(now - placement->atime)); + fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); + fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, + placement->rows); + fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, + placement->scaled_ch); + fprintf_ind(file, ind, "ram per frame: %u KiB\n", + gr_placement_single_frame_ram_size(placement) / 1024); + unsigned ram_size = gr_placement_current_ram_size(placement); + fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); +} + +/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) + return; + int frameidx = 1; + foreach_pixmap(*placement, pixmap, { + fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, + pixmap); + ++frameidx; + }); +} + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state() { + FILE *file = stderr; + int ind = 0; + fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); + fprintf_ind(file, ind, + "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " + "sizeof(ImagePlacement) = %lu\n", + sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); + fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); + fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); + fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", + images_ram_size / 1024); + fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", + images_disk_size / 1024); + + Milliseconds now = gr_now_ms(); + + int64_t images_ram_size_computed = 0; + int64_t images_disk_size_computed = 0; + + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + fprintf_ind(file, ind, "----------------\n"); + gr_dump_image_info(file, img, 0); + int64_t total_disk_size_computed = 0; + int total_duration_computed = 0; + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + if (frame->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + total_duration_computed += frame->gap; + images_disk_size_computed += frame->disk_size; + total_disk_size_computed += frame->disk_size; + if (frame->imlib_object) + images_ram_size_computed += + gr_frame_current_ram_size(frame); + }); + if (img->total_disk_size != total_disk_size_computed) { + fprintf_ind(file, ind, + " ERROR: total_disk_size is %u, but " + "computed value is %ld\n", + img->total_disk_size, total_disk_size_computed); + } + if (img->total_duration != total_duration_computed) { + fprintf_ind(file, ind, + " ERROR: total_duration is %d, but computed " + "value is %d\n", + img->total_duration, total_duration_computed); + } + kh_foreach_value(img->placements, placement, { + gr_dump_placement_info(file, placement, 4); + if (placement->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + fprintf_ind(file, 8, + "Pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 12); + unsigned ram_size = + gr_placement_current_ram_size(placement); + images_ram_size_computed += ram_size; + }); + }); + if (images_ram_size != images_ram_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_ram_size is %ld, but computed value " + "is %ld\n", + images_ram_size, images_ram_size_computed); + } + if (images_disk_size != images_disk_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_disk_size is %ld, but computed value " + "is %ld\n", + images_disk_size, images_disk_size_computed); + } + fprintf_ind(file, ind, "===========================================\n"); +} + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +// TODO: Currently we do this for the first frame only. Not sure what to do with +// animations. +void gr_preview_image(uint32_t image_id, const char *exec) { + char command[256]; + size_t len; + Image *img = gr_find_image(image_id); + if (img) { + ImageFrame *frame = &img->first_frame; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (frame->disk_size == 0) { + len = snprintf(command, 255, + "xmessage 'Image with id=%u is not " + "fully copied to %s'", + image_id, sanitized_filename(filename)); + } else { + len = snprintf(command, 255, "%s %s &", exec, + sanitized_filename(filename)); + } + } else { + len = snprintf(command, 255, + "xmessage 'Cannot find image with id=%u'", + image_id); + } + if (len > 255) { + fprintf(stderr, "error: command too long: %s\n", command); + snprintf(command, 255, "xmessage 'error: command too long'"); + } + if (system(command) != 0) { + fprintf(stderr, "error: could not execute command %s\n", + command); + } +} + +/// Executes `<st> -e less <file>` where <file> is the name of a temporary file +/// containing the information about an image and placement, and <st> is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable) { + char filename[MAX_FILENAME_SIZE]; + snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); + FILE *file = fopen(filename, "w"); + if (!file) { + perror("fopen"); + return; + } + // Basic information about the cell. + fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); + fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); + fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); + fprintf(file, "classic/unicode placeholder = %s\n", + is_classic_placeholder ? "classic" : "unicode"); + fprintf(file, "original diacritic count = %d\n", diacritic_count); + // Information about the image and the placement. + Image *img = gr_find_image(image_id); + ImagePlacement *placement = gr_find_placement(img, placement_id); + gr_dump_image_info(file, img, 0); + gr_dump_placement_info(file, placement, 0); + // The text underneath this particular cell. + if (placement && placement->text_underneath && imgcol >= 1 && + imgrow >= 1 && imgcol <= placement->cols && + imgrow <= placement->rows) { + fprintf(file, "Glyph underneath:\n"); + Glyph *glyph = + &placement->text_underneath[(imgrow - 1) * + placement->cols + + imgcol - 1]; + fprintf(file, " rune = 0x%08X\n", glyph->u); + fprintf(file, " bg = 0x%08X\n", glyph->bg); + fprintf(file, " fg = 0x%08X\n", glyph->fg); + fprintf(file, " decor = 0x%08X\n", glyph->decor); + fprintf(file, " mode = 0x%08X\n", glyph->mode); + } + 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; + debug_loaded_files_counter = 0; + debug_loaded_pixmaps_counter = 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, debug_loaded_files_counter, + debug_loaded_pixmaps_counter); + } + } + + // 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 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; + /// The offset of the frame image data in the shared memory ('O='). + unsigned offset; + /// '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; + placement->text_underneath = + calloc(placement->rows * placement->cols, sizeof(Glyph)); + graphics_command_result.placeholder.text_underneath = + placement->text_underneath; + 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` to the on-disk cache file of the frame `frame`. Creates the +/// file if it doesn't exist. Updates `frame->disk_size` and the total disk +/// size. Returns 1 on success and 0 on failure. +static int gr_append_raw_data_to_file(ImageFrame *frame, const char *data, + size_t data_size) { + // 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) + return 0; + frame->open_file = file; + } + + // Write data to the file and update disk size variables. + fwrite(data, 1, data_size, frame->open_file); + frame->disk_size += data_size; + frame->image->total_disk_size += data_size; + images_disk_size += data_size; + gr_touch_frame(frame); + return 1; +} + +/// 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; + } + + // Append the data to the file. + if (!gr_append_raw_data_to_file(frame, data, data_size)) { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; + if (!more) + gr_reportuploaderror(frame); + return; + } + free(data); + + 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. +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); + 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) { + cmd->image_id = img->image_id; + } else { + 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; + // The default format is 32. + frame->format = cmd->format ? cmd->format : 32; + 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; + } + // If the expected size is not specified, we can infer it from the pixel + // width and height if the format is 24 or 32 and there is no + // compression. This is required for the shared memory transmission. + if (!frame->expected_size && !frame->compression && + (frame->format == 24 || frame->format == 32)) { + frame->expected_size = frame->data_pix_width * + frame->data_pix_height * + (frame->format / 8); + } + // 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; + cmd->image_id = frame->image->image_id; + gr_append_data(frame, cmd->payload, cmd->more); + return frame; + } + // 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 if (cmd->transmission_medium == 's') { + // Shared memory 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; + // Check that we know the size. + if (!frame->expected_size) { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + gr_reporterror_cmd( + cmd, "EINVAL: the size of the image is not " + "specified and cannot be inferred"); + return frame; + } + // Check the data size limit. + if (frame->expected_size > graphics_max_single_image_file_size) { + frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; + gr_reportuploaderror(frame); + return frame; + } + // Decode the filename. + char *original_filename = gr_base64dec(cmd->payload, NULL); + GR_LOG("Loading image from shared memory %s\n", + sanitized_filename(original_filename)); + // Open the shared memory object. + int fd = shm_open(original_filename, O_RDONLY, 0); + if (fd == -1) { + gr_reporterror_cmd(cmd, "EBADF: shm_open: %s", + strerror(errno)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; + fprintf(stderr, "shm_open failed for %s\n", + sanitized_filename(original_filename)); + shm_unlink(original_filename); + free(original_filename); + return frame; + } + shm_unlink(original_filename); + free(original_filename); + // The offset we pass to mmap must be a multiple of the page + // size. If it's not, adjust it and the size. + size_t page_size = sysconf(_SC_PAGESIZE); + if (page_size == -1) + page_size = 1; + size_t offset = cmd->offset - (cmd->offset % page_size); + size_t size = frame->expected_size + (cmd->offset - offset); + // Map the shared memory object. + void *data = + mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset); + if (data == MAP_FAILED) { + gr_reporterror_cmd(cmd, "EBADF: mmap: %s", + strerror(errno)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; + fprintf(stderr, + "mmap failed for size = %ld, offset = %ld\n", + size, offset); + close(fd); + return frame; + } + close(fd); + // Append the data to the cache file. + if (gr_append_raw_data_to_file(frame, + data + (cmd->offset - offset), + frame->expected_size)) { + frame->status = STATUS_UPLOADING_SUCCESS; + } else { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = + ERROR_CANNOT_OPEN_CACHED_FILE; + gr_reportuploaderror(frame); + } + // Close the cache file. + if (frame->open_file) { + fclose(frame->open_file); + frame->open_file = NULL; + } + // Unmap the data + if (munmap(data, size) != 0) + fprintf(stderr, "munmap failed: %s\n", strerror(errno)); + // Try to load and redraw existing instances. + gr_schedule_image_redraw(frame->image); + frame = gr_loadimage_and_report(frame); + gr_check_limits(); + } 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) { + cmd->image_id = img->image_id; + } else { + 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; + /// Visible placement found during screen traversal that need to be + /// deleted. Each placement must occur only once in this vector. + ImagePlacementVec placements_to_delete; +} DeletionData; + +/// The callback called for each cell to perform deletion. +static int gr_deletion_callback(void *data, Glyph *gp) { + DeletionData *del_data = data; + // Leave unicode placeholders alone. + if (!tgetisclassicplaceholder(gp)) + return 0; + uint32_t image_id = tgetimgid(gp); + uint32_t placement_id = tgetimgplacementid(gp); + 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; + + ImagePlacement *placement = NULL; + + // Record the placement to delete. We will actually delete it later. + for (int i = 0; i < kv_size(del_data->placements_to_delete); ++i) { + ImagePlacement *cand = kv_A(del_data->placements_to_delete, i); + if (cand->image->image_id == image_id && + cand->placement_id == placement_id) { + placement = cand; + break; + } + } + if (!placement) { + placement = gr_find_image_and_placement(image_id, placement_id); + if (placement) { + kv_push(ImagePlacement *, + del_data->placements_to_delete, placement); + } + } + + // Restore the text underneath the placement if possible. + if (placement && placement->text_underneath) { + int row = tgetimgrow(gp) - 1; + int col = tgetimgcol(gp) - 1; + if (col >= 0 && row >= 0 && row < placement->rows && + col < placement->cols) { + *gp = placement->text_underneath[row * placement->cols + + col]; + return 1; + } + } + + // Otherwise just erase the cell. + gp->mode = 0; + gp->u = ' '; + return 1; +} + +/// Handles the delete command. +static void gr_handle_delete_command(GraphicsCommand *cmd) { + DeletionData del_data = {0}; + char 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; + } + + kv_init(del_data.placements_to_delete); + + 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"); + kv_destroy(del_data.placements_to_delete); + return; + } + del_data.placement_id = cmd->placement_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); + } + + // Delete the placements we have collected and maybe images too. + for (int i = 0; i < kv_size(del_data.placements_to_delete); ++i) { + ImagePlacement *placement = + kv_A(del_data.placements_to_delete, i); + // Delete the text underneath the placement and set it to NULL + // to avoid erasing it from the screen again. + free(placement->text_underneath); + placement->text_underneath = NULL; + Image *img = placement->image; + gr_delete_placement(placement); + // Delete the image if image deletion is requested (uppercase + // delete specifier) and there are no more placements. + if (delete_image_if_no_ref && kh_size(img->placements) == 0) + gr_delete_image(img); + } + + // 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 (d == 'i' && !del_data.placement_id && delete_image_if_no_ref) + gr_delete_image(gr_find_image(cmd->image_id)); + + kv_destroy(del_data.placements_to_delete); +} + +/// Clears the cells occupied by the placement. This is normally done when +/// implicitly deleting a classic placement. +static void gr_erase_placement(ImagePlacement *placement) { + DeletionData del_data = {0}; + del_data.image_id = placement->image->image_id; + del_data.placement_id = placement->placement_id; + kv_init(del_data.placements_to_delete); + gr_for_each_image_cell(gr_deletion_callback, &del_data); + // Delete the text underneath the placement and set it to NULL + // to avoid erasing it from the screen again. + free(placement->text_underneath); + placement->text_underneath = NULL; + kv_destroy(del_data.placements_to_delete); +} + +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) { + cmd->image_id = img->image_id; + } else { + 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 is data transmission. + 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->more = num; + break; + case 'S': + cmd->size = num; + break; + case 'O': + cmd->offset = 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; + + Milliseconds command_start_time = gr_now_ms(); + debug_loaded_files_counter = 0; + debug_loaded_pixmaps_counter = 0; + global_command_counter++; + GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); + + memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); + + // 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'; + } + + Milliseconds command_end_time = gr_now_ms(); + GR_LOG("Command %lu took %ld ms (loaded %d files, %d pixmaps)\n\n", + global_command_counter, command_end_time - command_start_time, + debug_loaded_files_counter, debug_loaded_pixmaps_counter); + + 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..f9a649a --- /dev/null +++ b/graphics.h @@ -0,0 +1,112 @@ + +#include <stdint.h> +#include <sys/types.h> +#include <X11/Xlib.h> + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm); +/// Deinitialize the graphics module. +void gr_deinit(); + +/// Add an image rectangle to a list if rectangles to draw. This function may +/// actually draw some rectangles, or it may wait till more rectangles are +/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. +/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the +/// part of the image to draw (row/col indices are zero-based, ends are +/// excluded). +/// - `x_col` and `y_row` are the coordinates of the top-left corner of the +/// image in the terminal grid. +/// - `x_pix` and `y_pix` are the same but in pixels. +/// - `reverse` indicates whether colors should be inverted. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse); +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch); +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf); +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows); + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters (including '\0'). Returns 1 on success. +/// Additional informations is returned through `graphics_command_result`. +int gr_parse_command(char *buf, size_t len); + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +void gr_preview_image(uint32_t image_id, const char *command); + +/// Executes `<st> -e less <file>` where <file> is the name of a temporary file +/// containing the information about an image and placement, and <st> is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable); + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state(); + +/// Unloads images to reduce RAM usage. +void gr_unload_images_to_reduce_ram(); + +/// Executes `callback` for each image cell. The callback should return 1 if it +/// changed the glyph. This function is implemented in `st.c`. +void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp), + 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); + +/// Returns a pointer to the glyph under the classic placement with `image_id` +/// and `placement_id` at `col` and `row` (1-based). May return NULL if the +/// underneath text is unknown. +Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, + int col, int row); + +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; + Glyph *text_underneath; + } placeholder; +} GraphicsCommandResult; + +/// The result of a graphics command. +extern GraphicsCommandResult graphics_command_result; @@ -102,6 +102,9 @@ void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int mode = glyphs[glyph_idx].mode; if (mode & ATTR_WDUMMY) hbrunebuffer.runes[rune_idx] = 0x0020; + /* Draw spaces for image placeholders. */ + if (mode & ATTR_IMAGE) + hbrunebuffer.runes[rune_idx] = 0x0020; } hb_buffer_add_codepoints(buffer, hbrunebuffer.runes, length, 0, length); diff --git a/icat-mini.sh b/icat-mini.sh new file mode 100755 index 0000000..8f240e9 --- /dev/null +++ b/icat-mini.sh @@ -0,0 +1,875 @@ +#!/bin/sh + +# vim: shiftwidth=4 + +script_name="$(basename "$0")" + +short_help="Usage: $script_name [OPTIONS] <image_file> + +This is a script to display images in the terminal using the kitty graphics +protocol with Unicode placeholders. It is very basic, please use something else +if you have alternatives. + +Options: + -h Show this help. + -s SCALE The scale of the image, may be floating point. + -c N, --cols N The number of columns. + -r N, --rows N The number of rows. + --max-cols N The maximum number of columns. + --max-rows N The maximum number of rows. + --cell-size WxH The cell size in pixels. + -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. + --speed SPEED The multiplier for the animation speed (float). +" + +# Exit the script on keyboard interrupt +trap "echo 'icat-mini was interrupted' >&2; exit 1" INT + +cols="" +rows="" +file="" +command_tty="" +response_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")" + +##################################################################### +# 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 + inside_tmux=1 +fi + +if [ -z "$command_tty" ] && [ -n "$inside_tmux" ]; then + # Get the pty of the current tmux pane. + command_tty="$(tmux display-message -t "$TMUX_PANE" -p "#{pane_tty}")" + if [ ! -e "$command_tty" ]; then + command_tty="" + fi +fi + +##################################################################### +# Adjust the terminal state +##################################################################### + +if [ -z "$command_tty" ]; then + command_tty="/dev/tty" +fi +if [ -z "$response_tty" ]; then + response_tty="/dev/tty" +fi + +stty_orig="$(stty -g < "$response_tty")" +stty -echo < "$response_tty" +# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some +# horrible issues otherwise. +stty susp undef < "$response_tty" +stty -icanon < "$response_tty" + +restore_echo() { + [ -n "$stty_orig" ] || return + stty $stty_orig < "$response_tty" +} + +trap restore_echo EXIT TERM + +##################################################################### +# 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" < "$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\\' >> "$command_tty" + else + printf '\e[16t' >> "$command_tty" + fi + # The expected response will look like ^[[6;<height>;<width>t + term_response="" + while true; do + char=$(dd bs=1 count=1 <"$response_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 + +# Send a graphics command with the correct start and end +gr_command() { + printf "${graphics_command_start}%s${graphics_command_end}" "$1" >> "$command_tty" +} + +# Compute the size of a data chunk for direct transmission. +if [ "$uploading_method" = "direct" ]; then + # Get the value of PIPE_BUF. + pipe_buf="$(getconf PIPE_BUF "$command_tty" 2> /dev/null)" + if is_pos_int "$pipe_buf"; then + # Make sure it's between 512 and 4096. + if [ "$(expr "$pipe_buf" \< 512)" -eq 1 ]; then + pipe_buf=512 + elif [ "$(expr "$pipe_buf" \> 4096)" -eq 1 ]; then + pipe_buf=4096 + fi + else + pipe_buf=512 + fi + + # The size of each graphics command shouldn't be more than PIPE_BUF, so we + # set the size of an encoded chunk to be PIPE_BUF - 128 to leave some space + # for the command. + chunk_size="$(expr "$pipe_buf" - 128)" +fi + +# Check if the image format is supported. +is_format_supported() { + arg_format="$1" + if [ "$arg_format" = "PNG" ]; then + return 0 + elif [ "$arg_format" = "JPEG" ]; then + if [ -z "$inside_tmux" ]; then + actual_term="$TERM" + else + # Get the actual current terminal name from tmux. + actual_term="$(tmux display-message -p "#{client_termname}")" + fi + # st is known to support JPEG. + case "$actual_term" in + st | *-st | st-* | *-st-*) + return 0 + ;; + esac + return 1 + else + return 1 + fi +} + +# Send an uploading command. Usage: gr_upload <action> <command> <file> +# Where <action> is a part of command that specifies the action, it will be +# repeated for every chunk (if the method is direct), and <command> is the rest +# of the command that specifies the image parameters. <action> and <command> +# must not include the transmission method or ';'. +# Example: +# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" +gr_upload() { + arg_action="$1" + arg_command="$2" + arg_file="$3" + if [ "$uploading_method" = "file" ]; then + # base64-encode the filename + encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) + # If the file name contains 'tty-graphics-protocol', assume it's + # temporary and use t=t. + medium="t=f" + case "$arg_file" in + *tty-graphics-protocol*) + medium="t=t" + ;; + *) + medium="t=f" + ;; + esac + gr_command "${arg_action},${arg_command},${medium};${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 + gr_command "${arg_action},i=${image_id},m=1;$(cat "$chunk")" + 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. + format_output=$($identify -format '%n %m\n' "$file" | head -n 1) + frame_count="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" + image_format="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" + + 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 + # Abort the previous transmission, just in case. + gr_command "q=2,a=t,i=${image_id},m=0" + # 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 & + elif is_format_supported "$image_format"; then + # The file is not an animation and has a supported format, 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 + else + # The format is not supported, try to convert it to png. + temp_file="$(mktemp --tmpdir "icat-mini-tty-graphics-protocol-XXXXX.png")" + if ! $convert "$file" "$temp_file"; then + echo "Failed to convert the image to PNG" >&2 + exit 1 + fi + # Upload the converted image. + gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$temp_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}" + line="$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 + line="${line}${placeholder}${row_diacritic}" + else + line="${line}${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" + fi + done + line="${line}${line_end}" + printf "%s\n" "$line" + 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 @@ -0,0 +1,627 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk> + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +/* compiler specific configuration */ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash set containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ @@ -0,0 +1,90 @@ +/* The MIT License + + Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk> + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "kvec.h" +int main() { + kvec_t(int) array; + kv_init(array); + kv_push(int, array, 10); // append + kv_a(int, array, 20) = 5; // dynamic + kv_A(array, 20) = 4; // static + kv_destroy(array); + return 0; +} +*/ + +/* + 2008-09-22 (0.1.0): + + * The initial version. + +*/ + +#ifndef AC_KVEC_H +#define AC_KVEC_H + +#include <stdlib.h> + +#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) + +#define kvec_t(type) struct { size_t n, m; type *a; } +#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) +#define kv_destroy(v) free((v).a) +#define kv_A(v, i) ((v).a[(i)]) +#define kv_pop(v) ((v).a[--(v).n]) +#define kv_size(v) ((v).n) +#define kv_max(v) ((v).m) + +#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) + +#define kv_copy(type, v1, v0) do { \ + if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ + (v1).n = (v0).n; \ + memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ + } while (0) \ + +#define kv_push(type, v, x) do { \ + if ((v).n == (v).m) { \ + (v).m = (v).m? (v).m<<1 : 2; \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ + } \ + (v).a[(v).n++] = (x); \ + } while (0) + +#define kv_pushp(type, v) ((((v).n == (v).m)? \ + ((v).m = ((v).m? (v).m<<1 : 2), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : 0), ((v).a + ((v).n++))) + +#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ + ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ + : 0), (v).a[(i)]) + +#endif diff --git a/patches/st-kitty-graphics-20240922-a0274bc.diff b/patches/st-kitty-graphics-20240922-a0274bc.diff new file mode 100644 index 0000000..6049fe2 --- /dev/null +++ b/patches/st-kitty-graphics-20240922-a0274bc.diff @@ -0,0 +1,7324 @@ +From 25d9cca81ce48141de7f6a823b006dddaafd9de8 Mon Sep 17 00:00:00 2001 +From: Sergei Grechanik <sergei.grechanik@gmail.com> +Date: Sun, 22 Sep 2024 08:36:05 -0700 +Subject: [PATCH] Kitty graphics protocol support 7b717e3 2024-09-22 + +This patch implements the kitty graphics protocol in st. +See https://github.com/sergei-grechanik/st-graphics +Created by squashing the graphics branch, the most recent +commit is 7b717e38b1739e11356b34df9fdfdfa339960864 (2024-09-22). + +Squashed on top of a0274bc20e11d8672bb2953fdd1d3010c0e708c5 + +Note that the following files were excluded from the squash: + .clang-format + README.md + generate-rowcolumn-helpers.py + rowcolumn-diacritics.txt + rowcolumn_diacritics.sh +--- + Makefile | 4 +- + config.def.h | 46 +- + config.mk | 5 +- + graphics.c | 3812 ++++++++++++++++++++++++++++++++ + graphics.h | 107 + + icat-mini.sh | 801 +++++++ + khash.h | 627 ++++++ + kvec.h | 90 + + rowcolumn_diacritics_helpers.c | 391 ++++ + st.c | 279 ++- + st.h | 84 +- + st.info | 6 + + win.h | 3 + + x.c | 411 +++- + 14 files changed, 6615 insertions(+), 51 deletions(-) + create mode 100644 graphics.c + create mode 100644 graphics.h + create mode 100755 icat-mini.sh + create mode 100644 khash.h + create mode 100644 kvec.h + create mode 100644 rowcolumn_diacritics_helpers.c + +diff --git a/Makefile b/Makefile +index 15db421..e79b89e 100644 +--- a/Makefile ++++ b/Makefile +@@ -4,7 +4,7 @@ + + include config.mk + +-SRC = st.c x.c ++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c + OBJ = $(SRC:.c=.o) + + all: st +@@ -16,7 +16,7 @@ config.h: + $(CC) $(STCFLAGS) -c $< + + st.o: config.h st.h win.h +-x.o: arg.h config.h st.h win.h ++x.o: arg.h config.h st.h win.h graphics.h + + $(OBJ): config.h config.mk + +diff --git a/config.def.h b/config.def.h +index 2cd740a..4aadbbc 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -8,6 +8,13 @@ + static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; + static int borderpx = 2; + ++/* How to align the content in the window when the size of the terminal ++ * doesn't perfectly match the size of the window. The values are percentages. ++ * 50 means center, 0 means flush left/top, 100 means flush right/bottom. ++ */ ++static int anysize_halign = 50; ++static int anysize_valign = 50; ++ + /* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e +@@ -23,7 +30,8 @@ char *scroll = NULL; + char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + + /* identification sequence returned in DA and DECID */ +-char *vtiden = "\033[?6c"; ++/* By default, use the same one as kitty. */ ++char *vtiden = "\033[?62c"; + + /* Kerning / character bounding-box multipliers */ + static float cwscale = 1.0; +@@ -163,6 +171,28 @@ static unsigned int mousebg = 0; + */ + static unsigned int defaultattr = 11; + ++/* ++ * Graphics configuration ++ */ ++ ++/// The template for the cache directory. ++const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX"; ++/// The max size of a single image file, in bytes. ++unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024; ++/// The max size of the cache, in bytes. ++unsigned graphics_total_file_cache_size = 300 * 1024 * 1024; ++/// The max ram size of an image or placement, in bytes. ++unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024; ++/// The max total size of all images loaded into RAM. ++unsigned graphics_max_total_ram_size = 300 * 1024 * 1024; ++/// The max total number of image placements and images. ++unsigned graphics_max_total_placements = 4096; ++/// The ratio by which limits can be exceeded. This is to reduce the frequency ++/// of image removal. ++double graphics_excess_tolerance_ratio = 0.05; ++/// The minimum delay between redraws caused by animations, in milliseconds. ++unsigned graphics_animation_min_delay = 20; ++ + /* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other +@@ -170,12 +200,18 @@ static unsigned int defaultattr = 11; + */ + static uint forcemousemod = ShiftMask; + ++/* Internal keyboard shortcuts. */ ++#define MODKEY Mod1Mask ++#define TERMMOD (ControlMask|ShiftMask) ++ + /* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ + static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ ++ { TERMMOD, Button3, previewimage, {.s = "feh"} }, ++ { TERMMOD, Button2, showimageinfo, {}, 1 }, + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, +@@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = { + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, + }; + +-/* Internal keyboard shortcuts. */ +-#define MODKEY Mod1Mask +-#define TERMMOD (ControlMask|ShiftMask) +- + static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, +@@ -201,6 +233,10 @@ static Shortcut shortcuts[] = { + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, ++ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} }, ++ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, ++ { TERMMOD, XK_F7, unloadimages, {.i = 0} }, ++ { TERMMOD, XK_F8, toggleimages, {.i = 0} }, + }; + + /* +diff --git a/config.mk b/config.mk +index fdc29a7..cb2875c 100644 +--- a/config.mk ++++ b/config.mk +@@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config + + # includes and libs + INCS = -I$(X11INC) \ ++ `$(PKG_CONFIG) --cflags imlib2` \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +-LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ ++LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \ ++ `$(PKG_CONFIG) --libs imlib2` \ ++ `$(PKG_CONFIG) --libs zlib` \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +diff --git a/graphics.c b/graphics.c +new file mode 100644 +index 0000000..64e6fe0 +--- /dev/null ++++ b/graphics.c +@@ -0,0 +1,3812 @@ ++/* The MIT License ++ ++ Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com> ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// This file implements a subset of the kitty graphics protocol. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define _POSIX_C_SOURCE 200809L ++ ++#include <zlib.h> ++#include <Imlib2.h> ++#include <X11/Xlib.h> ++#include <X11/extensions/Xrender.h> ++#include <assert.h> ++#include <ctype.h> ++#include <spawn.h> ++#include <stdarg.h> ++#include <stdio.h> ++#include <stdlib.h> ++#include <string.h> ++#include <sys/stat.h> ++#include <time.h> ++#include <unistd.h> ++#include <errno.h> ++ ++#include "graphics.h" ++#include "khash.h" ++#include "kvec.h" ++ ++extern char **environ; ++ ++#define MAX_FILENAME_SIZE 256 ++#define MAX_INFO_LEN 256 ++#define MAX_IMAGE_RECTS 20 ++ ++/// The type used in this file to represent time. Used both for time differences ++/// and absolute times (as milliseconds since an arbitrary point in time, see ++/// `initialization_time`). ++typedef int64_t Milliseconds; ++ ++enum ScaleMode { ++ SCALE_MODE_UNSET = 0, ++ /// Stretch or shrink the image to fill the box, ignoring aspect ratio. ++ SCALE_MODE_FILL = 1, ++ /// Preserve aspect ratio and fit to width or to height so that the ++ /// whole image is visible. ++ SCALE_MODE_CONTAIN = 2, ++ /// Do not scale. The image may be cropped if the box is too small. ++ SCALE_MODE_NONE = 3, ++ /// Do not scale, unless the box is too small, in which case the image ++ /// will be shrunk like with `SCALE_MODE_CONTAIN`. ++ SCALE_MODE_NONE_OR_CONTAIN = 4, ++}; ++ ++enum AnimationState { ++ ANIMATION_STATE_UNSET = 0, ++ /// The animation is stopped. Display the current frame, but don't ++ /// advance to the next one. ++ ANIMATION_STATE_STOPPED = 1, ++ /// Run the animation to then end, then wait for the next frame. ++ ANIMATION_STATE_LOADING = 2, ++ /// Run the animation in a loop. ++ ANIMATION_STATE_LOOPING = 3, ++}; ++ ++/// The status of an image. Each image uploaded to the terminal is cached on ++/// disk, then it is loaded to ram when needed. ++enum ImageStatus { ++ STATUS_UNINITIALIZED = 0, ++ STATUS_UPLOADING = 1, ++ STATUS_UPLOADING_ERROR = 2, ++ STATUS_UPLOADING_SUCCESS = 3, ++ STATUS_RAM_LOADING_ERROR = 4, ++ STATUS_RAM_LOADING_IN_PROGRESS = 5, ++ STATUS_RAM_LOADING_SUCCESS = 6, ++}; ++ ++const char *image_status_strings[6] = { ++ "STATUS_UNINITIALIZED", ++ "STATUS_UPLOADING", ++ "STATUS_UPLOADING_ERROR", ++ "STATUS_UPLOADING_SUCCESS", ++ "STATUS_RAM_LOADING_ERROR", ++ "STATUS_RAM_LOADING_SUCCESS", ++}; ++ ++enum ImageUploadingFailure { ++ ERROR_OVER_SIZE_LIMIT = 1, ++ ERROR_CANNOT_OPEN_CACHED_FILE = 2, ++ ERROR_UNEXPECTED_SIZE = 3, ++ ERROR_CANNOT_COPY_FILE = 4, ++}; ++ ++const char *image_uploading_failure_strings[5] = { ++ "NO_ERROR", ++ "ERROR_OVER_SIZE_LIMIT", ++ "ERROR_CANNOT_OPEN_CACHED_FILE", ++ "ERROR_UNEXPECTED_SIZE", ++ "ERROR_CANNOT_COPY_FILE", ++}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// We use the following structures to represent images and placements: ++// ++// - Image: this is the main structure representing an image, usually created ++// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id, ++// specified by 'i='). An image may have multiple frames (ImageFrame) and ++// placements (ImagePlacement). ++// ++// - ImageFrame: represents a single frame of an image, usually created by ++// the action 'a=f' (and the first frame is created with the image itself). ++// Each frame has an index and also: ++// - a file containing the frame data (considered to be "on disk", although ++// it's probably in tmpfs), ++// - an imlib object containing the fully composed frame (i.e. the frame ++// data from the file composed onto the background frame or color). It is ++// not ready for display yet, because it needs to be scaled and uploaded ++// to the X server. ++// ++// - ImagePlacement: represents a placement of an image, created by 'a=p' and ++// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also ++// each placement has an array of pixmaps: one for each frame of the image. ++// Each pixmap is a scaled and uploaded image ready to be displayed. ++// ++// Images are store in the `images` hash table, mapping image ids to Image ++// objects (allocated on the heap). ++// ++// Placements are stored in the `placements` hash table of each Image object, ++// mapping placement ids to ImagePlacement objects (also allocated on the heap). ++// ++// ImageFrames are stored in the `first_frame` field and in the ++// `frames_beyond_the_first` array of each Image object. They are stored by ++// value, so ImageFrame pointer may be invalidated when frames are ++// added/deleted, be careful. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++struct Image; ++struct ImageFrame; ++struct ImagePlacement; ++ ++KHASH_MAP_INIT_INT(id2image, struct Image *) ++KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *) ++ ++typedef struct ImageFrame { ++ /// The image this frame belongs to. ++ struct Image *image; ++ /// The 1-based index of the frame. Zero if the frame isn't initialized. ++ int index; ++ /// The last time when the frame was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The background color of the frame in the 0xRRGGBBAA format. ++ uint32_t background_color; ++ /// The index of the background frame. Zero to use the color instead. ++ int background_frame_index; ++ /// The duration of the frame in milliseconds. ++ int gap; ++ /// The expected size of the frame image file (specified with 'S='), ++ /// used to check if uploading succeeded. ++ unsigned expected_size; ++ /// Format specification (see the `f=` key). ++ int format; ++ /// Pixel width and height of the non-composed (on-disk) frame data. May ++ /// differ from the image (i.e. first frame) dimensions. ++ int data_pix_width, data_pix_height; ++ /// The offset of the frame relative to the first frame. ++ int x, y; ++ /// Compression mode (see the `o=` key). ++ char compression; ++ /// The status (see `ImageStatus`). ++ char status; ++ /// The reason of uploading failure (see `ImageUploadingFailure`). ++ char uploading_failure; ++ /// Whether failures and successes should be reported ('q='). ++ char quiet; ++ /// Whether to blend the frame with the background or replace it. ++ char blend; ++ /// The file corresponding to the on-disk cache, used when uploading. ++ FILE *open_file; ++ /// The size of the corresponding file cached on disk. ++ unsigned disk_size; ++ /// The imlib object containing the fully composed frame. It's not ++ /// scaled for screen display yet. ++ Imlib_Image imlib_object; ++} ImageFrame; ++ ++typedef struct Image { ++ /// The client id (the one specified with 'i='). Must be nonzero. ++ uint32_t image_id; ++ /// The client id specified in the query command (`a=q`). This one must ++ /// be used to create the response if it's non-zero. ++ uint32_t query_id; ++ /// The number specified in the transmission command (`I=`). If ++ /// non-zero, it may be used to identify the image instead of the ++ /// image_id, and it also should be mentioned in responses. ++ uint32_t image_number; ++ /// The last time when the image was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The total duration of the animation in milliseconds. ++ int total_duration; ++ /// The total size of cached image files for all frames. ++ int total_disk_size; ++ /// The global index of the creation command. Used to decide which image ++ /// is newer if they have the same image number. ++ uint64_t global_command_index; ++ /// The 1-based index of the currently displayed frame. ++ int current_frame; ++ /// The state of the animation, see `AnimationState`. ++ char animation_state; ++ /// The absolute time that is assumed to be the start of the current ++ /// frame (in ms since initialization). ++ Milliseconds current_frame_time; ++ /// The absolute time of the last redraw (in ms since initialization). ++ /// Used to check whether it's the first time we draw the image in the ++ /// current redraw cycle. ++ Milliseconds last_redraw; ++ /// The absolute time of the next redraw (in ms since initialization). ++ /// 0 means no redraw is scheduled. ++ Milliseconds next_redraw; ++ /// The unscaled pixel width and height of the image. Usually inherited ++ /// from the first frame. ++ int pix_width, pix_height; ++ /// The first frame. ++ ImageFrame first_frame; ++ /// The array of frames beyond the first one. ++ kvec_t(ImageFrame) frames_beyond_the_first; ++ /// Image placements. ++ khash_t(id2placement) *placements; ++ /// The default placement. ++ uint32_t default_placement; ++ /// The initial placement id, specified with the transmission command, ++ /// used to report success or failure. ++ uint32_t initial_placement_id; ++} Image; ++ ++typedef struct ImagePlacement { ++ /// The image this placement belongs to. ++ Image *image; ++ /// The id of the placement. Must be nonzero. ++ uint32_t placement_id; ++ /// The last time when the placement was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The 1-based index of the protected pixmap. We protect a pixmap in ++ /// gr_load_pixmap to avoid unloading it right after it was loaded. ++ int protected_frame; ++ /// Whether the placement is used only for Unicode placeholders. ++ char virtual; ++ /// The scaling mode (see `ScaleMode`). ++ char scale_mode; ++ /// Height and width in cells. ++ uint16_t rows, cols; ++ /// Top-left corner of the source rectangle ('x=' and 'y='). ++ int src_pix_x, src_pix_y; ++ /// Height and width of the source rectangle (zero if full image). ++ int src_pix_width, src_pix_height; ++ /// The image appropriately scaled and uploaded to the X server. This ++ /// pixmap is premultiplied by alpha. ++ Pixmap first_pixmap; ++ /// The array of pixmaps beyond the first one. ++ kvec_t(Pixmap) pixmaps_beyond_the_first; ++ /// The dimensions of the cell used to scale the image. If cell ++ /// dimensions are changed (font change), the image will be rescaled. ++ uint16_t scaled_cw, scaled_ch; ++ /// If true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++} ImagePlacement; ++ ++/// A rectangular piece of an image to be drawn. ++typedef struct { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// The position of the rectangle in pixels. ++ int screen_x_pix, screen_y_pix; ++ /// The starting row on the screen. ++ int screen_y_row; ++ /// The part of the whole image to be drawn, in cells. Starts are ++ /// zero-based, ends are exclusive. ++ int img_start_col, img_end_col, img_start_row, img_end_row; ++ /// The current cell width and height in pixels. ++ int cw, ch; ++ /// Whether colors should be inverted. ++ int reverse; ++} ImageRect; ++ ++/// Executes `code` for each frame of an image. Example: ++/// ++/// foreach_frame(image, frame, { ++/// printf("Frame %d\n", frame->index); ++/// }); ++/// ++#define foreach_frame(image, framevar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \ ++ ImageFrame *framevar = \ ++ __i == 0 ? &(image).first_frame \ ++ : &kv_A((image).frames_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++/// Executes `code` for each pixmap of a placement. Example: ++/// ++/// foreach_pixmap(placement, pixmap, { ++/// ... ++/// }); ++/// ++#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \ ++ Pixmap pixmapvar = \ ++ __i == 0 ? (placement).first_pixmap \ ++ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++ ++static Image *gr_find_image(uint32_t image_id); ++static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len); ++static void gr_delete_image(Image *img); ++static void gr_check_limits(); ++static char *gr_base64dec(const char *src, size_t *size); ++static void sanitize_str(char *str, size_t max_len); ++static const char *sanitized_filename(const char *str); ++ ++/// The array of image rectangles to draw. It is reset each frame. ++static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}}; ++/// The known images (including the ones being uploaded). ++static khash_t(id2image) *images = NULL; ++/// The total number of placements in all images. ++static unsigned total_placement_count = 0; ++/// The total size of all image files stored in the on-disk cache. ++static int64_t images_disk_size = 0; ++/// The total size of all images and placements loaded into ram. ++static int64_t images_ram_size = 0; ++/// The id of the last loaded image. ++static uint32_t last_image_id = 0; ++/// Current cell width and heigh in pixels. ++static int current_cw = 0, current_ch = 0; ++/// The id of the currently uploaded image (when using direct uploading). ++static uint32_t current_upload_image_id = 0; ++/// The index of the frame currently being uploaded. ++static int current_upload_frame_index = 0; ++/// The time when the graphics module was initialized. ++static struct timespec initialization_time = {0}; ++/// The time when the current frame drawing started, used for debugging fps and ++/// to calculate the current frame for animations. ++static Milliseconds drawing_start_time; ++/// The global index of the current command. ++static uint64_t global_command_counter = 0; ++/// The next redraw times for each row of the terminal. Used for animations. ++/// 0 means no redraw is scheduled. ++static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL}; ++/// The number of files loaded in the current redraw cycle. ++static int this_redraw_cycle_loaded_files = 0; ++/// The number of pixmaps loaded in the current redraw cycle. ++static int this_redraw_cycle_loaded_pixmaps = 0; ++ ++/// The directory where the cache files are stored. ++static char cache_dir[MAX_FILENAME_SIZE - 16]; ++ ++/// The table used for color inversion. ++static unsigned char reverse_table[256]; ++ ++// Declared in the header. ++GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE; ++char graphics_display_images = 1; ++GraphicsCommandResult graphics_command_result = {0}; ++int graphics_next_redraw_delay = INT_MAX; ++ ++// Defined in config.h ++extern const char graphics_cache_dir_template[]; ++extern unsigned graphics_max_single_image_file_size; ++extern unsigned graphics_total_file_cache_size; ++extern unsigned graphics_max_single_image_ram_size; ++extern unsigned graphics_max_total_ram_size; ++extern unsigned graphics_max_total_placements; ++extern double graphics_excess_tolerance_ratio; ++extern unsigned graphics_animation_min_delay; ++ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic helpers. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define MIN(a, b) ((a) < (b) ? (a) : (b)) ++#define MAX(a, b) ((a) < (b) ? (b) : (a)) ++ ++/// Returns the difference between `end` and `start` in milliseconds. ++static int64_t gr_timediff_ms(const struct timespec *end, ++ const struct timespec *start) { ++ return (end->tv_sec - start->tv_sec) * 1000 + ++ (end->tv_nsec - start->tv_nsec) / 1000000; ++} ++ ++/// Returns the current time in milliseconds since the initialization. ++static Milliseconds gr_now_ms() { ++ struct timespec now; ++ clock_gettime(CLOCK_MONOTONIC, &now); ++ return gr_timediff_ms(&now, &initialization_time); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Logging. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define GR_LOG(...) \ ++ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0) ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic image management functions (create, delete, find, etc). ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns the 1-based index of the last frame. Note that you may want to use ++/// `gr_last_uploaded_frame_index` instead since the last frame may be not ++/// fully uploaded yet. ++static inline int gr_last_frame_index(Image *img) { ++ return kv_size(img->frames_beyond_the_first) + 1; ++} ++ ++/// Returns the frame with the given index. Returns NULL if the index is out of ++/// bounds. The index is 1-based. ++static ImageFrame *gr_get_frame(Image *img, int index) { ++ if (!img) ++ return NULL; ++ if (index == 1) ++ return &img->first_frame; ++ if (2 <= index && index <= gr_last_frame_index(img)) ++ return &kv_A(img->frames_beyond_the_first, index - 2); ++ return NULL; ++} ++ ++/// Returns the last frame of the image. Returns NULL if `img` is NULL. ++static ImageFrame *gr_get_last_frame(Image *img) { ++ if (!img) ++ return NULL; ++ return gr_get_frame(img, gr_last_frame_index(img)); ++} ++ ++/// Returns the 1-based index of the last frame or the second-to-last frame if ++/// the last frame is not fully uploaded yet. ++static inline int gr_last_uploaded_frame_index(Image *img) { ++ int last_index = gr_last_frame_index(img); ++ if (last_index > 1 && ++ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS) ++ return last_index - 1; ++ return last_index; ++} ++ ++/// Returns the pixmap for the frame with the given index. Returns 0 if the ++/// index is out of bounds. The index is 1-based. ++static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) { ++ if (index == 1) ++ return placement->first_pixmap; ++ if (2 <= index && ++ index <= kv_size(placement->pixmaps_beyond_the_first) + 1) ++ return kv_A(placement->pixmaps_beyond_the_first, index - 2); ++ return 0; ++} ++ ++/// Sets the pixmap for the frame with the given index. The index is 1-based. ++/// The array of pixmaps is resized if needed. ++static void gr_set_frame_pixmap(ImagePlacement *placement, int index, ++ Pixmap pixmap) { ++ if (index == 1) { ++ placement->first_pixmap = pixmap; ++ return; ++ } ++ // Resize the array if needed. ++ size_t old_size = kv_size(placement->pixmaps_beyond_the_first); ++ if (old_size < index - 1) { ++ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2); ++ for (size_t i = old_size; i < index - 1; i++) ++ kv_A(placement->pixmaps_beyond_the_first, i) = 0; ++ } ++ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap; ++} ++ ++/// Finds the image corresponding to the client id. Returns NULL if cannot find. ++static Image *gr_find_image(uint32_t image_id) { ++ khiter_t k = kh_get(id2image, images, image_id); ++ if (k == kh_end(images)) ++ return NULL; ++ Image *res = kh_value(images, k); ++ return res; ++} ++ ++/// Finds the newest image corresponding to the image number. Returns NULL if ++/// cannot find. ++static Image *gr_find_image_by_number(uint32_t image_number) { ++ if (image_number == 0) ++ return NULL; ++ Image *newest_img = NULL; ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ if (img->image_number == image_number && ++ (!newest_img || newest_img->global_command_index < ++ img->global_command_index)) ++ newest_img = img; ++ }); ++ if (!newest_img) ++ GR_LOG("Image number %u not found\n", image_number); ++ else ++ GR_LOG("Found image number %u, its id is %u\n", image_number, ++ img->image_id); ++ return newest_img; ++} ++ ++/// Finds the placement corresponding to the id. If the placement id is 0, ++/// returns some default placement. ++static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) { ++ if (!img) ++ return NULL; ++ if (placement_id == 0) { ++ // Try to get the default placement. ++ ImagePlacement *dflt = NULL; ++ if (img->default_placement != 0) ++ dflt = gr_find_placement(img, img->default_placement); ++ if (dflt) ++ return dflt; ++ // If there is no default placement, return the first one and ++ // set it as the default. ++ kh_foreach_value(img->placements, dflt, { ++ img->default_placement = dflt->placement_id; ++ return dflt; ++ }); ++ // If there are no placements, return NULL. ++ return NULL; ++ } ++ khiter_t k = kh_get(id2placement, img->placements, placement_id); ++ if (k == kh_end(img->placements)) ++ return NULL; ++ ImagePlacement *res = kh_value(img->placements, k); ++ return res; ++} ++ ++/// Finds the placement by image id and placement id. ++static ImagePlacement *gr_find_image_and_placement(uint32_t image_id, ++ uint32_t placement_id) { ++ return gr_find_placement(gr_find_image(image_id), placement_id); ++} ++ ++/// Writes the name of the on-disk cache file to `out`. `max_len` should be the ++/// size of `out`. The name will be something like ++/// "/tmp/st-images-xxx/img-ID-FRAME". ++static void gr_get_frame_filename(ImageFrame *frame, char *out, ++ size_t max_len) { ++ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir, ++ frame->image->image_id, frame->index); ++} ++ ++/// Returns the (estimation) of the RAM size used by the frame right now. ++static unsigned gr_frame_current_ram_size(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return 0; ++ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by a single frame pixmap. ++static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) { ++ return (unsigned)placement->rows * placement->cols * ++ placement->scaled_ch * placement->scaled_cw * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by the placemenet right now. ++static unsigned gr_placement_current_ram_size(ImagePlacement *placement) { ++ unsigned single_frame_size = ++ gr_placement_single_frame_ram_size(placement); ++ unsigned result = 0; ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ result += single_frame_size; ++ }); ++ return result; ++} ++ ++/// Unload the frame from RAM (i.e. delete the corresponding imlib object). ++/// If the on-disk file of the frame is preserved, it can be reloaded later. ++static void gr_unload_frame(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return; ++ ++ unsigned frame_ram_size = gr_frame_current_ram_size(frame); ++ images_ram_size -= frame_ram_size; ++ ++ imlib_context_set_image(frame->imlib_object); ++ imlib_free_image_and_decache(); ++ frame->imlib_object = NULL; ++ ++ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_ram_size / 1024, ++ frame_ram_size / 1024); ++} ++ ++/// Unload all frames of the image. ++static void gr_unload_all_frames(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_unload_frame(frame); ++ }); ++} ++ ++/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps). ++/// If the on-disk files or imlib objects of the corresponding image are ++/// preserved, the placement can be reloaded later. ++static void gr_unload_placement(ImagePlacement *placement) { ++ unsigned placement_ram_size = gr_placement_current_ram_size(placement); ++ images_ram_size -= placement_ram_size; ++ ++ Display *disp = imlib_context_get_display(); ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ XFreePixmap(disp, pixmap); ++ }); ++ ++ placement->first_pixmap = 0; ++ placement->pixmaps_beyond_the_first.n = 0; ++ placement->scaled_ch = placement->scaled_cw = 0; ++ ++ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, images_ram_size / 1024, ++ placement_ram_size / 1024); ++} ++ ++/// Unload a single pixmap of the placement from RAM. ++static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) { ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (!pixmap) ++ return; ++ ++ Display *disp = imlib_context_get_display(); ++ XFreePixmap(disp, pixmap); ++ gr_set_frame_pixmap(placement, frameidx, 0); ++ images_ram_size -= gr_placement_single_frame_ram_size(placement); ++ ++ GR_LOG("After unloading pixmap %ld of " ++ "placement %u/%u (atime %ld ms ago) " ++ "frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ pixmap, placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, frameidx, ++ drawing_start_time - ++ gr_get_frame(placement->image, frameidx)->atime, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024); ++} ++ ++/// Deletes the on-disk cache file corresponding to the frame. The in-ram image ++/// object (if it exists) is not deleted, placements are not unloaded either. ++static void gr_delete_imagefile(ImageFrame *frame) { ++ // It may still be being loaded. Close the file in this case. ++ if (frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++ ++ if (frame->disk_size == 0) ++ return; ++ ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ remove(filename); ++ ++ unsigned disk_size = frame->disk_size; ++ images_disk_size -= disk_size; ++ frame->image->total_disk_size -= disk_size; ++ frame->disk_size = 0; ++ ++ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) " ++ "disk: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_disk_size / 1024, ++ disk_size / 1024); ++} ++ ++/// Deletes all on-disk cache files of the image (for each frame). ++static void gr_delete_imagefiles(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ }); ++} ++ ++/// Deletes the given placement: unloads, frees the object, but doesn't change ++/// the `placements` hash table. ++static void gr_delete_placement_keep_id(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id, ++ placement->placement_id); ++ gr_unload_placement(placement); ++ kv_destroy(placement->pixmaps_beyond_the_first); ++ free(placement); ++ total_placement_count--; ++} ++ ++/// Deletes all placements of `img`. ++static void gr_delete_all_placements(Image *img) { ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_delete_placement_keep_id(placement); ++ }); ++ kh_clear(id2placement, img->placements); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// but doesn't change the `images` hash table. ++static void gr_delete_image_keep_id(Image *img) { ++ if (!img) ++ return; ++ GR_LOG("Deleting image %u\n", img->image_id); ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ gr_unload_frame(frame); ++ }); ++ kv_destroy(img->frames_beyond_the_first); ++ gr_delete_all_placements(img); ++ kh_destroy(id2placement, img->placements); ++ free(img); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// and also removes it from `images`. ++static void gr_delete_image(Image *img) { ++ if (!img) ++ return; ++ uint32_t id = img->image_id; ++ gr_delete_image_keep_id(img); ++ khiter_t k = kh_get(id2image, images, id); ++ kh_del(id2image, images, k); ++} ++ ++/// Deletes the given placement: unloads, frees the object, and also removes it ++/// from `placements`. ++static void gr_delete_placement(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ uint32_t id = placement->placement_id; ++ Image *img = placement->image; ++ gr_delete_placement_keep_id(placement); ++ khiter_t k = kh_get(id2placement, img->placements, id); ++ kh_del(id2placement, img->placements, k); ++} ++ ++/// Deletes all images and clears `images`. ++static void gr_delete_all_images() { ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ gr_delete_image_keep_id(img); ++ }); ++ kh_clear(id2image, images); ++} ++ ++/// Update the atime of the image. ++static void gr_touch_image(Image *img) { ++ img->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the frame. ++static void gr_touch_frame(ImageFrame *frame) { ++ frame->image->atime = frame->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the placement. Touches the images too. ++static void gr_touch_placement(ImagePlacement *placement) { ++ placement->image->atime = placement->atime = gr_now_ms(); ++} ++ ++/// Creates a new image with the given id. If an image with that id already ++/// exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static Image *gr_new_image(uint32_t id) { ++ if (id == 0) { ++ do { ++ id = rand(); ++ // Avoid IDs that don't need full 32 bits. ++ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 || ++ gr_find_image(id)); ++ GR_LOG("Generated random image id %u\n", id); ++ } ++ Image *img = gr_find_image(id); ++ gr_delete_image_keep_id(img); ++ GR_LOG("Creating image %u\n", id); ++ img = malloc(sizeof(Image)); ++ memset(img, 0, sizeof(Image)); ++ img->placements = kh_init(id2placement); ++ int ret; ++ khiter_t k = kh_put(id2image, images, id, &ret); ++ kh_value(images, k) = img; ++ img->image_id = id; ++ gr_touch_image(img); ++ img->global_command_index = global_command_counter; ++ return img; ++} ++ ++/// Creates a new frame at the end of the frame array. It may be the first frame ++/// if there are no frames yet. ++static ImageFrame *gr_append_new_frame(Image *img) { ++ ImageFrame *frame = NULL; ++ if (img->first_frame.index == 0 && ++ kv_size(img->frames_beyond_the_first) == 0) { ++ frame = &img->first_frame; ++ frame->index = 1; ++ } else { ++ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first); ++ memset(frame, 0, sizeof(ImageFrame)); ++ frame->index = kv_size(img->frames_beyond_the_first) + 1; ++ } ++ frame->image = img; ++ gr_touch_frame(frame); ++ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id); ++ return frame; ++} ++ ++/// Creates a new placement with the given id. If a placement with that id ++/// already exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static ImagePlacement *gr_new_placement(Image *img, uint32_t id) { ++ if (id == 0) { ++ do { ++ // Currently we support only 24-bit IDs. ++ id = rand() & 0xFFFFFF; ++ // Avoid IDs that need only one byte. ++ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id)); ++ } ++ ImagePlacement *placement = gr_find_placement(img, id); ++ gr_delete_placement_keep_id(placement); ++ GR_LOG("Creating placement %u/%u\n", img->image_id, id); ++ placement = malloc(sizeof(ImagePlacement)); ++ memset(placement, 0, sizeof(ImagePlacement)); ++ total_placement_count++; ++ int ret; ++ khiter_t k = kh_put(id2placement, img->placements, id, &ret); ++ kh_value(img->placements, k) = placement; ++ placement->image = img; ++ placement->placement_id = id; ++ gr_touch_placement(placement); ++ if (img->default_placement == 0) ++ img->default_placement = id; ++ return placement; ++} ++ ++static int64_t ceil_div(int64_t a, int64_t b) { ++ return (a + b - 1) / b; ++} ++ ++/// Computes the best number of rows and columns for a placement if it's not ++/// specified, and also adjusts the source rectangle size. ++static void gr_infer_placement_size_maybe(ImagePlacement *placement) { ++ // The size of the image. ++ int image_pix_width = placement->image->pix_width; ++ int image_pix_height = placement->image->pix_height; ++ // Negative values are not allowed. Quietly set them to 0. ++ if (placement->src_pix_x < 0) ++ placement->src_pix_x = 0; ++ if (placement->src_pix_y < 0) ++ placement->src_pix_y = 0; ++ if (placement->src_pix_width < 0) ++ placement->src_pix_width = 0; ++ if (placement->src_pix_height < 0) ++ placement->src_pix_height = 0; ++ // If the source rectangle is outside the image, truncate it. ++ if (placement->src_pix_x > image_pix_width) ++ placement->src_pix_x = image_pix_width; ++ if (placement->src_pix_y > image_pix_height) ++ placement->src_pix_y = image_pix_height; ++ // If the source rectangle is not specified, use the whole image. If ++ // it's partially outside the image, truncate it. ++ if (placement->src_pix_width == 0 || ++ placement->src_pix_x + placement->src_pix_width > image_pix_width) ++ placement->src_pix_width = ++ image_pix_width - placement->src_pix_x; ++ if (placement->src_pix_height == 0 || ++ placement->src_pix_y + placement->src_pix_height > image_pix_height) ++ placement->src_pix_height = ++ image_pix_height - placement->src_pix_y; ++ ++ if (placement->cols != 0 && placement->rows != 0) ++ return; ++ if (placement->src_pix_width == 0 || placement->src_pix_height == 0) ++ return; ++ if (current_cw == 0 || current_ch == 0) ++ return; ++ ++ // If no size is specified, use the image size. ++ if (placement->cols == 0 && placement->rows == 0) { ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ return; ++ } ++ ++ // Some applications specify only one of the dimensions. ++ if (placement->scale_mode == SCALE_MODE_CONTAIN) { ++ // If we preserve aspect ratio and fit to width/height, the most ++ // logical thing is to find the minimum size of the ++ // non-specified dimension that allows the image to fit the ++ // specified dimension. ++ if (placement->cols == 0) { ++ placement->cols = ceil_div( ++ placement->src_pix_width * placement->rows * ++ current_ch, ++ placement->src_pix_height * current_cw); ++ return; ++ } ++ if (placement->rows == 0) { ++ placement->rows = ++ ceil_div(placement->src_pix_height * ++ placement->cols * current_cw, ++ placement->src_pix_width * current_ch); ++ return; ++ } ++ } else { ++ // Otherwise we stretch the image or preserve the original size. ++ // In both cases we compute the best number of columns from the ++ // pixel size and cell size. ++ // TODO: In the case of stretching it's not the most logical ++ // thing to do, may need to revisit in the future. ++ // Currently we switch to SCALE_MODE_CONTAIN when only one ++ // of the dimensions is specified, so this case shouldn't ++ // happen in practice. ++ if (!placement->cols) ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ if (!placement->rows) ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ } ++} ++ ++/// Adjusts the current frame index if enough time has passed since the display ++/// of the current frame. Also computes the time of the next redraw of this ++/// image (`img->next_redraw`). The current time is passed as an argument so ++/// that all animations are in sync. ++static void gr_update_frame_index(Image *img, Milliseconds now) { ++ if (img->current_frame == 0) { ++ img->current_frame_time = now; ++ img->current_frame = 1; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ // If the animation is stopped, show the current frame. ++ if (!img->animation_state || ++ img->animation_state == ANIMATION_STATE_STOPPED || ++ img->animation_state == ANIMATION_STATE_UNSET) { ++ // The next redraw is never (unless the state is changed). ++ img->next_redraw = 0; ++ return; ++ } ++ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img); ++ // If we are loading and we reached the last frame, show the last frame. ++ if (img->animation_state == ANIMATION_STATE_LOADING && ++ img->current_frame == last_uploaded_frame_index) { ++ // The next redraw is never (unless the state is changed or ++ // frames are added). ++ img->next_redraw = 0; ++ return; ++ } ++ ++ // Check how many milliseconds passed since the current frame was shown. ++ int passed_ms = now - img->current_frame_time; ++ // If the animation is looping and too much time has passes, we can ++ // make a shortcut. ++ if (img->animation_state == ANIMATION_STATE_LOOPING && ++ img->total_duration > 0 && passed_ms >= img->total_duration) { ++ passed_ms %= img->total_duration; ++ img->current_frame_time = now - passed_ms; ++ } ++ // Find the next frame. ++ int original_frame_index = img->current_frame; ++ while (1) { ++ ImageFrame *frame = gr_get_frame(img, img->current_frame); ++ if (!frame) { ++ // The frame doesn't exist, go to the first frame. ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ if (frame->gap >= 0 && passed_ms < frame->gap) { ++ // Not enough time has passed, we are still in the same ++ // frame, and it's not a gapless frame. ++ img->next_redraw = ++ img->current_frame_time + MAX(1, frame->gap); ++ return; ++ } ++ // Otherwise go to the next frame. ++ passed_ms -= MAX(0, frame->gap); ++ if (img->current_frame >= last_uploaded_frame_index) { ++ // It's the last frame, if the animation is loading, ++ // remain on it. ++ if (img->animation_state == ANIMATION_STATE_LOADING) { ++ img->next_redraw = 0; ++ return; ++ } ++ // Otherwise the animation is looping. ++ img->current_frame = 1; ++ // TODO: Support finite number of loops. ++ } else { ++ img->current_frame++; ++ } ++ // Make sure we don't get stuck in an infinite loop. ++ if (img->current_frame == original_frame_index) { ++ // We looped through all frames, but haven't reached the ++ // next frame yet. This may happen if too much time has ++ // passed since the last redraw or all the frames are ++ // gapless. Just move on to the next frame. ++ img->current_frame++; ++ if (img->current_frame > ++ last_uploaded_frame_index) ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX( ++ 1, gr_get_frame(img, img->current_frame)->gap); ++ return; ++ } ++ // Adjust the start time of the frame. The next redraw time will ++ // be set in the next iteration. ++ img->current_frame_time += MAX(0, frame->gap); ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Unloading and deleting images to save resources. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// A helper to compare frames by atime for qsort. ++static int gr_cmp_frames_by_atime(const void *a, const void *b) { ++ ImageFrame *frame_a = *(ImageFrame *const *)a; ++ ImageFrame *frame_b = *(ImageFrame *const *)b; ++ if (frame_a->atime == frame_b->atime) ++ return frame_a->image->global_command_index - ++ frame_b->image->global_command_index; ++ return frame_a->atime - frame_b->atime; ++} ++ ++/// A helper to compare images by atime for qsort. ++static int gr_cmp_images_by_atime(const void *a, const void *b) { ++ Image *img_a = *(Image *const *)a; ++ Image *img_b = *(Image *const *)b; ++ if (img_a->atime == img_b->atime) ++ return img_a->global_command_index - ++ img_b->global_command_index; ++ return img_a->atime - img_b->atime; ++} ++ ++/// A helper to compare placements by atime for qsort. ++static int gr_cmp_placements_by_atime(const void *a, const void *b) { ++ ImagePlacement *p_a = *(ImagePlacement **)a; ++ ImagePlacement *p_b = *(ImagePlacement **)b; ++ if (p_a->atime == p_b->atime) ++ return p_a->image->global_command_index - ++ p_b->image->global_command_index; ++ return p_a->atime - p_b->atime; ++} ++ ++typedef kvec_t(Image *) ImageVec; ++typedef kvec_t(ImagePlacement *) ImagePlacementVec; ++typedef kvec_t(ImageFrame *) ImageFrameVec; ++ ++/// Returns an array of pointers to all images sorted by atime. ++static ImageVec gr_get_images_sorted_by_atime() { ++ ImageVec vec; ++ kv_init(vec); ++ if (kh_size(images) == 0) ++ return vec; ++ kv_resize(Image *, vec, kh_size(images)); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { kv_push(Image *, vec, img); }); ++ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all placements sorted by atime. ++static ImagePlacementVec gr_get_placements_sorted_by_atime() { ++ ImagePlacementVec vec; ++ kv_init(vec); ++ if (total_placement_count == 0) ++ return vec; ++ kv_resize(ImagePlacement *, vec, total_placement_count); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ kv_push(ImagePlacement *, vec, placement); ++ }); ++ }); ++ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *), ++ gr_cmp_placements_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all frames sorted by atime. ++static ImageFrameVec gr_get_frames_sorted_by_atime() { ++ ImageFrameVec frames; ++ kv_init(frames); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ kv_push(ImageFrame *, frames, frame); ++ }); ++ }); ++ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *), ++ gr_cmp_frames_by_atime); ++ return frames; ++} ++ ++/// An object that can be unloaded from RAM. ++typedef struct { ++ /// Some score, probably based on access time. The lower the score, the ++ /// more likely that the object should be unloaded. ++ int64_t score; ++ union { ++ ImagePlacement *placement; ++ ImageFrame *frame; ++ }; ++ /// If zero, the object is the imlib object of `frame`, if non-zero, ++ /// the object is a pixmap of `frameidx`-th frame of `placement`. ++ int frameidx; ++} UnloadableObject; ++ ++typedef kvec_t(UnloadableObject) UnloadableObjectVec; ++ ++/// A helper to compare unloadable objects by score for qsort. ++static int gr_cmp_unloadable_objects(const void *a, const void *b) { ++ UnloadableObject *obj_a = (UnloadableObject *)a; ++ UnloadableObject *obj_b = (UnloadableObject *)b; ++ return obj_a->score - obj_b->score; ++} ++ ++/// Unloads an unloadable object from RAM. ++static void gr_unload_object(UnloadableObject *obj) { ++ if (obj->frameidx) { ++ if (obj->placement->protected_frame == obj->frameidx) ++ return; ++ gr_unload_pixmap(obj->placement, obj->frameidx); ++ } else { ++ gr_unload_frame(obj->frame); ++ } ++} ++ ++/// Returns the recency threshold for an image. Frames that were accessed within ++/// this threshold from now are considered recent and may be handled ++/// differently because we may need them again very soon. ++static Milliseconds gr_recency_threshold(Image *img) { ++ return img->total_duration * 2 + 1000; ++} ++ ++/// Creates an unloadable object for the imlib object of a frame. ++static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now, ++ ImageFrame *frame) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = 0; ++ obj.frame = frame; ++ Milliseconds atime = frame->atime; ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent frame, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Randomize the score because it's not very clear in which ++ // order we want to unload them: reloading a frame may require ++ // reloading other frames. ++ obj.score = now + 1000 + rand() % 1000; ++ } ++ return obj; ++} ++ ++/// Creates an unloadable object for a pixmap. ++static UnloadableObject ++gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame, ++ ImagePlacement *placement) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = frame->index; ++ obj.placement = placement; ++ obj.score = placement->atime; ++ // Since we don't store pixmap atimes, use the ++ // oldest atime of the frame and the placement. ++ Milliseconds atime = MIN(placement->atime, frame->atime); ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent pixmap, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Also assign higher scores to frames that are closer to the ++ // current frame (more likely to be used soon). ++ int num_frames = gr_last_frame_index(frame->image); ++ int dist = frame->index - frame->image->current_frame; ++ if (dist < 0) ++ dist += num_frames; ++ obj.score = ++ now + 1000 + (num_frames - dist) * 1000 / num_frames; ++ // If the pixmap is much larger than the imlib image, prefer to ++ // unload the pixmap by adding up to -1000 to the score. If the ++ // imlib image is larger, add up to +1000. ++ float imlib_size = gr_frame_current_ram_size(frame); ++ float pixmap_size = ++ gr_placement_single_frame_ram_size(placement); ++ obj.score += ++ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5); ++ } ++ return obj; ++} ++ ++/// Returns an array of unloadable objects sorted by score. ++static UnloadableObjectVec ++gr_get_unloadable_objects_sorted_by_score(Milliseconds now) { ++ UnloadableObjectVec objects; ++ kv_init(objects); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ if (!frame->imlib_object) ++ continue; ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_frame(now, frame)); ++ int frameidx = frame->index; ++ kh_foreach_value(img->placements, placement, { ++ if (!gr_get_frame_pixmap(placement, frameidx)) ++ continue; ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_pixmap( ++ now, frame, placement)); ++ }); ++ }); ++ }); ++ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject), ++ gr_cmp_unloadable_objects); ++ return objects; ++} ++ ++/// Returns the limit adjusted by the excess tolerance ratio. ++static inline unsigned apply_tolerance(unsigned limit) { ++ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio); ++} ++ ++/// Checks RAM and disk cache limits and deletes/unloads some images. ++static void gr_check_limits() { ++ Milliseconds now = gr_now_ms(); ++ ImageVec images_sorted = {0}; ++ ImagePlacementVec placements_sorted = {0}; ++ ImageFrameVec frames_sorted = {0}; ++ UnloadableObjectVec objects_sorted = {0}; ++ int images_begin = 0; ++ int placements_begin = 0; ++ char changed = 0; ++ // First reduce the number of images if there are too many. ++ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many images: %d\n", kh_size(images)); ++ changed = 1; ++ images_sorted = gr_get_images_sorted_by_atime(); ++ int to_delete = kv_size(images_sorted) - ++ graphics_max_total_placements; ++ for (; images_begin < to_delete; images_begin++) ++ gr_delete_image(images_sorted.a[images_begin]); ++ } ++ // Then reduce the number of placements if there are too many. ++ if (total_placement_count > ++ apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many placements: %d\n", total_placement_count); ++ changed = 1; ++ placements_sorted = gr_get_placements_sorted_by_atime(); ++ int to_delete = kv_size(placements_sorted) - ++ graphics_max_total_placements; ++ for (; placements_begin < to_delete; placements_begin++) { ++ ImagePlacement *placement = ++ placements_sorted.a[placements_begin]; ++ if (placement->protected_frame) ++ break; ++ gr_delete_placement(placement); ++ } ++ } ++ // Then reduce the size of the image file cache. The files correspond to ++ // image frames. ++ if (images_disk_size > ++ apply_tolerance(graphics_total_file_cache_size)) { ++ GR_LOG("Too big disk cache: %ld KiB\n", ++ images_disk_size / 1024); ++ changed = 1; ++ frames_sorted = gr_get_frames_sorted_by_atime(); ++ for (int i = 0; i < kv_size(frames_sorted); i++) { ++ if (images_disk_size <= graphics_total_file_cache_size) ++ break; ++ gr_delete_imagefile(kv_A(frames_sorted, i)); ++ } ++ } ++ // Then unload images from RAM. ++ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) { ++ changed = 1; ++ int frames_begin = 0; ++ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024); ++ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now); ++ for (int i = 0; i < kv_size(objects_sorted); i++) { ++ if (images_ram_size <= graphics_max_total_ram_size) ++ break; ++ gr_unload_object(&kv_A(objects_sorted, i)); ++ } ++ } ++ if (changed) { ++ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " ++ "img count: %d placement count: %d\n", ++ images_ram_size / 1024, images_disk_size / 1024, ++ kh_size(images), total_placement_count); ++ } ++ kv_destroy(images_sorted); ++ kv_destroy(placements_sorted); ++ kv_destroy(frames_sorted); ++ kv_destroy(objects_sorted); ++} ++ ++/// Unloads all images by user request. ++void gr_unload_images_to_reduce_ram() { ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ if (placement->protected_frame) ++ continue; ++ gr_unload_placement(placement); ++ }); ++ gr_unload_all_frames(img); ++ }); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Image loading. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2 ++/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted ++/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout ++/// on little-endian architectures). ++static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format, ++ size_t num_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ if (format == 32) { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | ++ ((DATA32)from[byte_i + 3]) << 24; ++ } ++ } else { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | 0xFF000000; ++ } ++ } ++} ++ ++/// Loads uncompressed RGB or RGBA image data from a file. ++static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file, ++ int format, ++ size_t total_pixels) { ++ unsigned char chunk[BUFSIZ]; ++ size_t pixel_size = format == 24 ? 3 : 4; ++ size_t chunk_size_pix = BUFSIZ / 4; ++ size_t chunk_size_bytes = chunk_size_pix * pixel_size; ++ size_t bytes = total_pixels * pixel_size; ++ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels; ++ chunk_start_pix += chunk_size_pix) { ++ size_t read_size = fread(chunk, 1, chunk_size_bytes, file); ++ size_t read_pixels = read_size / pixel_size; ++ if (chunk_start_pix + read_pixels > total_pixels) ++ read_pixels = total_pixels - chunk_start_pix; ++ gr_copy_pixels(data + chunk_start_pix, chunk, format, ++ read_pixels); ++ } ++} ++ ++#define COMPRESSED_CHUNK_SIZE BUFSIZ ++#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4) ++ ++/// Loads compressed RGB or RGBA image data from a file. ++static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file, ++ int format, size_t total_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE]; ++ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE]; ++ ++ z_stream strm; ++ strm.zalloc = Z_NULL; ++ strm.zfree = Z_NULL; ++ strm.opaque = Z_NULL; ++ strm.next_out = decompressed_chunk; ++ strm.avail_out = DECOMPRESSED_CHUNK_SIZE; ++ strm.avail_in = 0; ++ strm.next_in = Z_NULL; ++ int ret = inflateInit(&strm); ++ if (ret != Z_OK) ++ return 1; ++ ++ int error = 0; ++ int progress = 0; ++ size_t total_copied_pixels = 0; ++ while (1) { ++ // If we don't have enough data in the input buffer, try to read ++ // from the file. ++ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) { ++ // Move the existing data to the beginning. ++ memmove(compressed_chunk, strm.next_in, strm.avail_in); ++ strm.next_in = compressed_chunk; ++ // Read more data. ++ size_t bytes_read = fread( ++ compressed_chunk + strm.avail_in, 1, ++ COMPRESSED_CHUNK_SIZE - strm.avail_in, file); ++ strm.avail_in += bytes_read; ++ if (bytes_read != 0) ++ progress = 1; ++ } ++ ++ // Try to inflate the data. ++ int ret = inflate(&strm, Z_SYNC_FLUSH); ++ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) { ++ error = 1; ++ fprintf(stderr, ++ "error: could not decompress the image, error " ++ "%s\n", ++ ret == Z_MEM_ERROR ? "Z_MEM_ERROR" ++ : "Z_DATA_ERROR"); ++ break; ++ } ++ ++ // Copy the data from the output buffer to the image. ++ size_t full_pixels = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size; ++ // Make sure we don't overflow the image. ++ if (full_pixels > total_pixels - total_copied_pixels) ++ full_pixels = total_pixels - total_copied_pixels; ++ if (full_pixels > 0) { ++ // Copy pixels. ++ gr_copy_pixels(data, decompressed_chunk, format, ++ full_pixels); ++ data += full_pixels; ++ total_copied_pixels += full_pixels; ++ if (total_copied_pixels >= total_pixels) { ++ // We filled the whole image, there may be some ++ // data left, but we just truncate it. ++ break; ++ } ++ // Move the remaining data to the beginning. ++ size_t copied_bytes = full_pixels * pixel_size; ++ size_t leftover = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) - ++ copied_bytes; ++ memmove(decompressed_chunk, ++ decompressed_chunk + copied_bytes, leftover); ++ strm.next_out -= copied_bytes; ++ strm.avail_out += copied_bytes; ++ progress = 1; ++ } ++ ++ // If we haven't made any progress, then we have reached the end ++ // of both the file and the inflated data. ++ if (!progress) ++ break; ++ progress = 0; ++ } ++ ++ inflateEnd(&strm); ++ return error; ++} ++ ++#undef COMPRESSED_CHUNK_SIZE ++#undef DECOMPRESSED_CHUNK_SIZE ++ ++/// Load the image from a file containing raw pixel data (RGB or RGBA), the data ++/// may be compressed. ++static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame, ++ const char *filename) { ++ size_t total_pixels = frame->data_pix_width * frame->data_pix_height; ++ if (total_pixels * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: image %u frame %u is too big too load: %zu > %u\n", ++ frame->image->image_id, frame->index, total_pixels * 4, ++ graphics_max_single_image_ram_size); ++ return NULL; ++ } ++ ++ FILE* file = fopen(filename, "rb"); ++ if (!file) { ++ fprintf(stderr, ++ "error: could not open image file: %s\n", ++ sanitized_filename(filename)); ++ return NULL; ++ } ++ ++ Imlib_Image image = imlib_create_image(frame->data_pix_width, ++ frame->data_pix_height); ++ if (!image) { ++ fprintf(stderr, ++ "error: could not create an image of size %d x %d\n", ++ frame->data_pix_width, frame->data_pix_height); ++ fclose(file); ++ return NULL; ++ } ++ ++ imlib_context_set_image(image); ++ imlib_image_set_has_alpha(1); ++ DATA32* data = imlib_image_get_data(); ++ ++ // The default format is 32. ++ int format = frame->format ? frame->format : 32; ++ ++ if (frame->compression == 0) { ++ gr_load_raw_pixel_data_uncompressed(data, file, format, ++ total_pixels); ++ } else { ++ int ret = gr_load_raw_pixel_data_compressed(data, file, format, ++ total_pixels); ++ if (ret != 0) { ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ fclose(file); ++ return NULL; ++ } ++ } ++ ++ fclose(file); ++ imlib_image_put_back_data(data); ++ return image; ++} ++ ++/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object ++/// is fully composed on top of the background frame. If the frame is already ++/// loaded, does nothing. Loading may fail, in which case the status of the ++/// frame will be set to STATUS_RAM_LOADING_ERROR. ++static void gr_load_imlib_object(ImageFrame *frame) { ++ if (frame->imlib_object) ++ return; ++ ++ // If the image is uninitialized or uploading has failed, or the file ++ // has been deleted, we cannot load the image. ++ if (frame->status < STATUS_UPLOADING_SUCCESS) ++ return; ++ if (frame->disk_size == 0) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: cached image was deleted: %u frame %u\n", ++ frame->image->image_id, frame->index); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ // Prevent recursive dependences between frames. ++ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) { ++ fprintf(stderr, ++ "error: recursive loading of image %u frame %u\n", ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ frame->status = STATUS_RAM_LOADING_IN_PROGRESS; ++ ++ // Load the background frame if needed. Hopefully it's not recursive. ++ ImageFrame *bg_frame = NULL; ++ if (frame->background_frame_index) { ++ bg_frame = gr_get_frame(frame->image, ++ frame->background_frame_index); ++ if (!bg_frame) { ++ fprintf(stderr, ++ "error: could not find background " ++ "frame %d for image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ gr_load_imlib_object(bg_frame); ++ if (!bg_frame->imlib_object) { ++ fprintf(stderr, ++ "error: could not load background frame %d for " ++ "image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ } ++ ++ // Load the frame data image. ++ Imlib_Image frame_data_image = NULL; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ GR_LOG("Loading image: %s\n", sanitized_filename(filename)); ++ if (frame->format == 100 || frame->format == 0) ++ frame_data_image = imlib_load_image(filename); ++ if (frame->format == 32 || frame->format == 24 || ++ (!frame_data_image && frame->format == 0)) ++ frame_data_image = gr_load_raw_pixel_data(frame, filename); ++ this_redraw_cycle_loaded_files++; ++ ++ if (!frame_data_image) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, "error: could not load image: %s\n", ++ sanitized_filename(filename)); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ imlib_context_set_image(frame_data_image); ++ int frame_data_width = imlib_image_get_width(); ++ int frame_data_height = imlib_image_get_height(); ++ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width, ++ frame_data_height); ++ // If imlib loading succeeded, and it is the first frame, set the ++ // information about the original image size, unless it's already set. ++ if (frame->index == 1 && frame->image->pix_width == 0 && ++ frame->image->pix_height == 0) { ++ frame->image->pix_width = frame_data_width; ++ frame->image->pix_height = frame_data_height; ++ } ++ ++ int image_width = frame->image->pix_width; ++ int image_height = frame->image->pix_height; ++ ++ // Compose the image with the background color or frame. ++ if (frame->background_color != 0 || bg_frame || ++ image_width != frame_data_width || ++ image_height != frame_data_height) { ++ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n", ++ frame->background_color, frame->background_frame_index); ++ Imlib_Image composed_image = imlib_create_image( ++ image_width, image_height); ++ imlib_context_set_image(composed_image); ++ imlib_image_set_has_alpha(1); ++ imlib_context_set_anti_alias(0); ++ ++ // Start with the background frame or color. ++ imlib_context_set_blend(0); ++ if (bg_frame && bg_frame->imlib_object) { ++ imlib_blend_image_onto_image( ++ bg_frame->imlib_object, 1, 0, 0, ++ image_width, image_height, 0, 0, ++ image_width, image_height); ++ } else { ++ int r = (frame->background_color >> 24) & 0xFF; ++ int g = (frame->background_color >> 16) & 0xFF; ++ int b = (frame->background_color >> 8) & 0xFF; ++ int a = frame->background_color & 0xFF; ++ imlib_context_set_color(r, g, b, a); ++ imlib_image_fill_rectangle(0, 0, image_width, ++ image_height); ++ } ++ ++ // Blend the frame data image onto the background. ++ imlib_context_set_blend(1); ++ imlib_blend_image_onto_image( ++ frame_data_image, 1, 0, 0, frame->data_pix_width, ++ frame->data_pix_height, frame->x, frame->y, ++ frame->data_pix_width, frame->data_pix_height); ++ ++ // Free the frame data image. ++ imlib_context_set_image(frame_data_image); ++ imlib_free_image(); ++ ++ frame_data_image = composed_image; ++ } ++ ++ frame->imlib_object = frame_data_image; ++ ++ images_ram_size += gr_frame_current_ram_size(frame); ++ frame->status = STATUS_RAM_LOADING_SUCCESS; ++ ++ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n", ++ frame->image->image_id, frame->index, ++ images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024); ++} ++ ++/// Premultiplies the alpha channel of the image data. The data is an array of ++/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB. ++static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) { ++ for (size_t i = 0; i < num_pixels; ++i) { ++ DATA32 pixel = data[i]; ++ unsigned char a = pixel >> 24; ++ if (a == 0) { ++ data[i] = 0; ++ } else if (a != 255) { ++ unsigned char b = (pixel & 0xFF) * a / 255; ++ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255; ++ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255; ++ data[i] = (a << 24) | (r << 16) | (g << 8) | b; ++ } ++ } ++} ++ ++/// Creates a pixmap for the frame of an image placement. The pixmap contain the ++/// image data correctly scaled and fit to the box defined by the number of ++/// rows/columns of the image placement and the provided cell dimensions in ++/// pixels. If the placement is already loaded, it will be reloaded only if the ++/// cell dimensions have changed. ++Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) { ++ Image *img = placement->image; ++ ImageFrame *frame = gr_get_frame(img, frameidx); ++ ++ // Update the atime uncoditionally. ++ gr_touch_placement(placement); ++ if (frame) ++ gr_touch_frame(frame); ++ ++ // If cw or ch are different, unload all the pixmaps. ++ if (placement->scaled_cw != cw || placement->scaled_ch != ch) { ++ gr_unload_placement(placement); ++ placement->scaled_cw = cw; ++ placement->scaled_ch = ch; ++ } ++ ++ // If it's already loaded, do nothing. ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (pixmap) ++ return pixmap; ++ ++ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id, ++ placement->placement_id, frameidx); ++ ++ // Load the imlib object for the frame. ++ if (!frame) { ++ fprintf(stderr, ++ "error: could not find frame %u for image %u\n", ++ frameidx, img->image_id); ++ return 0; ++ } ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) ++ return 0; ++ ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ ++ // Create the scaled image. This is temporary, we will scale it ++ // appropriately, upload to the X server, and then delete immediately. ++ int scaled_w = (int)placement->cols * cw; ++ int scaled_h = (int)placement->rows * ch; ++ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: placement %u/%u would be too big to load: %d x " ++ "%d x 4 > %u\n", ++ img->image_id, placement->placement_id, scaled_w, ++ scaled_h, graphics_max_single_image_ram_size); ++ return 0; ++ } ++ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h); ++ if (!scaled_image) { ++ fprintf(stderr, ++ "error: imlib_create_image(%d, %d) returned " ++ "null\n", ++ scaled_w, scaled_h); ++ return 0; ++ } ++ imlib_context_set_image(scaled_image); ++ imlib_image_set_has_alpha(1); ++ ++ // First fill the scaled image with the transparent color. ++ imlib_context_set_blend(0); ++ imlib_context_set_color(0, 0, 0, 0); ++ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ ++ // The source rectangle. ++ int src_x = placement->src_pix_x; ++ int src_y = placement->src_pix_y; ++ int src_w = placement->src_pix_width; ++ int src_h = placement->src_pix_height; ++ // Whether the box is too small to use the true size of the image. ++ char box_too_small = scaled_w < src_w || scaled_h < src_h; ++ char mode = placement->scale_mode; ++ ++ // Then blend the original image onto the transparent background. ++ if (src_w <= 0 || src_h <= 0) { ++ fprintf(stderr, "warning: image of zero size\n"); ++ } else if (mode == SCALE_MODE_FILL) { ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, 0, 0, ++ scaled_w, scaled_h); ++ } else if (mode == SCALE_MODE_NONE || ++ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, 0, 0, src_w, ++ src_h); ++ } else { ++ if (mode != SCALE_MODE_CONTAIN && ++ mode != SCALE_MODE_NONE_OR_CONTAIN) { ++ fprintf(stderr, ++ "warning: unknown scale mode %u, using " ++ "'contain' instead\n", ++ mode); ++ } ++ int dest_x, dest_y; ++ int dest_w, dest_h; ++ if (scaled_w * src_h > src_w * scaled_h) { ++ // If the box is wider than the original image, fit to ++ // height. ++ dest_h = scaled_h; ++ dest_y = 0; ++ dest_w = src_w * scaled_h / src_h; ++ dest_x = (scaled_w - dest_w) / 2; ++ } else { ++ // Otherwise, fit to width. ++ dest_w = scaled_w; ++ dest_x = 0; ++ dest_h = src_h * scaled_w / src_w; ++ dest_y = (scaled_h - dest_h) / 2; ++ } ++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x, ++ src_y, src_w, src_h, dest_x, ++ dest_y, dest_w, dest_h); ++ } ++ ++ // XRender needs the alpha channel premultiplied. ++ DATA32 *data = imlib_image_get_data(); ++ gr_premultiply_alpha(data, scaled_w * scaled_h); ++ ++ // Upload the image to the X server. ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ Colormap cmap = imlib_context_get_colormap(); ++ Drawable drawable = imlib_context_get_drawable(); ++ if (!drawable) ++ drawable = DefaultRootWindow(disp); ++ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32); ++ XVisualInfo visinfo; ++ XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo); ++ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, ++ (char *)data, scaled_w, scaled_h, 32, 0); ++ GC gc = XCreateGC(disp, pixmap, 0, NULL); ++ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w, ++ scaled_h); ++ XFreeGC(disp, gc); ++ // XDestroyImage will free the data as well, but it is managed by imlib, ++ // so set it to NULL. ++ ximage->data = NULL; ++ XDestroyImage(ximage); ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ ++ // Assign the pixmap to the frame and increase the ram size. ++ gr_set_frame_pixmap(placement, frameidx, pixmap); ++ images_ram_size += gr_placement_single_frame_ram_size(placement); ++ this_redraw_cycle_loaded_pixmaps++; ++ ++ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " ++ "KiB)\n", ++ frame->image->image_id, placement->placement_id, frame->index, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ ++ // Free up ram if needed, but keep the pixmap we've loaded no matter ++ // what. ++ placement->protected_frame = frameidx; ++ gr_check_limits(); ++ placement->protected_frame = 0; ++ ++ return pixmap; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Initialization and deinitialization. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Creates a temporary directory. ++static int gr_create_cache_dir() { ++ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); ++ if (!mkdtemp(cache_dir)) { ++ fprintf(stderr, ++ "error: could not create temporary dir from template " ++ "%s\n", ++ sanitized_filename(cache_dir)); ++ return 0; ++ } ++ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); ++ return 1; ++} ++ ++/// Checks whether `tmp_dir` exists and recreates it if it doesn't. ++static void gr_make_sure_tmpdir_exists() { ++ struct stat st; ++ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) ++ return; ++ fprintf(stderr, ++ "error: %s is not a directory, will need to create a new " ++ "graphics cache directory\n", ++ sanitized_filename(cache_dir)); ++ gr_create_cache_dir(); ++} ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm) { ++ // Set the initialization time. ++ clock_gettime(CLOCK_MONOTONIC, &initialization_time); ++ ++ // Create the temporary dir. ++ if (!gr_create_cache_dir()) ++ abort(); ++ ++ // Initialize imlib. ++ imlib_context_set_display(disp); ++ imlib_context_set_visual(vis); ++ imlib_context_set_colormap(cm); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ // Imlib2 checks only the file name when caching, which is not enough ++ // for us since we reuse file names. Disable caching. ++ imlib_set_cache_size(0); ++ ++ // Prepare for color inversion. ++ for (size_t i = 0; i < 256; ++i) ++ reverse_table[i] = 255 - i; ++ ++ // Create data structures. ++ images = kh_init(id2image); ++ kv_init(next_redraw_times); ++ ++ atexit(gr_deinit); ++} ++ ++/// Deinitialize the graphics module. ++void gr_deinit() { ++ // Remove the cache dir. ++ remove(cache_dir); ++ kv_destroy(next_redraw_times); ++ if (images) { ++ // Delete all images. ++ gr_delete_all_images(); ++ // Destroy the data structures. ++ kh_destroy(id2image, images); ++ images = NULL; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Dumping, debugging, and image preview. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns a string containing a time difference in a human-readable format. ++/// Uses a static buffer, so be careful. ++static const char *gr_ago(Milliseconds diff) { ++ static char result[32]; ++ double seconds = (double)diff / 1000.0; ++ if (seconds < 1) ++ snprintf(result, sizeof(result), "%.2f sec ago", seconds); ++ else if (seconds < 60) ++ snprintf(result, sizeof(result), "%d sec ago", (int)seconds); ++ else if (seconds < 3600) ++ snprintf(result, sizeof(result), "%d min %d sec ago", ++ (int)(seconds / 60), (int)(seconds) % 60); ++ else { ++ snprintf(result, sizeof(result), "%d hr %d min %d sec ago", ++ (int)(seconds / 3600), (int)(seconds) % 3600 / 60, ++ (int)(seconds) % 60); ++ } ++ return result; ++} ++ ++/// Prints to `file` with an indentation of `ind` spaces. ++static void fprintf_ind(FILE *file, int ind, const char *format, ...) { ++ fprintf(file, "%*s", ind, ""); ++ va_list args; ++ va_start(args, format); ++ vfprintf(file, format, args); ++ va_end(args); ++} ++ ++/// Dumps the image info to `file` with an indentation of `ind` spaces. ++static void gr_dump_image_info(FILE *file, Image *img, int ind) { ++ if (!img) { ++ fprintf_ind(file, ind, "Image is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Image %u\n", img->image_id); ++ ind += 4; ++ fprintf_ind(file, ind, "number: %u\n", img->image_number); ++ fprintf_ind(file, ind, "global command index: %lu\n", ++ img->global_command_index); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, ++ gr_ago(now - img->atime)); ++ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, ++ img->pix_height); ++ fprintf_ind(file, ind, "cur frame start time: %ld %s\n", ++ img->current_frame_time, ++ gr_ago(now - img->current_frame_time)); ++ if (img->next_redraw) ++ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", ++ img->next_redraw, img->next_redraw - now); ++ fprintf_ind(file, ind, "total disk size: %u KiB\n", ++ img->total_disk_size / 1024); ++ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); ++ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); ++ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); ++ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); ++ fprintf_ind(file, ind, "default_placement: %u\n", ++ img->default_placement); ++} ++ ++/// Dumps the frame info to `file` with an indentation of `ind` spaces. ++static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { ++ if (!frame) { ++ fprintf_ind(file, ind, "Frame is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Frame %d\n", frame->index); ++ ind += 4; ++ if (frame->index == 0) { ++ fprintf_ind(file, ind, "NOT INITIALIZED\n"); ++ return; ++ } ++ if (frame->uploading_failure) ++ fprintf_ind(file, ind, "uploading failure: %s\n", ++ image_uploading_failure_strings ++ [frame->uploading_failure]); ++ fprintf_ind(file, ind, "gap: %d\n", frame->gap); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, ++ gr_ago(now - frame->atime)); ++ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, ++ frame->data_pix_height); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (access(filename, F_OK) != -1) ++ fprintf_ind(file, ind, "file: %s\n", ++ sanitized_filename(filename)); ++ else ++ fprintf_ind(file, ind, "not on disk\n"); ++ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); ++ if (frame->imlib_object) { ++ unsigned ram_size = gr_frame_current_ram_size(frame); ++ fprintf_ind(file, ind, ++ "loaded into ram, size: %d " ++ "KiB\n", ++ ram_size / 1024); ++ } else { ++ fprintf_ind(file, ind, "not loaded into ram\n"); ++ } ++} ++ ++/// Dumps the placement info to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) { ++ fprintf_ind(file, ind, "Placement is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); ++ ind += 4; ++ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, ++ gr_ago(now - placement->atime)); ++ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); ++ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, ++ placement->rows); ++ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, ++ placement->scaled_ch); ++ fprintf_ind(file, ind, "ram per frame: %u KiB\n", ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ unsigned ram_size = gr_placement_current_ram_size(placement); ++ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); ++} ++ ++/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) ++ return; ++ int frameidx = 1; ++ foreach_pixmap(*placement, pixmap, { ++ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, ++ pixmap); ++ ++frameidx; ++ }); ++} ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state() { ++ FILE *file = stderr; ++ int ind = 0; ++ fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); ++ fprintf_ind(file, ind, ++ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " ++ "sizeof(ImagePlacement) = %lu\n", ++ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); ++ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); ++ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); ++ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", ++ images_ram_size / 1024); ++ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", ++ images_disk_size / 1024); ++ ++ Milliseconds now = gr_now_ms(); ++ ++ int64_t images_ram_size_computed = 0; ++ int64_t images_disk_size_computed = 0; ++ ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ fprintf_ind(file, ind, "----------------\n"); ++ gr_dump_image_info(file, img, 0); ++ int64_t total_disk_size_computed = 0; ++ int total_duration_computed = 0; ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ if (frame->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ total_duration_computed += frame->gap; ++ images_disk_size_computed += frame->disk_size; ++ total_disk_size_computed += frame->disk_size; ++ if (frame->imlib_object) ++ images_ram_size_computed += ++ gr_frame_current_ram_size(frame); ++ }); ++ if (img->total_disk_size != total_disk_size_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_disk_size is %u, but " ++ "computed value is %ld\n", ++ img->total_disk_size, total_disk_size_computed); ++ } ++ if (img->total_duration != total_duration_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_duration is %d, but computed " ++ "value is %d\n", ++ img->total_duration, total_duration_computed); ++ } ++ kh_foreach_value(img->placements, placement, { ++ gr_dump_placement_info(file, placement, 4); ++ if (placement->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ fprintf_ind(file, 8, ++ "Pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 12); ++ unsigned ram_size = ++ gr_placement_current_ram_size(placement); ++ images_ram_size_computed += ram_size; ++ }); ++ }); ++ if (images_ram_size != images_ram_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_ram_size is %ld, but computed value " ++ "is %ld\n", ++ images_ram_size, images_ram_size_computed); ++ } ++ if (images_disk_size != images_disk_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_disk_size is %ld, but computed value " ++ "is %ld\n", ++ images_disk_size, images_disk_size_computed); ++ } ++ fprintf_ind(file, ind, "===========================================\n"); ++} ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++// TODO: Currently we do this for the first frame only. Not sure what to do with ++// animations. ++void gr_preview_image(uint32_t image_id, const char *exec) { ++ char command[256]; ++ size_t len; ++ Image *img = gr_find_image(image_id); ++ if (img) { ++ ImageFrame *frame = &img->first_frame; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (frame->disk_size == 0) { ++ len = snprintf(command, 255, ++ "xmessage 'Image with id=%u is not " ++ "fully copied to %s'", ++ image_id, sanitized_filename(filename)); ++ } else { ++ len = snprintf(command, 255, "%s %s &", exec, ++ sanitized_filename(filename)); ++ } ++ } else { ++ len = snprintf(command, 255, ++ "xmessage 'Cannot find image with id=%u'", ++ image_id); ++ } ++ if (len > 255) { ++ fprintf(stderr, "error: command too long: %s\n", command); ++ snprintf(command, 255, "xmessage 'error: command too long'"); ++ } ++ if (system(command) != 0) { ++ fprintf(stderr, "error: could not execute command %s\n", ++ command); ++ } ++} ++ ++/// Executes `<st> -e less <file>` where <file> is the name of a temporary file ++/// containing the information about an image and placement, and <st> is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable) { ++ char filename[MAX_FILENAME_SIZE]; ++ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); ++ FILE *file = fopen(filename, "w"); ++ if (!file) { ++ perror("fopen"); ++ return; ++ } ++ // Basic information about the cell. ++ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); ++ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); ++ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); ++ fprintf(file, "classic/unicode placeholder = %s\n", ++ is_classic_placeholder ? "classic" : "unicode"); ++ fprintf(file, "original diacritic count = %d\n", diacritic_count); ++ // Information about the image and the placement. ++ Image *img = gr_find_image(image_id); ++ ImagePlacement *placement = gr_find_placement(img, placement_id); ++ gr_dump_image_info(file, img, 0); ++ gr_dump_placement_info(file, placement, 0); ++ if (img) { ++ fprintf(file, "Frames:\n"); ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ }); ++ } ++ if (placement) { ++ fprintf(file, "Placement pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 4); ++ } ++ fclose(file); ++ char *argv[] = {st_executable, "-e", "less", filename, NULL}; ++ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) { ++ perror("posix_spawnp"); ++ return; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Appending and displaying image rectangles. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Displays debug information in the rectangle using colors col1 and col2. ++static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2, ++ const char *message) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ char info[MAX_INFO_LEN]; ++ if (rect->placement_id) ++ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->placement_id, ++ rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ else ++ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ XSetForeground(disp, gc, col1); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 4, ++ rect->screen_y_pix + h_pix - 3, info, strlen(info)); ++ XSetForeground(disp, gc, col2); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 2, ++ rect->screen_y_pix + h_pix - 5, info, strlen(info)); ++ XFreeGC(disp, gc); ++} ++ ++/// Draws a rectangle (bounding box) for debugging. ++static void gr_showrect(Drawable buf, ImageRect *rect) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ XSetForeground(disp, gc, 0xFF00FF00); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix, ++ w_pix - 1, h_pix - 1); ++ XSetForeground(disp, gc, 0xFFFF0000); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1, ++ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3); ++ XFreeGC(disp, gc); ++} ++ ++/// Updates the next redraw time for the given row. Resizes the ++/// next_redraw_times array if needed. ++static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) { ++ if (next_redraw == 0) ++ return; ++ if (row >= kv_size(next_redraw_times)) { ++ size_t old_size = kv_size(next_redraw_times); ++ kv_a(Milliseconds, next_redraw_times, row); ++ for (size_t i = old_size; i <= row; ++i) ++ kv_A(next_redraw_times, i) = 0; ++ } ++ Milliseconds old_value = kv_A(next_redraw_times, row); ++ if (old_value == 0 || old_value > next_redraw) ++ kv_A(next_redraw_times, row) = next_redraw; ++} ++ ++/// Draws the given part of an image. ++static void gr_drawimagerect(Drawable buf, ImageRect *rect) { ++ ImagePlacement *placement = ++ gr_find_image_and_placement(rect->image_id, rect->placement_id); ++ // If the image does not exist or image display is switched off, draw ++ // the bounding box. ++ if (!placement || !graphics_display_images) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ Image *img = placement->image; ++ ++ if (img->last_redraw < drawing_start_time) { ++ // This is the first time we draw this image in this redraw ++ // cycle. Update the frame index we are going to display. Note ++ // that currently all image placements are synchronized. ++ int old_frame = img->current_frame; ++ gr_update_frame_index(img, drawing_start_time); ++ img->last_redraw = drawing_start_time; ++ } ++ ++ // Adjust next redraw times for the rows of this image rect. ++ if (img->next_redraw) { ++ for (int row = rect->screen_y_row; ++ row <= rect->screen_y_row + rect->img_end_row - ++ rect->img_start_row - 1; ++row) { ++ gr_update_next_redraw_time( ++ row, img->next_redraw); ++ } ++ } ++ ++ // Load the frame. ++ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw, ++ rect->ch); ++ ++ // If the image couldn't be loaded, display the bounding box. ++ if (!pixmap) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ int src_x = rect->img_start_col * rect->cw; ++ int src_y = rect->img_start_row * rect->ch; ++ int width = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int height = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ int dst_x = rect->screen_x_pix; ++ int dst_y = rect->screen_y_pix; ++ ++ // Display the image. ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ ++ // Create an xrender picture for the window. ++ XRenderPictFormat *win_format = ++ XRenderFindVisualFormat(disp, vis); ++ Picture window_pic = ++ XRenderCreatePicture(disp, buf, win_format, 0, NULL); ++ ++ // If needed, invert the image pixmap. Note that this naive approach of ++ // inverting the pixmap is not entirely correct, because the pixmap is ++ // premultiplied. But the result is good enough to visually indicate ++ // selection. ++ if (rect->reverse) { ++ unsigned pixmap_w = ++ (unsigned)placement->cols * placement->scaled_cw; ++ unsigned pixmap_h = ++ (unsigned)placement->rows * placement->scaled_ch; ++ Pixmap invpixmap = ++ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32); ++ XGCValues gcv = {.function = GXcopyInverted}; ++ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv); ++ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w, ++ pixmap_h, 0, 0); ++ XFreeGC(disp, gc); ++ pixmap = invpixmap; ++ } ++ ++ // Create a picture for the image pixmap. ++ XRenderPictFormat *pic_format = ++ XRenderFindStandardFormat(disp, PictStandardARGB32); ++ Picture pixmap_pic = ++ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL); ++ ++ // Composite the image onto the window. In the reverse mode we ignore ++ // the alpha channel of the image because the naive inversion above ++ // seems to invert the alpha channel as well. ++ int pictop = rect->reverse ? PictOpSrc : PictOpOver; ++ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, ++ src_x, src_y, src_x, src_y, dst_x, dst_y, width, ++ height); ++ ++ // Free resources ++ XRenderFreePicture(disp, pixmap_pic); ++ XRenderFreePicture(disp, window_pic); ++ if (rect->reverse) ++ XFreePixmap(disp, pixmap); ++ ++ // In debug mode always draw bounding boxes and print info. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) { ++ gr_showrect(buf, rect); ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ } ++} ++ ++/// Removes the given image rectangle. ++static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); } ++ ++/// Returns the bottom coordinate of the rect. ++static int gr_getrectbottom(ImageRect *rect) { ++ return rect->screen_y_pix + ++ (rect->img_end_row - rect->img_start_row) * rect->ch; ++} ++ ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch) { ++ current_cw = cw; ++ current_ch = ch; ++ this_redraw_cycle_loaded_files = 0; ++ this_redraw_cycle_loaded_pixmaps = 0; ++ drawing_start_time = gr_now_ms(); ++ imlib_context_set_drawable(buf); ++} ++ ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf) { ++ // Draw and then delete all known image rectangles. ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!rect->image_id) ++ continue; ++ gr_drawimagerect(buf, rect); ++ gr_freerect(rect); ++ } ++ ++ // Compute the delay until the next redraw as the minimum of the next ++ // redraw delays for all rows. ++ Milliseconds drawing_end_time = gr_now_ms(); ++ graphics_next_redraw_delay = INT_MAX; ++ for (int row = 0; row < kv_size(next_redraw_times); ++row) { ++ Milliseconds row_next_redraw = kv_A(next_redraw_times, row); ++ if (row_next_redraw > 0) { ++ int delay = MAX(graphics_animation_min_delay, ++ row_next_redraw - drawing_end_time); ++ graphics_next_redraw_delay = ++ MIN(graphics_next_redraw_delay, delay); ++ } ++ } ++ ++ // In debug mode display additional info. ++ if (graphics_debug_mode) { ++ int milliseconds = drawing_end_time - drawing_start_time; ++ ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ const char *debug_mode_str = ++ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES ++ ? "(boxes shown) " ++ : ""; ++ int redraw_delay = graphics_next_redraw_delay == INT_MAX ++ ? -1 ++ : graphics_next_redraw_delay; ++ char info[MAX_INFO_LEN]; ++ snprintf(info, MAX_INFO_LEN, ++ "%sRender time: %d ms ram %ld K disk %ld K count " ++ "%d cell %dx%d delay %d", ++ debug_mode_str, milliseconds, images_ram_size / 1024, ++ images_disk_size / 1024, kh_size(images), current_cw, ++ current_ch, redraw_delay); ++ XSetForeground(disp, gc, 0xFF000000); ++ XFillRectangle(disp, buf, gc, 0, 0, 600, 16); ++ XSetForeground(disp, gc, 0xFFFFFFFF); ++ XDrawString(disp, buf, gc, 0, 14, info, strlen(info)); ++ XFreeGC(disp, gc); ++ ++ if (milliseconds > 0) { ++ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n", ++ info, this_redraw_cycle_loaded_files, ++ this_redraw_cycle_loaded_pixmaps); ++ } ++ } ++ ++ // Check the limits in case we have used too much ram for placements. ++ gr_check_limits(); ++} ++ ++// Add an image rectangle to the list of rectangles to draw. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse) { ++ current_cw = cw; ++ current_ch = ch; ++ ++ ImageRect new_rect; ++ new_rect.image_id = image_id; ++ new_rect.placement_id = placement_id; ++ new_rect.img_start_col = img_start_col; ++ new_rect.img_end_col = img_end_col; ++ new_rect.img_start_row = img_start_row; ++ new_rect.img_end_row = img_end_row; ++ new_rect.screen_y_row = y_row; ++ new_rect.screen_x_pix = x_pix; ++ new_rect.screen_y_pix = y_pix; ++ new_rect.ch = ch; ++ new_rect.cw = cw; ++ new_rect.reverse = reverse; ++ ++ // Display some red text in debug mode. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? "); ++ ++ // If it's the empty image (image_id=0) or an empty rectangle, do ++ // nothing. ++ if (image_id == 0 || img_end_col - img_start_col <= 0 || ++ img_end_row - img_start_row <= 0) ++ return; ++ // Try to find a rect to merge with. ++ ImageRect *free_rect = NULL; ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (rect->image_id == 0) { ++ if (!free_rect) ++ free_rect = rect; ++ continue; ++ } ++ if (rect->image_id != image_id || ++ rect->placement_id != placement_id || rect->cw != cw || ++ rect->ch != ch || rect->reverse != reverse) ++ continue; ++ // We only support the case when the new stripe is added to the ++ // bottom of an existing rectangle and they are perfectly ++ // aligned. ++ if (rect->img_end_row == img_start_row && ++ gr_getrectbottom(rect) == y_pix) { ++ if (rect->img_start_col == img_start_col && ++ rect->img_end_col == img_end_col && ++ rect->screen_x_pix == x_pix) { ++ rect->img_end_row = img_end_row; ++ return; ++ } ++ } ++ } ++ // If we haven't merged the new rect with any existing rect, and there ++ // is no free rect, we have to render one of the existing rects. ++ if (!free_rect) { ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!free_rect || gr_getrectbottom(free_rect) > ++ gr_getrectbottom(rect)) ++ free_rect = rect; ++ } ++ gr_drawimagerect(buf, free_rect); ++ gr_freerect(free_rect); ++ } ++ // Start a new rectangle in `free_rect`. ++ *free_rect = new_rect; ++} ++ ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows) { ++ if (rows < kv_size(next_redraw_times)) ++ kv_size(next_redraw_times) = rows; ++ if (rows * 2 < kv_max(next_redraw_times)) ++ kv_resize(Milliseconds, next_redraw_times, rows); ++ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) { ++ if (dirty[i]) { ++ kv_A(next_redraw_times, i) = 0; ++ continue; ++ } ++ Milliseconds next_update = kv_A(next_redraw_times, i); ++ if (next_update > 0 && next_update <= drawing_start_time) { ++ dirty[i] = 1; ++ kv_A(next_redraw_times, i) = 0; ++ } ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Command parsing and handling. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// A parsed kitty graphics protocol command. ++typedef struct { ++ /// The command itself, without the 'G'. ++ char *command; ++ /// The payload (after ';'). ++ char *payload; ++ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'. ++ char action; ++ /// 'q=', 1 to suppress OK response, 2 to suppress errors too. ++ int quiet; ++ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with ++ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to ++ /// 32-bit pixel data. ++ int format; ++ /// 'o=', may be 'z' for RFC 1950 ZLIB. ++ int compression; ++ /// 't=', may be 'f', 't' or 'd'. ++ char transmission_medium; ++ /// 'd=' ++ char delete_specifier; ++ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'. ++ /// When 'a=f', this is the size of the frame rectangle when composed on ++ /// top of another frame. ++ int frame_pix_width, frame_pix_height; ++ /// 'x=', 'y=' - top-left corner of the source rectangle. ++ int src_pix_x, src_pix_y; ++ /// 'w=', 'h=' - width and height of the source rectangle. ++ int src_pix_width, src_pix_height; ++ /// 'r=', 'c=' ++ int rows, columns; ++ /// 'i=' ++ uint32_t image_id; ++ /// 'I=' ++ uint32_t image_number; ++ /// 'p=' ++ uint32_t placement_id; ++ /// 'm=', may be 0 or 1. ++ int more; ++ /// True if either 'm=0' or 'm=1' is specified. ++ char is_data_transmission; ++ /// True if turns out that this command is a continuation of a data ++ /// transmission and not the first one for this image. Populated by ++ /// `gr_handle_transmit_command`. ++ char is_direct_transmission_continuation; ++ /// 'S=', used to check the size of uploaded data. ++ int size; ++ /// 'U=', whether it's a virtual placement for Unicode placeholders. ++ int virtual; ++ /// 'C=', if true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++ // --------------------------------------------------------------------- ++ // Animation-related fields. Their keys often overlap with keys of other ++ // commands, so these make sense only if the action is 'a=f' (frame ++ // transmission) or 'a=a' (animation control). ++ // ++ // 'x=' and 'y=', the relative position of the frame image when it's ++ // composed on top of another frame. ++ int frame_dst_pix_x, frame_dst_pix_y; ++ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of ++ /// the background color or frame. ++ char replace_instead_of_blending; ++ /// 'Y=', the background color in the 0xRRGGBBAA format (still ++ /// transmitted as a decimal number). ++ uint32_t background_color; ++ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame. ++ int background_frame; ++ /// (Only for 'a=a'). 'c=', sets the index of the current frame. ++ int current_frame; ++ /// 'r=', the 1-based index of the frame to edit. ++ int edit_frame; ++ /// 'z=', the duration of the frame. Zero if not specified, negative if ++ /// the frame is gapless (i.e. skipped). ++ int gap; ++ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the ++ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop. ++ int animation_state; ++ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the ++ /// animation will loop. 1 to loop infinitely, N to loop N-1 times. ++ int loops; ++} GraphicsCommand; ++ ++/// Replaces all non-printed characters in `str` with '?' and truncates the ++/// string to `max_size`, maybe inserting ellipsis at the end. ++static void sanitize_str(char *str, size_t max_size) { ++ assert(max_size >= 4); ++ for (size_t i = 0; i < max_size; ++i) { ++ unsigned c = str[i]; ++ if (c == '\0') ++ return; ++ if (c >= 128 || !isprint(c)) ++ str[i] = '?'; ++ } ++ str[max_size - 1] = '\0'; ++ str[max_size - 2] = '.'; ++ str[max_size - 3] = '.'; ++ str[max_size - 4] = '.'; ++} ++ ++/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be ++/// careful. ++static const char *sanitized_filename(const char *str) { ++ static char buf[MAX_FILENAME_SIZE]; ++ strncpy(buf, str, sizeof(buf)); ++ sanitize_str(buf, sizeof(buf)); ++ return buf; ++} ++ ++/// Creates a response to the current command in `graphics_command_result`. ++static void gr_createresponse(uint32_t image_id, uint32_t image_number, ++ uint32_t placement_id, const char *msg) { ++ if (!image_id && !image_number && !placement_id) { ++ // Nobody expects the response in this case, so just print it to ++ // stderr. ++ fprintf(stderr, ++ "error: No image id or image number or placement_id, " ++ "but still there is a response: %s\n", ++ msg); ++ return; ++ } ++ char *buf = graphics_command_result.response; ++ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN; ++ size_t written; ++ written = snprintf(buf, maxlen, "\033_G"); ++ buf += written; ++ maxlen -= written; ++ if (image_id) { ++ written = snprintf(buf, maxlen, "i=%u,", image_id); ++ buf += written; ++ maxlen -= written; ++ } ++ if (image_number) { ++ written = snprintf(buf, maxlen, "I=%u,", image_number); ++ buf += written; ++ maxlen -= written; ++ } ++ if (placement_id) { ++ written = snprintf(buf, maxlen, "p=%u,", placement_id); ++ buf += written; ++ maxlen -= written; ++ } ++ buf[-1] = ';'; ++ written = snprintf(buf, maxlen, "%s\033\\", msg); ++ buf += written; ++ maxlen -= written; ++ buf[-2] = '\033'; ++ buf[-1] = '\\'; ++} ++ ++/// Creates the 'OK' response to the current command, unless suppressed or a ++/// non-final data transmission. ++static void gr_reportsuccess_cmd(GraphicsCommand *cmd) { ++ if (cmd->quiet < 1 && !cmd->more) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, "OK"); ++} ++ ++/// Creates the 'OK' response to the current command (unless suppressed). ++static void gr_reportsuccess_frame(ImageFrame *frame) { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ if (frame->quiet < 1) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, "OK"); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command); ++ if (cmd->quiet < 2) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, errmsg); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ if (!frame) { ++ fprintf(stderr, "%s\n", errmsg); ++ gr_createresponse(0, 0, 0, errmsg); ++ } else { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ fprintf(stderr, "%s id=%u\n", errmsg, id); ++ if (frame->quiet < 2) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, ++ errmsg); ++ } ++} ++ ++/// Loads an image and creates a success/failure response. Returns `frame`, or ++/// NULL if it's a query action and the image was deleted. ++static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) { ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) { ++ gr_reporterror_frame(frame, "EBADF: could not load image"); ++ } else { ++ gr_reportsuccess_frame(frame); ++ } ++ // If it was a query action, discard the image. ++ if (frame->image->query_id) { ++ gr_delete_image(frame->image); ++ return NULL; ++ } ++ return frame; ++} ++ ++/// Creates an appropriate uploading failure response to the current command. ++static void gr_reportuploaderror(ImageFrame *frame) { ++ switch (frame->uploading_failure) { ++ case 0: ++ return; ++ case ERROR_CANNOT_OPEN_CACHED_FILE: ++ gr_reporterror_frame(frame, ++ "EIO: could not create a file for image"); ++ break; ++ case ERROR_OVER_SIZE_LIMIT: ++ gr_reporterror_frame( ++ frame, ++ "EFBIG: the size of the uploaded image exceeded " ++ "the image size limit %u", ++ graphics_max_single_image_file_size); ++ break; ++ case ERROR_UNEXPECTED_SIZE: ++ gr_reporterror_frame(frame, ++ "EINVAL: the size of the uploaded image %u " ++ "doesn't match the expected size %u", ++ frame->disk_size, frame->expected_size); ++ break; ++ }; ++} ++ ++/// Displays a non-virtual placement. This functions records the information in ++/// `graphics_command_result`, the placeholder itself is created by the terminal ++/// after handling the current command in the graphics module. ++static void gr_display_nonvirtual_placement(ImagePlacement *placement) { ++ if (placement->virtual) ++ return; ++ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS) ++ return; ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ // Populate the information about the placeholder which will be created ++ // by the terminal. ++ graphics_command_result.create_placeholder = 1; ++ graphics_command_result.placeholder.image_id = placement->image->image_id; ++ graphics_command_result.placeholder.placement_id = placement->placement_id; ++ graphics_command_result.placeholder.columns = placement->cols; ++ graphics_command_result.placeholder.rows = placement->rows; ++ graphics_command_result.placeholder.do_not_move_cursor = ++ placement->do_not_move_cursor; ++ GR_LOG("Creating a placeholder for %u/%u %d x %d\n", ++ placement->image->image_id, placement->placement_id, ++ placement->cols, placement->rows); ++} ++ ++/// Marks the rows that are occupied by the image as dirty. ++static void gr_schedule_image_redraw(Image *img) { ++ if (!img) ++ return; ++ gr_schedule_image_redraw_by_id(img->image_id); ++} ++ ++/// Appends data from `payload` to the frame `frame` when using direct ++/// transmission. Note that we report errors only for the final command ++/// (`!more`) to avoid spamming the client. If the frame is not specified, use ++/// the image id and frame index we are currently uploading. ++static void gr_append_data(ImageFrame *frame, const char *payload, int more) { ++ if (!frame) { ++ Image *img = gr_find_image(current_upload_image_id); ++ frame = gr_get_frame(img, current_upload_frame_index); ++ GR_LOG("Appending data to image %u frame %d\n", ++ current_upload_image_id, current_upload_frame_index); ++ if (!img) ++ GR_LOG("ERROR: this image doesn't exist\n"); ++ if (!frame) ++ GR_LOG("ERROR: this frame doesn't exist\n"); ++ } ++ if (!more) { ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ } ++ if (!frame) { ++ if (!more) ++ gr_reporterror_frame(NULL, "ENOENT: could not find the " ++ "image to append data to"); ++ return; ++ } ++ if (frame->status != STATUS_UPLOADING) { ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // Decode the data. ++ size_t data_size = 0; ++ char *data = gr_base64dec(payload, &data_size); ++ ++ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size, ++ frame->disk_size + data_size); ++ ++ // Do not append this data if the image exceeds the size limit. ++ if (frame->disk_size + data_size > ++ graphics_max_single_image_file_size || ++ frame->expected_size > graphics_max_single_image_file_size) { ++ free(data); ++ gr_delete_imagefile(frame); ++ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // If there is no open file corresponding to the image, create it. ++ if (!frame->open_file) { ++ gr_make_sure_tmpdir_exists(); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ FILE *file = fopen(filename, frame->disk_size ? "a" : "w"); ++ if (!file) { ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; ++ if (!more) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ frame->open_file = file; ++ } ++ ++ // Write data to the file and update disk size variables. ++ fwrite(data, 1, data_size, frame->open_file); ++ free(data); ++ frame->disk_size += data_size; ++ frame->image->total_disk_size += data_size; ++ images_disk_size += data_size; ++ gr_touch_frame(frame); ++ ++ if (more) { ++ current_upload_image_id = frame->image->image_id; ++ current_upload_frame_index = frame->index; ++ } else { ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ // Close the file. ++ if (frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ uint32_t placement_id = frame->image->default_placement; ++ if (frame->expected_size && ++ frame->expected_size != frame->disk_size) { ++ // Report failure if the uploaded image size doesn't ++ // match the expected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ gr_reportuploaderror(frame); ++ } else { ++ // Make sure to redraw all existing image instances. ++ gr_schedule_image_redraw(frame->image); ++ // Try to load the image into ram and report the result. ++ frame = gr_loadimage_and_report(frame); ++ // If there is a non-virtual image placement, we may ++ // need to display it. ++ if (frame && frame->index == 1) { ++ Image *img = frame->image; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_display_nonvirtual_placement(placement); ++ }); ++ } ++ } ++ } ++ ++ // Check whether we need to delete old images. ++ gr_check_limits(); ++} ++ ++/// Finds the image either by id or by number specified in the command and sets ++/// the image_id of `cmd` if the image was found. ++static Image *gr_find_image_for_command(GraphicsCommand *cmd) { ++ if (cmd->image_id) ++ return gr_find_image(cmd->image_id); ++ Image *img = NULL; ++ // If the image number is not specified, we can't find the image, unless ++ // it's a put command, in which case we will try the last image. ++ if (cmd->image_number == 0 && cmd->action == 'p') ++ img = gr_find_image(last_image_id); ++ else ++ img = gr_find_image_by_number(cmd->image_number); ++ if (img) ++ cmd->image_id = img->image_id; ++ return img; ++} ++ ++/// Creates a new image or a new frame in an existing image (depending on the ++/// command's action) and initializes its parameters from the command. ++static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) { ++ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 && ++ cmd->compression != 0) { ++ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only " ++ "for raw pixel data (f=32 or f=24)"); ++ // Even though we report an error, we still create an image. ++ } ++ ++ Image *img = NULL; ++ if (cmd->action == 'f') { ++ // If it's a frame transmission action, there must be an ++ // existing image. ++ img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return NULL; ++ } ++ } else { ++ // Otherwise create a new image object. If the action is `q`, ++ // we'll use random id instead of the one specified in the ++ // command. ++ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id; ++ img = gr_new_image(image_id); ++ if (!img) ++ return NULL; ++ if (cmd->action == 'q') ++ img->query_id = cmd->image_id; ++ else if (!cmd->image_id) ++ cmd->image_id = img->image_id; ++ // Set the image number. ++ img->image_number = cmd->image_number; ++ } ++ ++ ImageFrame *frame = gr_append_new_frame(img); ++ // Initialize the frame. ++ frame->expected_size = cmd->size; ++ frame->format = cmd->format; ++ frame->compression = cmd->compression; ++ frame->background_color = cmd->background_color; ++ frame->background_frame_index = cmd->background_frame; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ frame->blend = !cmd->replace_instead_of_blending; ++ frame->data_pix_width = cmd->frame_pix_width; ++ frame->data_pix_height = cmd->frame_pix_height; ++ if (cmd->action == 'f') { ++ frame->x = cmd->frame_dst_pix_x; ++ frame->y = cmd->frame_dst_pix_y; ++ } ++ // We save the quietness information in the frame because for direct ++ // transmission subsequent transmission command won't contain this info. ++ frame->quiet = cmd->quiet; ++ return frame; ++} ++ ++/// Removes a file if it actually looks like a temporary file. ++static void gr_delete_tmp_file(const char *filename) { ++ if (strstr(filename, "tty-graphics-protocol") == NULL) ++ return; ++ if (strstr(filename, "/tmp/") != filename) { ++ const char *tmpdir = getenv("TMPDIR"); ++ if (!tmpdir || !tmpdir[0] || ++ strstr(filename, tmpdir) != filename) ++ return; ++ } ++ unlink(filename); ++} ++ ++/// Handles a data transmission command. ++static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) { ++ // The default is direct transmission. ++ if (!cmd->transmission_medium) ++ cmd->transmission_medium = 'd'; ++ ++ // If neither id, nor image number is specified, and the transmission ++ // medium is 'd' (or unspecified), and there is an active direct upload, ++ // this is a continuation of the upload. ++ if (current_upload_image_id != 0 && cmd->image_id == 0 && ++ cmd->image_number == 0 && cmd->transmission_medium == 'd') { ++ cmd->image_id = current_upload_image_id; ++ GR_LOG("No images id is specified, continuing uploading %u\n", ++ cmd->image_id); ++ } ++ ++ ImageFrame *frame = NULL; ++ if (cmd->transmission_medium == 'f' || ++ cmd->transmission_medium == 't') { ++ // File transmission. ++ // Create a new image or a new frame of an existing image. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ // Decode the filename. ++ char *original_filename = gr_base64dec(cmd->payload, NULL); ++ GR_LOG("Copying image %s\n", ++ sanitized_filename(original_filename)); ++ // Stat the file and check that it's a regular file and not too ++ // big. ++ struct stat st; ++ int stat_res = stat(original_filename, &st); ++ const char *stat_error = NULL; ++ if (stat_res) ++ stat_error = strerror(errno); ++ else if (!S_ISREG(st.st_mode)) ++ stat_error = "Not a regular file"; ++ else if (st.st_size == 0) ++ stat_error = "The size of the file is zero"; ++ else if (st.st_size > graphics_max_single_image_file_size) ++ stat_error = "The file is too large"; ++ if (stat_error) { ++ gr_reporterror_cmd(cmd, ++ "EBADF: %s", stat_error); ++ fprintf(stderr, "Could not load the file %s\n", ++ sanitized_filename(original_filename)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ } else { ++ gr_make_sure_tmpdir_exists(); ++ // Build the filename for the cached copy of the file. ++ char cache_filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, cache_filename, ++ MAX_FILENAME_SIZE); ++ // We will create a symlink to the original file, and ++ // then copy the file to the temporary cache dir. We do ++ // this symlink trick mostly to be able to use cp for ++ // copying, and avoid escaping file name characters when ++ // calling system at the same time. ++ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0}; ++ strcat(tmp_filename_symlink, cache_filename); ++ strcat(tmp_filename_symlink, ".sym"); ++ char command[MAX_FILENAME_SIZE + 256]; ++ size_t len = ++ snprintf(command, MAX_FILENAME_SIZE + 255, ++ "cp '%s' '%s'", tmp_filename_symlink, ++ cache_filename); ++ if (len > MAX_FILENAME_SIZE + 255 || ++ symlink(original_filename, tmp_filename_symlink) || ++ system(command) != 0) { ++ gr_reporterror_cmd(cmd, ++ "EBADF: could not copy the " ++ "image to the cache dir"); ++ fprintf(stderr, ++ "Could not copy the image " ++ "%s (symlink %s) to %s", ++ sanitized_filename(original_filename), ++ tmp_filename_symlink, cache_filename); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ } else { ++ // Get the file size of the copied file. ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ frame->disk_size = st.st_size; ++ frame->image->total_disk_size += st.st_size; ++ images_disk_size += frame->disk_size; ++ if (frame->expected_size && ++ frame->expected_size != frame->disk_size) { ++ // The file has unexpected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ++ ERROR_UNEXPECTED_SIZE; ++ gr_reportuploaderror(frame); ++ } else { ++ // Everything seems fine, try to load ++ // and redraw existing instances. ++ gr_schedule_image_redraw(frame->image); ++ frame = gr_loadimage_and_report(frame); ++ } ++ } ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ // Delete the original file if it's temporary. ++ if (cmd->transmission_medium == 't') ++ gr_delete_tmp_file(original_filename); ++ } ++ free(original_filename); ++ gr_check_limits(); ++ } else if (cmd->transmission_medium == 'd') { ++ // Direct transmission (default if 't' is not specified). ++ frame = gr_get_last_frame(gr_find_image_for_command(cmd)); ++ if (frame && frame->status == STATUS_UPLOADING) { ++ // This is a continuation of the previous transmission. ++ cmd->is_direct_transmission_continuation = 1; ++ gr_append_data(frame, cmd->payload, cmd->more); ++ return frame; ++ } ++ // If no action is specified, it's not the first transmission ++ // command. If we couldn't find the image, something went wrong ++ // and we should just drop this command. ++ if (cmd->action == 0) ++ return NULL; ++ // Otherwise create a new image or frame structure. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ frame->status = STATUS_UPLOADING; ++ // Start appending data. ++ gr_append_data(frame, cmd->payload, cmd->more); ++ } else { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: transmission medium '%c' is not supported", ++ cmd->transmission_medium); ++ return NULL; ++ } ++ ++ return frame; ++} ++ ++/// Handles the 'put' command by creating a placement. ++static void gr_handle_put_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Create a placement. If a placement with the same id already exists, ++ // it will be deleted. If the id is zero, a random id will be generated. ++ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id); ++ placement->virtual = cmd->virtual; ++ placement->src_pix_x = cmd->src_pix_x; ++ placement->src_pix_y = cmd->src_pix_y; ++ placement->src_pix_width = cmd->src_pix_width; ++ placement->src_pix_height = cmd->src_pix_height; ++ placement->cols = cmd->columns; ++ placement->rows = cmd->rows; ++ placement->do_not_move_cursor = cmd->do_not_move_cursor; ++ ++ if (placement->virtual) { ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else if (placement->cols && placement->rows) { ++ // For classic placements the default is to stretch the image if ++ // both cols and rows are specified. ++ placement->scale_mode = SCALE_MODE_FILL; ++ } else if (placement->cols || placement->rows) { ++ // But if only one of them is specified, the default is to ++ // contain. ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else { ++ // If none of them are specified, the default is to use the ++ // original size. ++ placement->scale_mode = SCALE_MODE_NONE; ++ } ++ ++ // Display the placement unless it's virtual. ++ gr_display_nonvirtual_placement(placement); ++ ++ // Report success. ++ gr_reportsuccess_cmd(cmd); ++} ++ ++/// Information about what to delete. ++typedef struct DeletionData { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// If true, delete the image object if there are no more placements. ++ char delete_image_if_no_ref; ++} DeletionData; ++ ++/// The callback called for each cell to perform deletion. ++static int gr_deletion_callback(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic) { ++ DeletionData *del_data = data; ++ // Leave unicode placeholders alone. ++ if (!is_classic) ++ return 0; ++ if (del_data->image_id && del_data->image_id != image_id) ++ return 0; ++ if (del_data->placement_id && del_data->placement_id != placement_id) ++ return 0; ++ Image *img = gr_find_image(image_id); ++ // If the image is already deleted, just erase the placeholder. ++ if (!img) ++ return 1; ++ // Delete the placement. ++ if (placement_id) ++ gr_delete_placement(gr_find_placement(img, placement_id)); ++ // Delete the image if image deletion is requested (uppercase delete ++ // specifier) and there are no more placements. ++ if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0) ++ gr_delete_image(img); ++ return 1; ++} ++ ++/// Handles the delete command. ++static void gr_handle_delete_command(GraphicsCommand *cmd) { ++ DeletionData del_data = {0}; ++ del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0; ++ char d = tolower(cmd->delete_specifier); ++ ++ if (d == 'n') { ++ d = 'i'; ++ Image *img = gr_find_image_by_number(cmd->image_number); ++ if (!img) ++ return; ++ del_data.image_id = img->image_id; ++ } ++ ++ if (!d || d == 'a') { ++ // Delete all visible placements. ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else if (d == 'i') { ++ // Delete the specified image by image id and maybe placement ++ // id. ++ if (!del_data.image_id) ++ del_data.image_id = cmd->image_id; ++ if (!del_data.image_id) { ++ fprintf(stderr, ++ "ERROR: image id is not specified in the " ++ "delete command\n"); ++ return; ++ } ++ del_data.placement_id = cmd->placement_id; ++ // NOTE: It's not very clear whether we should delete the image ++ // even if there are no _visible_ placements to delete. We do ++ // this because otherwise there is no way to delete an image ++ // with virtual placements in one command. ++ if (!del_data.placement_id && del_data.delete_image_if_no_ref) ++ gr_delete_image(gr_find_image(cmd->image_id)); ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else { ++ fprintf(stderr, ++ "WARNING: unsupported value of the d key: '%c'. The " ++ "command is ignored.\n", ++ cmd->delete_specifier); ++ } ++} ++ ++static void gr_handle_animation_control_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (!img) { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Find the frame to edit, if requested. ++ ImageFrame *frame = NULL; ++ if (cmd->edit_frame) ++ frame = gr_get_frame(img, cmd->edit_frame); ++ if (cmd->edit_frame || cmd->gap) { ++ if (!frame) { ++ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found", ++ cmd->edit_frame); ++ return; ++ } ++ if (cmd->gap) { ++ img->total_duration -= frame->gap; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ } ++ } ++ ++ // Set animation-related parameters of the image. ++ if (cmd->current_frame) ++ img->current_frame = cmd->current_frame; ++ if (cmd->animation_state) { ++ if (cmd->animation_state == 1) { ++ img->animation_state = ANIMATION_STATE_STOPPED; ++ } else if (cmd->animation_state == 2) { ++ img->animation_state = ANIMATION_STATE_LOADING; ++ } else if (cmd->animation_state == 3) { ++ img->animation_state = ANIMATION_STATE_LOOPING; ++ } else { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: invalid animation state: %d", ++ cmd->animation_state); ++ } ++ } ++ // TODO: Set the number of loops to cmd->loops ++ ++ // Make sure we redraw all instances of the image. ++ gr_schedule_image_redraw(img); ++} ++ ++/// Handles a command. ++static void gr_handle_command(GraphicsCommand *cmd) { ++ if (!cmd->image_id && !cmd->image_number) { ++ // If there is no image id or image number, nobody expects a ++ // response, so set quiet to 2. ++ cmd->quiet = 2; ++ } ++ ImageFrame *frame = NULL; ++ switch (cmd->action) { ++ case 0: ++ // If no action is specified, it may be a data transmission ++ // command if 'm=' is specified. ++ if (cmd->is_data_transmission) { ++ gr_handle_transmit_command(cmd); ++ break; ++ } ++ gr_reporterror_cmd(cmd, "EINVAL: no action specified"); ++ break; ++ case 't': ++ case 'q': ++ case 'f': ++ // Transmit data. 'q' means query, which is basically the same ++ // as transmit, but the image is discarded, and the id is fake. ++ // 'f' appends a frame to an existing image. ++ gr_handle_transmit_command(cmd); ++ break; ++ case 'p': ++ // Display (put) the image. ++ gr_handle_put_command(cmd); ++ break; ++ case 'T': ++ // Transmit and display. ++ frame = gr_handle_transmit_command(cmd); ++ if (frame && !cmd->is_direct_transmission_continuation) { ++ gr_handle_put_command(cmd); ++ if (cmd->placement_id) ++ frame->image->initial_placement_id = ++ cmd->placement_id; ++ } ++ break; ++ case 'd': ++ gr_handle_delete_command(cmd); ++ break; ++ case 'a': ++ gr_handle_animation_control_command(cmd); ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c", ++ cmd->action); ++ return; ++ } ++} ++ ++/// A partially parsed key-value pair. ++typedef struct KeyAndValue { ++ char *key_start; ++ char *val_start; ++ unsigned key_len, val_len; ++} KeyAndValue; ++ ++/// Parses the value of a key and assigns it to the appropriate field of `cmd`. ++static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) { ++ char *key_start = kv->key_start; ++ char *key_end = key_start + kv->key_len; ++ char *value_start = kv->val_start; ++ char *value_end = value_start + kv->val_len; ++ // Currently all keys are one-character. ++ if (key_end - key_start != 1) { ++ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s", ++ key_end - key_start, key_start); ++ return; ++ } ++ long num = 0; ++ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' || ++ *key_start == 'o') { ++ // Some keys have one-character values. ++ if (value_end - value_start != 1) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: value of 'a', 't' or 'd' must be a " ++ "single char: %s", ++ key_start); ++ return; ++ } ++ } else { ++ // All the other keys have integer values. ++ char *num_end = NULL; ++ num = strtol(value_start, &num_end, 10); ++ if (num_end != value_end) { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: could not parse number value: %s", ++ key_start); ++ return; ++ } ++ } ++ switch (*key_start) { ++ case 'a': ++ cmd->action = *value_start; ++ break; ++ case 't': ++ cmd->transmission_medium = *value_start; ++ break; ++ case 'd': ++ cmd->delete_specifier = *value_start; ++ break; ++ case 'q': ++ cmd->quiet = num; ++ break; ++ case 'f': ++ cmd->format = num; ++ if (num != 0 && num != 24 && num != 32 && num != 100) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: unsupported format specification: %s", ++ key_start); ++ } ++ break; ++ case 'o': ++ cmd->compression = *value_start; ++ if (cmd->compression != 'z') { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: unsupported compression " ++ "specification: %s", ++ key_start); ++ } ++ break; ++ case 's': ++ if (cmd->action == 'a') ++ cmd->animation_state = num; ++ else ++ cmd->frame_pix_width = num; ++ break; ++ case 'v': ++ if (cmd->action == 'a') ++ cmd->loops = num; ++ else ++ cmd->frame_pix_height = num; ++ break; ++ case 'i': ++ cmd->image_id = num; ++ break; ++ case 'I': ++ cmd->image_number = num; ++ break; ++ case 'p': ++ cmd->placement_id = num; ++ break; ++ case 'x': ++ cmd->src_pix_x = num; ++ cmd->frame_dst_pix_x = num; ++ break; ++ case 'y': ++ if (cmd->action == 'f') ++ cmd->frame_dst_pix_y = num; ++ else ++ cmd->src_pix_y = num; ++ break; ++ case 'w': ++ cmd->src_pix_width = num; ++ break; ++ case 'h': ++ cmd->src_pix_height = num; ++ break; ++ case 'c': ++ if (cmd->action == 'f') ++ cmd->background_frame = num; ++ else if (cmd->action == 'a') ++ cmd->current_frame = num; ++ else ++ cmd->columns = num; ++ break; ++ case 'r': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->edit_frame = num; ++ else ++ cmd->rows = num; ++ break; ++ case 'm': ++ cmd->is_data_transmission = 1; ++ cmd->more = num; ++ break; ++ case 'S': ++ cmd->size = num; ++ break; ++ case 'U': ++ cmd->virtual = num; ++ break; ++ case 'X': ++ if (cmd->action == 'f') ++ cmd->replace_instead_of_blending = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'Y': ++ if (cmd->action == 'f') ++ cmd->background_color = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'z': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->gap = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'C': ++ cmd->do_not_move_cursor = num; ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s", ++ key_start); ++ return; ++ } ++} ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters. Returns 1 on success. ++int gr_parse_command(char *buf, size_t len) { ++ if (buf[0] != 'G') ++ return 0; ++ ++ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); ++ ++ global_command_counter++; ++ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); ++ ++ // Eat the 'G'. ++ ++buf; ++ --len; ++ ++ GraphicsCommand cmd = {.command = buf}; ++ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to ++ // parse the payload. ++ char state = 'k'; ++ // An array of partially parsed key-value pairs. ++ KeyAndValue key_vals[32]; ++ unsigned key_vals_count = 0; ++ char *key_start = buf; ++ char *key_end = NULL; ++ char *val_start = NULL; ++ char *val_end = NULL; ++ char *c = buf; ++ while (c - buf < len + 1) { ++ if (state == 'k') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ key_end = c; ++ gr_reporterror_cmd( ++ &cmd, "EINVAL: key without value: %s ", ++ key_start); ++ break; ++ case '=': ++ key_end = c; ++ state = 'v'; ++ val_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'v') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ val_end = c; ++ if (key_vals_count >= ++ sizeof(key_vals) / sizeof(*key_vals)) { ++ gr_reporterror_cmd(&cmd, ++ "EINVAL: too many " ++ "key-value pairs"); ++ break; ++ } ++ key_vals[key_vals_count].key_start = key_start; ++ key_vals[key_vals_count].val_start = val_start; ++ key_vals[key_vals_count].key_len = ++ key_end - key_start; ++ key_vals[key_vals_count].val_len = ++ val_end - val_start; ++ ++key_vals_count; ++ key_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'p') { ++ cmd.payload = c; ++ // break out of the loop, we don't check the payload ++ break; ++ } ++ ++c; ++ } ++ ++ // Set the action key ('a=') first because we need it to disambiguate ++ // some keys. Also set 'i=' and 'I=' for better error reporting. ++ for (unsigned i = 0; i < key_vals_count; ++i) { ++ if (key_vals[i].key_len == 1) { ++ char *start = key_vals[i].key_start; ++ if (*start == 'a' || *start == 'i' || *start == 'I') { ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ break; ++ } ++ } ++ } ++ // Set the rest of the keys. ++ for (unsigned i = 0; i < key_vals_count; ++i) ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ ++ if (!cmd.payload) ++ cmd.payload = buf + len; ++ ++ if (cmd.payload && cmd.payload[0]) ++ GR_LOG(" payload size: %ld\n", strlen(cmd.payload)); ++ ++ if (!graphics_command_result.error) ++ gr_handle_command(&cmd); ++ ++ if (graphics_debug_mode) { ++ fprintf(stderr, "Response: "); ++ for (const char *resp = graphics_command_result.response; ++ *resp != '\0'; ++resp) { ++ if (isprint(*resp)) ++ fprintf(stderr, "%c", *resp); ++ else ++ fprintf(stderr, "(0x%x)", *resp); ++ } ++ fprintf(stderr, "\n"); ++ } ++ ++ // Make sure that we suppress response if needed. Usually cmd.quiet is ++ // taken into account when creating the response, but it's not very ++ // reliable in the current implementation. ++ if (cmd.quiet) { ++ if (!graphics_command_result.error || cmd.quiet >= 2) ++ graphics_command_result.response[0] = '\0'; ++ } ++ ++ return 1; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// base64 decoding part is basically copied from st.c ++//////////////////////////////////////////////////////////////////////////////// ++ ++static const char gr_base64_digits[] = { ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, ++ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, ++ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ++ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, ++ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, ++ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; ++ ++static char gr_base64_getc(const char **src) { ++ while (**src && !isprint(**src)) ++ (*src)++; ++ return **src ? *((*src)++) : '='; /* emulate padding if string ends */ ++} ++ ++char *gr_base64dec(const char *src, size_t *size) { ++ size_t in_len = strlen(src); ++ char *result, *dst; ++ ++ result = dst = malloc((in_len + 3) / 4 * 3 + 1); ++ while (*src) { ++ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ ++ if (a == -1 || b == -1) ++ break; ++ ++ *dst++ = (a << 2) | ((b & 0x30) >> 4); ++ if (c == -1) ++ break; ++ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); ++ if (d == -1) ++ break; ++ *dst++ = ((c & 0x03) << 6) | d; ++ } ++ *dst = '\0'; ++ if (size) { ++ *size = dst - result; ++ } ++ return result; ++} +diff --git a/graphics.h b/graphics.h +new file mode 100644 +index 0000000..2e75dea +--- /dev/null ++++ b/graphics.h +@@ -0,0 +1,107 @@ ++ ++#include <stdint.h> ++#include <sys/types.h> ++#include <X11/Xlib.h> ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm); ++/// Deinitialize the graphics module. ++void gr_deinit(); ++ ++/// Add an image rectangle to a list if rectangles to draw. This function may ++/// actually draw some rectangles, or it may wait till more rectangles are ++/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. ++/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the ++/// part of the image to draw (row/col indices are zero-based, ends are ++/// excluded). ++/// - `x_col` and `y_row` are the coordinates of the top-left corner of the ++/// image in the terminal grid. ++/// - `x_pix` and `y_pix` are the same but in pixels. ++/// - `reverse` indicates whether colors should be inverted. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse); ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch); ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf); ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows); ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters (including '\0'). Returns 1 on success. ++/// Additional informations is returned through `graphics_command_result`. ++int gr_parse_command(char *buf, size_t len); ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++void gr_preview_image(uint32_t image_id, const char *command); ++ ++/// Executes `<st> -e less <file>` where <file> is the name of a temporary file ++/// containing the information about an image and placement, and <st> is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable); ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state(); ++ ++/// Unloads images to reduce RAM usage. ++void gr_unload_images_to_reduce_ram(); ++ ++/// Executes `callback` for each image cell. `callback` may return 1 to erase ++/// the cell or 0 to keep it. This function is implemented in `st.c`. ++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic), ++ void *data); ++ ++/// Marks all the rows containing the image with `image_id` as dirty. ++void gr_schedule_image_redraw_by_id(uint32_t image_id); ++ ++typedef enum { ++ GRAPHICS_DEBUG_NONE = 0, ++ GRAPHICS_DEBUG_LOG = 1, ++ GRAPHICS_DEBUG_LOG_AND_BOXES = 2, ++} GraphicsDebugMode; ++ ++/// Print additional information, draw bounding bounding boxes, etc. ++extern GraphicsDebugMode graphics_debug_mode; ++ ++/// Whether to display images or just draw bounding boxes. ++extern char graphics_display_images; ++ ++/// The time in milliseconds until the next redraw to update animations. ++/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`. ++extern int graphics_next_redraw_delay; ++ ++#define MAX_GRAPHICS_RESPONSE_LEN 256 ++ ++/// A structure representing the result of a graphics command. ++typedef struct { ++ /// Indicates if the terminal needs to be redrawn. ++ char redraw; ++ /// The response of the command that should be sent back to the client ++ /// (may be empty if the quiet flag is set). ++ char response[MAX_GRAPHICS_RESPONSE_LEN]; ++ /// Whether there was an error executing this command (not very useful, ++ /// the response must be sent back anyway). ++ char error; ++ /// Whether the terminal has to create a placeholder for a non-virtual ++ /// placement. ++ char create_placeholder; ++ /// The placeholder that needs to be created. ++ struct { ++ uint32_t rows, columns; ++ uint32_t image_id, placement_id; ++ char do_not_move_cursor; ++ } placeholder; ++} GraphicsCommandResult; ++ ++/// The result of a graphics command. ++extern GraphicsCommandResult graphics_command_result; +diff --git a/icat-mini.sh b/icat-mini.sh +new file mode 100755 +index 0000000..0a8ebab +--- /dev/null ++++ b/icat-mini.sh +@@ -0,0 +1,801 @@ ++#!/bin/sh ++ ++# vim: shiftwidth=4 ++ ++script_name="$(basename "$0")" ++ ++short_help="Usage: $script_name [OPTIONS] <image_file> ++ ++This is a script to display images in the terminal using the kitty graphics ++protocol with Unicode placeholders. It is very basic, please use something else ++if you have alternatives. ++ ++Options: ++ -h Show this help. ++ -s SCALE The scale of the image, may be floating point. ++ -c N, --cols N The number of columns. ++ -r N, --rows N The number of rows. ++ --max-cols N The maximum number of columns. ++ --max-rows N The maximum number of rows. ++ --cell-size WxH The cell size in pixels. ++ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. ++ --speed SPEED The multiplier for the animation speed (float). ++" ++ ++# Exit the script on keyboard interrupt ++trap "echo 'icat-mini was interrupted' >&2; exit 1" INT ++ ++cols="" ++rows="" ++file="" ++tty="/dev/tty" ++uploading_method="auto" ++cell_size="" ++scale=1 ++max_cols="" ++max_rows="" ++speed="" ++ ++# Parse the command line. ++while [ $# -gt 0 ]; do ++ case "$1" in ++ -c|--columns|--cols) ++ cols="$2" ++ shift 2 ++ ;; ++ -r|--rows|-l|--lines) ++ rows="$2" ++ shift 2 ++ ;; ++ -s|--scale) ++ scale="$2" ++ shift 2 ++ ;; ++ -h|--help) ++ echo "$short_help" ++ exit 0 ++ ;; ++ -m|--upload-method|--uploading-method) ++ uploading_method="$2" ++ shift 2 ++ ;; ++ --cell-size) ++ cell_size="$2" ++ shift 2 ++ ;; ++ --max-cols) ++ max_cols="$2" ++ shift 2 ++ ;; ++ --max-rows) ++ max_rows="$2" ++ shift 2 ++ ;; ++ --speed) ++ speed="$2" ++ shift 2 ++ ;; ++ --) ++ file="$2" ++ shift 2 ++ ;; ++ -*) ++ echo "Unknown option: $1" >&2 ++ exit 1 ++ ;; ++ *) ++ if [ -n "$file" ]; then ++ echo "Multiple image files are not supported: $file and $1" >&2 ++ exit 1 ++ fi ++ file="$1" ++ shift ++ ;; ++ esac ++done ++ ++file="$(realpath "$file")" ++ ++##################################################################### ++# Adjust the terminal state ++##################################################################### ++ ++stty_orig="$(stty -g < "$tty")" ++stty -echo < "$tty" ++# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some ++# horrible issues otherwise. ++stty susp undef < "$tty" ++stty -icanon < "$tty" ++ ++restore_echo() { ++ [ -n "$stty_orig" ] || return ++ stty $stty_orig < "$tty" ++} ++ ++trap restore_echo EXIT TERM ++ ++##################################################################### ++# Detect imagemagick ++##################################################################### ++ ++# If there is the 'magick' command, use it instead of separate 'convert' and ++# 'identify' commands. ++if command -v magick > /dev/null; then ++ identify="magick identify" ++ convert="magick" ++else ++ identify="identify" ++ convert="convert" ++fi ++ ++##################################################################### ++# Detect tmux ++##################################################################### ++ ++# Check if we are inside tmux. ++inside_tmux="" ++if [ -n "$TMUX" ]; then ++ case "$TERM" in ++ *tmux*|*screen*) ++ inside_tmux=1 ++ ;; ++ esac ++fi ++ ++##################################################################### ++# Compute the number of rows and columns ++##################################################################### ++ ++is_pos_int() { ++ if [ -z "$1" ]; then ++ return 1 # false ++ fi ++ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then ++ if [ "$1" -gt 0 ]; then ++ return 0 # true ++ fi ++ fi ++ return 1 # false ++} ++ ++if [ -n "$cols" ] || [ -n "$rows" ]; then ++ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then ++ echo "You can't specify both max-cols/rows and cols/rows" >&2 ++ exit 1 ++ fi ++fi ++ ++# Get the max number of cols and rows. ++[ -n "$max_cols" ] || max_cols="$(tput cols)" ++[ -n "$max_rows" ] || max_rows="$(tput lines)" ++if [ "$max_rows" -gt 255 ]; then ++ max_rows=255 ++fi ++ ++python_ioctl_command="import array, fcntl, termios ++buf = array.array('H', [0, 0, 0, 0]) ++fcntl.ioctl(0, termios.TIOCGWINSZ, buf) ++print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" ++ ++# Get the cell size in pixels if either cols or rows are not specified. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ cell_width="" ++ cell_height="" ++ # If the cell size is specified, use it. ++ if [ -n "$cell_size" ]; then ++ cell_width="${cell_size%x*}" ++ cell_height="${cell_size#*x}" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ echo "Invalid cell size: $cell_size" >&2 ++ exit 1 ++ fi ++ fi ++ # Otherwise try to use TIOCGWINSZ ioctl via python. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)" ++ cell_width="${cell_size_ioctl% *}" ++ cell_height="${cell_size_ioctl#* }" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width="" ++ cell_height="" ++ fi ++ fi ++ # If it didn't work, try to use csi XTWINOPS. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ if [ -n "$inside_tmux" ]; then ++ printf '\ePtmux;\e\e[16t\e\\' >> "$tty" ++ else ++ printf '\e[16t' >> "$tty" ++ fi ++ # The expected response will look like ^[[6;<height>;<width>t ++ term_response="" ++ while true; do ++ char=$(dd bs=1 count=1 <"$tty" 2>/dev/null) ++ if [ "$char" = "t" ]; then ++ break ++ fi ++ term_response="$term_response$char" ++ done ++ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" ++ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width=8 ++ cell_height=16 ++ fi ++ fi ++fi ++ ++# Compute a formula with bc and round to the nearest integer. ++bc_round() { ++ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" ++} ++ ++# Compute the number of rows and columns of the image. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ # Get the size of the image and its resolution. If it's an animation, use ++ # the first frame. ++ format_output="$($identify -format '%w %h\n' "$file" | head -1)" ++ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" ++ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" ++ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then ++ echo "Couldn't get image size from identify: $format_output" >&2 ++ echo >&2 ++ exit 1 ++ fi ++ opt_cols_expr="(${scale}*${img_width}/${cell_width})" ++ opt_rows_expr="(${scale}*${img_height}/${cell_height})" ++ if [ -z "$cols" ] && [ -z "$rows" ]; then ++ # If columns and rows are not specified, compute the optimal values ++ # using the information about rows and columns per inch. ++ cols="$(bc_round "$opt_cols_expr")" ++ rows="$(bc_round "$opt_rows_expr")" ++ # Make sure that automatically computed rows and columns are within some ++ # sane limits ++ if [ "$cols" -gt "$max_cols" ]; then ++ rows="$(bc_round "$rows * $max_cols / $cols")" ++ cols="$max_cols" ++ fi ++ if [ "$rows" -gt "$max_rows" ]; then ++ cols="$(bc_round "$cols * $max_rows / $rows")" ++ rows="$max_rows" ++ fi ++ elif [ -z "$cols" ]; then ++ # If only one dimension is specified, compute the other one to match the ++ # aspect ratio as close as possible. ++ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" ++ elif [ -z "$rows" ]; then ++ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" ++ fi ++ ++ if [ "$cols" -lt 1 ]; then ++ cols=1 ++ fi ++ if [ "$rows" -lt 1 ]; then ++ rows=1 ++ fi ++fi ++ ++##################################################################### ++# Generate an image id ++##################################################################### ++ ++image_id="" ++while [ -z "$image_id" ]; do ++ image_id="$(shuf -i 16777217-4294967295 -n 1)" ++ # Check that the id requires 24-bit fg colors. ++ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then ++ image_id="" ++ fi ++done ++ ++##################################################################### ++# Uploading the image ++##################################################################### ++ ++# Choose the uploading method ++if [ "$uploading_method" = "auto" ]; then ++ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then ++ uploading_method="direct" ++ else ++ uploading_method="file" ++ fi ++fi ++ ++# Functions to emit the start and the end of a graphics command. ++if [ -n "$inside_tmux" ]; then ++ # If we are in tmux we have to wrap the command in Ptmux. ++ graphics_command_start='\ePtmux;\e\e_G' ++ graphics_command_end='\e\e\\\e\\' ++else ++ graphics_command_start='\e_G' ++ graphics_command_end='\e\\' ++fi ++ ++start_gr_command() { ++ printf "$graphics_command_start" >> "$tty" ++} ++end_gr_command() { ++ printf "$graphics_command_end" >> "$tty" ++} ++ ++# Send a graphics command with the correct start and end ++gr_command() { ++ start_gr_command ++ printf '%s' "$1" >> "$tty" ++ end_gr_command ++} ++ ++# Send an uploading command. Usage: gr_upload <action> <command> <file> ++# Where <action> is a part of command that specifies the action, it will be ++# repeated for every chunk (if the method is direct), and <command> is the rest ++# of the command that specifies the image parameters. <action> and <command> ++# must not include the transmission method or ';'. ++# Example: ++# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++gr_upload() { ++ arg_action="$1" ++ arg_command="$2" ++ arg_file="$3" ++ if [ "$uploading_method" = "file" ]; then ++ # base64-encode the filename ++ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) ++ gr_command "${arg_action},${arg_command},t=f;${encoded_filename}" ++ fi ++ if [ "$uploading_method" = "direct" ]; then ++ # Create a temporary directory to store the chunked image. ++ chunkdir="$(mktemp -d)" ++ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then ++ echo "Can't create a temp dir" >&2 ++ exit 1 ++ fi ++ # base64-encode the file and split it into chunks. The size of each ++ # graphics command shouldn't be more than 4096, so we set the size of an ++ # encoded chunk to be 3968, slightly less than that. ++ chunk_size=3968 ++ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" ++ ++ # Issue a command indicating that we want to start data transmission for ++ # a new image. ++ gr_command "${arg_action},${arg_command},t=d,m=1" ++ ++ # Transmit chunks. ++ for chunk in "$chunkdir/chunk_"*; do ++ start_gr_command ++ printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty" ++ cat "$chunk" >> "$tty" ++ end_gr_command ++ rm "$chunk" ++ done ++ ++ # Tell the terminal that we are done. ++ gr_command "${arg_action},i=$image_id,m=0" ++ ++ # Remove the temporary directory. ++ rmdir "$chunkdir" ++ fi ++} ++ ++delayed_frame_dir_cleanup() { ++ arg_frame_dir="$1" ++ sleep 2 ++ if [ -n "$arg_frame_dir" ]; then ++ for frame in "$arg_frame_dir"/frame_*.png; do ++ rm "$frame" ++ done ++ rmdir "$arg_frame_dir" ++ fi ++} ++ ++upload_image_and_print_placeholder() { ++ # Check if the file is an animation. ++ frame_count=$($identify -format '%n\n' "$file" | head -n 1) ++ if [ "$frame_count" -gt 1 ]; then ++ # The file is an animation, decompose into frames and upload each frame. ++ frame_dir="$(mktemp -d)" ++ frame_dir="$HOME/temp/frames${frame_dir}" ++ mkdir -p "$frame_dir" ++ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then ++ echo "Can't create a temp dir for frames" >&2 ++ exit 1 ++ fi ++ ++ # Decompose the animation into separate frames. ++ $convert "$file" -coalesce "$frame_dir/frame_%06d.png" ++ ++ # Get all frame delays at once, in centiseconds, as a space-separated ++ # string. ++ delays=$($identify -format "%T " "$file") ++ ++ frame_number=1 ++ for frame in "$frame_dir"/frame_*.png; do ++ # Read the delay for the current frame and convert it from ++ # centiseconds to milliseconds. ++ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") ++ delay=$((delay * 10)) ++ # If the delay is 0, set it to 100ms. ++ if [ "$delay" -eq 0 ]; then ++ delay=100 ++ fi ++ ++ if [ -n "$speed" ]; then ++ delay=$(bc_round "$delay / $speed") ++ fi ++ ++ if [ "$frame_number" -eq 1 ]; then ++ # Upload the first frame with a=T ++ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" ++ # Set the delay for the first frame and also play the animation ++ # in loading mode (s=2). ++ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" ++ # Print the placeholder after the first frame to reduce the wait ++ # time. ++ print_placeholder ++ else ++ # Upload subsequent frames with a=f ++ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" ++ fi ++ ++ frame_number=$((frame_number + 1)) ++ done ++ ++ # Play the animation in loop mode (s=3). ++ gr_command "a=a,v=1,s=3,i=${image_id}" ++ ++ # Remove the temporary directory, but do it in the background with a ++ # delay to avoid removing files before they are loaded by the terminal. ++ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & ++ else ++ # The file is not an animation, upload it directly ++ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++ # Print the placeholder ++ print_placeholder ++ fi ++} ++ ++##################################################################### ++# Printing the image placeholder ++##################################################################### ++ ++print_placeholder() { ++ # Each line starts with the escape sequence to set the foreground color to ++ # the image id. ++ blue="$(expr "$image_id" % 256 )" ++ green="$(expr \( "$image_id" / 256 \) % 256 )" ++ red="$(expr \( "$image_id" / 65536 \) % 256 )" ++ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" ++ line_end="$(printf "\e[39;m")" ++ ++ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" ++ eval "id_diacritic=\$d${id4th}" ++ ++ # Reset the brush state, mostly to reset the underline color. ++ printf "\e[0m" ++ ++ # Fill the output with characters representing the image ++ for y in $(seq 0 "$(expr "$rows" - 1)"); do ++ eval "row_diacritic=\$d${y}" ++ printf '%s' "$line_start" ++ for x in $(seq 0 "$(expr "$cols" - 1)"); do ++ eval "col_diacritic=\$d${x}" ++ # Note that when $x is out of bounds, the column diacritic will ++ # be empty, meaning that the column should be guessed by the ++ # terminal. ++ if [ "$x" -ge "$num_diacritics" ]; then ++ printf '%s' "${placeholder}${row_diacritic}" ++ else ++ printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" ++ fi ++ done ++ printf '%s\n' "$line_end" ++ done ++ ++ printf "\e[0m" ++} ++ ++d0="̅" ++d1="̍" ++d2="̎" ++d3="̐" ++d4="̒" ++d5="̽" ++d6="̾" ++d7="̿" ++d8="͆" ++d9="͊" ++d10="͋" ++d11="͌" ++d12="͐" ++d13="͑" ++d14="͒" ++d15="͗" ++d16="͛" ++d17="ͣ" ++d18="ͤ" ++d19="ͥ" ++d20="ͦ" ++d21="ͧ" ++d22="ͨ" ++d23="ͩ" ++d24="ͪ" ++d25="ͫ" ++d26="ͬ" ++d27="ͭ" ++d28="ͮ" ++d29="ͯ" ++d30="҃" ++d31="҄" ++d32="҅" ++d33="҆" ++d34="҇" ++d35="֒" ++d36="֓" ++d37="֔" ++d38="֕" ++d39="֗" ++d40="֘" ++d41="֙" ++d42="֜" ++d43="֝" ++d44="֞" ++d45="֟" ++d46="֠" ++d47="֡" ++d48="֨" ++d49="֩" ++d50="֫" ++d51="֬" ++d52="֯" ++d53="ׄ" ++d54="ؐ" ++d55="ؑ" ++d56="ؒ" ++d57="ؓ" ++d58="ؔ" ++d59="ؕ" ++d60="ؖ" ++d61="ؗ" ++d62="ٗ" ++d63="٘" ++d64="ٙ" ++d65="ٚ" ++d66="ٛ" ++d67="ٝ" ++d68="ٞ" ++d69="ۖ" ++d70="ۗ" ++d71="ۘ" ++d72="ۙ" ++d73="ۚ" ++d74="ۛ" ++d75="ۜ" ++d76="۟" ++d77="۠" ++d78="ۡ" ++d79="ۢ" ++d80="ۤ" ++d81="ۧ" ++d82="ۨ" ++d83="۫" ++d84="۬" ++d85="ܰ" ++d86="ܲ" ++d87="ܳ" ++d88="ܵ" ++d89="ܶ" ++d90="ܺ" ++d91="ܽ" ++d92="ܿ" ++d93="݀" ++d94="݁" ++d95="݃" ++d96="݅" ++d97="݇" ++d98="݉" ++d99="݊" ++d100="߫" ++d101="߬" ++d102="߭" ++d103="߮" ++d104="߯" ++d105="߰" ++d106="߱" ++d107="߳" ++d108="ࠖ" ++d109="ࠗ" ++d110="࠘" ++d111="࠙" ++d112="ࠛ" ++d113="ࠜ" ++d114="ࠝ" ++d115="ࠞ" ++d116="ࠟ" ++d117="ࠠ" ++d118="ࠡ" ++d119="ࠢ" ++d120="ࠣ" ++d121="ࠥ" ++d122="ࠦ" ++d123="ࠧ" ++d124="ࠩ" ++d125="ࠪ" ++d126="ࠫ" ++d127="ࠬ" ++d128="࠭" ++d129="॑" ++d130="॓" ++d131="॔" ++d132="ྂ" ++d133="ྃ" ++d134="྆" ++d135="྇" ++d136="፝" ++d137="፞" ++d138="፟" ++d139="៝" ++d140="᤺" ++d141="ᨗ" ++d142="᩵" ++d143="᩶" ++d144="᩷" ++d145="᩸" ++d146="᩹" ++d147="᩺" ++d148="᩻" ++d149="᩼" ++d150="᭫" ++d151="᭭" ++d152="᭮" ++d153="᭯" ++d154="᭰" ++d155="᭱" ++d156="᭲" ++d157="᭳" ++d158="᳐" ++d159="᳑" ++d160="᳒" ++d161="᳚" ++d162="᳛" ++d163="᳠" ++d164="᷀" ++d165="᷁" ++d166="᷃" ++d167="᷄" ++d168="᷅" ++d169="᷆" ++d170="᷇" ++d171="᷈" ++d172="᷉" ++d173="᷋" ++d174="᷌" ++d175="᷑" ++d176="᷒" ++d177="ᷓ" ++d178="ᷔ" ++d179="ᷕ" ++d180="ᷖ" ++d181="ᷗ" ++d182="ᷘ" ++d183="ᷙ" ++d184="ᷚ" ++d185="ᷛ" ++d186="ᷜ" ++d187="ᷝ" ++d188="ᷞ" ++d189="ᷟ" ++d190="ᷠ" ++d191="ᷡ" ++d192="ᷢ" ++d193="ᷣ" ++d194="ᷤ" ++d195="ᷥ" ++d196="ᷦ" ++d197="᷾" ++d198="⃐" ++d199="⃑" ++d200="⃔" ++d201="⃕" ++d202="⃖" ++d203="⃗" ++d204="⃛" ++d205="⃜" ++d206="⃡" ++d207="⃧" ++d208="⃩" ++d209="⃰" ++d210="⳯" ++d211="⳰" ++d212="⳱" ++d213="ⷠ" ++d214="ⷡ" ++d215="ⷢ" ++d216="ⷣ" ++d217="ⷤ" ++d218="ⷥ" ++d219="ⷦ" ++d220="ⷧ" ++d221="ⷨ" ++d222="ⷩ" ++d223="ⷪ" ++d224="ⷫ" ++d225="ⷬ" ++d226="ⷭ" ++d227="ⷮ" ++d228="ⷯ" ++d229="ⷰ" ++d230="ⷱ" ++d231="ⷲ" ++d232="ⷳ" ++d233="ⷴ" ++d234="ⷵ" ++d235="ⷶ" ++d236="ⷷ" ++d237="ⷸ" ++d238="ⷹ" ++d239="ⷺ" ++d240="ⷻ" ++d241="ⷼ" ++d242="ⷽ" ++d243="ⷾ" ++d244="ⷿ" ++d245="꙯" ++d246="꙼" ++d247="꙽" ++d248="꛰" ++d249="꛱" ++d250="꣠" ++d251="꣡" ++d252="꣢" ++d253="꣣" ++d254="꣤" ++d255="꣥" ++d256="꣦" ++d257="꣧" ++d258="꣨" ++d259="꣩" ++d260="꣪" ++d261="꣫" ++d262="꣬" ++d263="꣭" ++d264="꣮" ++d265="꣯" ++d266="꣰" ++d267="꣱" ++d268="ꪰ" ++d269="ꪲ" ++d270="ꪳ" ++d271="ꪷ" ++d272="ꪸ" ++d273="ꪾ" ++d274="꪿" ++d275="꫁" ++d276="︠" ++d277="︡" ++d278="︢" ++d279="︣" ++d280="︤" ++d281="︥" ++d282="︦" ++d283="𐨏" ++d284="𐨸" ++d285="𝆅" ++d286="𝆆" ++d287="𝆇" ++d288="𝆈" ++d289="𝆉" ++d290="𝆪" ++d291="𝆫" ++d292="𝆬" ++d293="𝆭" ++d294="𝉂" ++d295="𝉃" ++d296="𝉄" ++ ++num_diacritics="297" ++ ++placeholder="" ++ ++##################################################################### ++# Upload the image and print the placeholder ++##################################################################### ++ ++upload_image_and_print_placeholder +diff --git a/khash.h b/khash.h +new file mode 100644 +index 0000000..f75f347 +--- /dev/null ++++ b/khash.h +@@ -0,0 +1,627 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk> ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "khash.h" ++KHASH_MAP_INIT_INT(32, char) ++int main() { ++ int ret, is_missing; ++ khiter_t k; ++ khash_t(32) *h = kh_init(32); ++ k = kh_put(32, h, 5, &ret); ++ kh_value(h, k) = 10; ++ k = kh_get(32, h, 10); ++ is_missing = (k == kh_end(h)); ++ k = kh_get(32, h, 5); ++ kh_del(32, h, k); ++ for (k = kh_begin(h); k != kh_end(h); ++k) ++ if (kh_exist(h, k)) kh_value(h, k) = 1; ++ kh_destroy(32, h); ++ return 0; ++} ++*/ ++ ++/* ++ 2013-05-02 (0.2.8): ++ ++ * Use quadratic probing. When the capacity is power of 2, stepping function ++ i*(i+1)/2 guarantees to traverse each bucket. It is better than double ++ hashing on cache performance and is more robust than linear probing. ++ ++ In theory, double hashing should be more robust than quadratic probing. ++ However, my implementation is probably not for large hash tables, because ++ the second hash function is closely tied to the first hash function, ++ which reduce the effectiveness of double hashing. ++ ++ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php ++ ++ 2011-12-29 (0.2.7): ++ ++ * Minor code clean up; no actual effect. ++ ++ 2011-09-16 (0.2.6): ++ ++ * The capacity is a power of 2. This seems to dramatically improve the ++ speed for simple keys. Thank Zilong Tan for the suggestion. Reference: ++ ++ - http://code.google.com/p/ulib/ ++ - http://nothings.org/computer/judy/ ++ ++ * Allow to optionally use linear probing which usually has better ++ performance for random input. Double hashing is still the default as it ++ is more robust to certain non-random input. ++ ++ * Added Wang's integer hash function (not used by default). This hash ++ function is more robust to certain non-random input. ++ ++ 2011-02-14 (0.2.5): ++ ++ * Allow to declare global functions. ++ ++ 2009-09-26 (0.2.4): ++ ++ * Improve portability ++ ++ 2008-09-19 (0.2.3): ++ ++ * Corrected the example ++ * Improved interfaces ++ ++ 2008-09-11 (0.2.2): ++ ++ * Improved speed a little in kh_put() ++ ++ 2008-09-10 (0.2.1): ++ ++ * Added kh_clear() ++ * Fixed a compiling error ++ ++ 2008-09-02 (0.2.0): ++ ++ * Changed to token concatenation which increases flexibility. ++ ++ 2008-08-31 (0.1.2): ++ ++ * Fixed a bug in kh_get(), which has not been tested previously. ++ ++ 2008-08-31 (0.1.1): ++ ++ * Added destructor ++*/ ++ ++ ++#ifndef __AC_KHASH_H ++#define __AC_KHASH_H ++ ++/*! ++ @header ++ ++ Generic hash table library. ++ */ ++ ++#define AC_VERSION_KHASH_H "0.2.8" ++ ++#include <stdlib.h> ++#include <string.h> ++#include <limits.h> ++ ++/* compiler specific configuration */ ++ ++#if UINT_MAX == 0xffffffffu ++typedef unsigned int khint32_t; ++#elif ULONG_MAX == 0xffffffffu ++typedef unsigned long khint32_t; ++#endif ++ ++#if ULONG_MAX == ULLONG_MAX ++typedef unsigned long khint64_t; ++#else ++typedef unsigned long long khint64_t; ++#endif ++ ++#ifndef kh_inline ++#ifdef _MSC_VER ++#define kh_inline __inline ++#else ++#define kh_inline inline ++#endif ++#endif /* kh_inline */ ++ ++#ifndef klib_unused ++#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) ++#define klib_unused __attribute__ ((__unused__)) ++#else ++#define klib_unused ++#endif ++#endif /* klib_unused */ ++ ++typedef khint32_t khint_t; ++typedef khint_t khiter_t; ++ ++#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) ++#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) ++#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) ++#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) ++#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) ++#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) ++#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) ++ ++#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) ++ ++#ifndef kroundup32 ++#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++#endif ++ ++#ifndef kcalloc ++#define kcalloc(N,Z) calloc(N,Z) ++#endif ++#ifndef kmalloc ++#define kmalloc(Z) malloc(Z) ++#endif ++#ifndef krealloc ++#define krealloc(P,Z) realloc(P,Z) ++#endif ++#ifndef kfree ++#define kfree(P) free(P) ++#endif ++ ++static const double __ac_HASH_UPPER = 0.77; ++ ++#define __KHASH_TYPE(name, khkey_t, khval_t) \ ++ typedef struct kh_##name##_s { \ ++ khint_t n_buckets, size, n_occupied, upper_bound; \ ++ khint32_t *flags; \ ++ khkey_t *keys; \ ++ khval_t *vals; \ ++ } kh_##name##_t; ++ ++#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ ++ extern kh_##name##_t *kh_init_##name(void); \ ++ extern void kh_destroy_##name(kh_##name##_t *h); \ ++ extern void kh_clear_##name(kh_##name##_t *h); \ ++ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ ++ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ ++ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ ++ extern void kh_del_##name(kh_##name##_t *h, khint_t x); ++ ++#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ SCOPE kh_##name##_t *kh_init_##name(void) { \ ++ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ ++ } \ ++ SCOPE void kh_destroy_##name(kh_##name##_t *h) \ ++ { \ ++ if (h) { \ ++ kfree((void *)h->keys); kfree(h->flags); \ ++ kfree((void *)h->vals); \ ++ kfree(h); \ ++ } \ ++ } \ ++ SCOPE void kh_clear_##name(kh_##name##_t *h) \ ++ { \ ++ if (h && h->flags) { \ ++ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ ++ h->size = h->n_occupied = 0; \ ++ } \ ++ } \ ++ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ ++ { \ ++ if (h->n_buckets) { \ ++ khint_t k, i, last, mask, step = 0; \ ++ mask = h->n_buckets - 1; \ ++ k = __hash_func(key); i = k & mask; \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) return h->n_buckets; \ ++ } \ ++ return __ac_iseither(h->flags, i)? h->n_buckets : i; \ ++ } else return 0; \ ++ } \ ++ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ ++ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ ++ khint32_t *new_flags = 0; \ ++ khint_t j = 1; \ ++ { \ ++ kroundup32(new_n_buckets); \ ++ if (new_n_buckets < 4) new_n_buckets = 4; \ ++ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ ++ else { /* hash table size to be changed (shrink or expand); rehash */ \ ++ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (!new_flags) return -1; \ ++ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (h->n_buckets < new_n_buckets) { /* expand */ \ ++ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (!new_keys) { kfree(new_flags); return -1; } \ ++ h->keys = new_keys; \ ++ if (kh_is_map) { \ ++ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ if (!new_vals) { kfree(new_flags); return -1; } \ ++ h->vals = new_vals; \ ++ } \ ++ } /* otherwise shrink */ \ ++ } \ ++ } \ ++ if (j) { /* rehashing is needed */ \ ++ for (j = 0; j != h->n_buckets; ++j) { \ ++ if (__ac_iseither(h->flags, j) == 0) { \ ++ khkey_t key = h->keys[j]; \ ++ khval_t val; \ ++ khint_t new_mask; \ ++ new_mask = new_n_buckets - 1; \ ++ if (kh_is_map) val = h->vals[j]; \ ++ __ac_set_isdel_true(h->flags, j); \ ++ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ ++ khint_t k, i, step = 0; \ ++ k = __hash_func(key); \ ++ i = k & new_mask; \ ++ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ ++ __ac_set_isempty_false(new_flags, i); \ ++ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ ++ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ ++ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ ++ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ ++ } else { /* write the element and jump out of the loop */ \ ++ h->keys[i] = key; \ ++ if (kh_is_map) h->vals[i] = val; \ ++ break; \ ++ } \ ++ } \ ++ } \ ++ } \ ++ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ ++ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ } \ ++ kfree(h->flags); /* free the working space */ \ ++ h->flags = new_flags; \ ++ h->n_buckets = new_n_buckets; \ ++ h->n_occupied = h->size; \ ++ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ ++ } \ ++ return 0; \ ++ } \ ++ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ ++ { \ ++ khint_t x; \ ++ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ ++ if (h->n_buckets > (h->size<<1)) { \ ++ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ ++ { \ ++ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ ++ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ ++ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ ++ else { \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ if (__ac_isdel(h->flags, i)) site = i; \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) { x = site; break; } \ ++ } \ ++ if (x == h->n_buckets) { \ ++ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ ++ else x = i; \ ++ } \ ++ } \ ++ } \ ++ if (__ac_isempty(h->flags, x)) { /* not present at all */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; ++h->n_occupied; \ ++ *ret = 1; \ ++ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; \ ++ *ret = 2; \ ++ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ ++ return x; \ ++ } \ ++ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ ++ { \ ++ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ ++ __ac_set_isdel_true(h->flags, x); \ ++ --h->size; \ ++ } \ ++ } ++ ++#define KHASH_DECLARE(name, khkey_t, khval_t) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_PROTOTYPES(name, khkey_t, khval_t) ++ ++#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++/* --- BEGIN OF HASH FUNCTIONS --- */ ++ ++/*! @function ++ @abstract Integer hash function ++ @param key The integer [khint32_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int_hash_func(key) (khint32_t)(key) ++/*! @function ++ @abstract Integer comparison function ++ */ ++#define kh_int_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract 64-bit integer hash function ++ @param key The integer [khint64_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) ++/*! @function ++ @abstract 64-bit integer comparison function ++ */ ++#define kh_int64_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract const char* hash function ++ @param s Pointer to a null terminated string ++ @return The hash value ++ */ ++static kh_inline khint_t __ac_X31_hash_string(const char *s) ++{ ++ khint_t h = (khint_t)*s; ++ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; ++ return h; ++} ++/*! @function ++ @abstract Another interface to const char* hash function ++ @param key Pointer to a null terminated string [const char*] ++ @return The hash value [khint_t] ++ */ ++#define kh_str_hash_func(key) __ac_X31_hash_string(key) ++/*! @function ++ @abstract Const char* comparison function ++ */ ++#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) ++ ++static kh_inline khint_t __ac_Wang_hash(khint_t key) ++{ ++ key += ~(key << 15); ++ key ^= (key >> 10); ++ key += (key << 3); ++ key ^= (key >> 6); ++ key += ~(key << 11); ++ key ^= (key >> 16); ++ return key; ++} ++#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) ++ ++/* --- END OF HASH FUNCTIONS --- */ ++ ++/* Other convenient macros... */ ++ ++/*! ++ @abstract Type of the hash table. ++ @param name Name of the hash table [symbol] ++ */ ++#define khash_t(name) kh_##name##_t ++ ++/*! @function ++ @abstract Initiate a hash table. ++ @param name Name of the hash table [symbol] ++ @return Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_init(name) kh_init_##name() ++ ++/*! @function ++ @abstract Destroy a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_destroy(name, h) kh_destroy_##name(h) ++ ++/*! @function ++ @abstract Reset a hash table without deallocating memory. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_clear(name, h) kh_clear_##name(h) ++ ++/*! @function ++ @abstract Resize a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param s New size [khint_t] ++ */ ++#define kh_resize(name, h, s) kh_resize_##name(h, s) ++ ++/*! @function ++ @abstract Insert a key to the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @param r Extra return code: -1 if the operation failed; ++ 0 if the key is present in the hash table; ++ 1 if the bucket is empty (never used); 2 if the element in ++ the bucket has been deleted [int*] ++ @return Iterator to the inserted element [khint_t] ++ */ ++#define kh_put(name, h, k, r) kh_put_##name(h, k, r) ++ ++/*! @function ++ @abstract Retrieve a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] ++ */ ++#define kh_get(name, h, k) kh_get_##name(h, k) ++ ++/*! @function ++ @abstract Remove a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Iterator to the element to be deleted [khint_t] ++ */ ++#define kh_del(name, h, k) kh_del_##name(h, k) ++ ++/*! @function ++ @abstract Test whether a bucket contains data. ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return 1 if containing data; 0 otherwise [int] ++ */ ++#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) ++ ++/*! @function ++ @abstract Get key given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Key [type of keys] ++ */ ++#define kh_key(h, x) ((h)->keys[x]) ++ ++/*! @function ++ @abstract Get value given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Value [type of values] ++ @discussion For hash sets, calling this results in segfault. ++ */ ++#define kh_val(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Alias of kh_val() ++ */ ++#define kh_value(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Get the start iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The start iterator [khint_t] ++ */ ++#define kh_begin(h) (khint_t)(0) ++ ++/*! @function ++ @abstract Get the end iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The end iterator [khint_t] ++ */ ++#define kh_end(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Get the number of elements in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of elements in the hash table [khint_t] ++ */ ++#define kh_size(h) ((h)->size) ++ ++/*! @function ++ @abstract Get the number of buckets in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of buckets in the hash table [khint_t] ++ */ ++#define kh_n_buckets(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Iterate over the entries in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param kvar Variable to which key will be assigned ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (kvar) = kh_key(h,__i); \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/*! @function ++ @abstract Iterate over the values in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach_value(h, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/* More convenient interfaces */ ++ ++/*! @function ++ @abstract Instantiate a hash set containing integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT(name) \ ++ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT(name, khval_t) \ ++ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash set containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT64(name) \ ++ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT64(name, khval_t) \ ++ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) ++ ++typedef const char *kh_cstr_t; ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_STR(name) \ ++ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_STR(name, khval_t) \ ++ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) ++ ++#endif /* __AC_KHASH_H */ +diff --git a/kvec.h b/kvec.h +new file mode 100644 +index 0000000..10f1c5b +--- /dev/null ++++ b/kvec.h +@@ -0,0 +1,90 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk> ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "kvec.h" ++int main() { ++ kvec_t(int) array; ++ kv_init(array); ++ kv_push(int, array, 10); // append ++ kv_a(int, array, 20) = 5; // dynamic ++ kv_A(array, 20) = 4; // static ++ kv_destroy(array); ++ return 0; ++} ++*/ ++ ++/* ++ 2008-09-22 (0.1.0): ++ ++ * The initial version. ++ ++*/ ++ ++#ifndef AC_KVEC_H ++#define AC_KVEC_H ++ ++#include <stdlib.h> ++ ++#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++ ++#define kvec_t(type) struct { size_t n, m; type *a; } ++#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) ++#define kv_destroy(v) free((v).a) ++#define kv_A(v, i) ((v).a[(i)]) ++#define kv_pop(v) ((v).a[--(v).n]) ++#define kv_size(v) ((v).n) ++#define kv_max(v) ((v).m) ++ ++#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) ++ ++#define kv_copy(type, v1, v0) do { \ ++ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ ++ (v1).n = (v0).n; \ ++ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ ++ } while (0) \ ++ ++#define kv_push(type, v, x) do { \ ++ if ((v).n == (v).m) { \ ++ (v).m = (v).m? (v).m<<1 : 2; \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ ++ } \ ++ (v).a[(v).n++] = (x); \ ++ } while (0) ++ ++#define kv_pushp(type, v) ((((v).n == (v).m)? \ ++ ((v).m = ((v).m? (v).m<<1 : 2), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : 0), ((v).a + ((v).n++))) ++ ++#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ ++ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ ++ : 0), (v).a[(i)]) ++ ++#endif +diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c +new file mode 100644 +index 0000000..829c0fc +--- /dev/null ++++ b/rowcolumn_diacritics_helpers.c +@@ -0,0 +1,391 @@ ++#include <stdint.h> ++ ++uint16_t diacritic_to_num(uint32_t code) ++{ ++ switch (code) { ++ case 0x305: ++ return code - 0x305 + 1; ++ case 0x30d: ++ case 0x30e: ++ return code - 0x30d + 2; ++ case 0x310: ++ return code - 0x310 + 4; ++ case 0x312: ++ return code - 0x312 + 5; ++ case 0x33d: ++ case 0x33e: ++ case 0x33f: ++ return code - 0x33d + 6; ++ case 0x346: ++ return code - 0x346 + 9; ++ case 0x34a: ++ case 0x34b: ++ case 0x34c: ++ return code - 0x34a + 10; ++ case 0x350: ++ case 0x351: ++ case 0x352: ++ return code - 0x350 + 13; ++ case 0x357: ++ return code - 0x357 + 16; ++ case 0x35b: ++ return code - 0x35b + 17; ++ case 0x363: ++ case 0x364: ++ case 0x365: ++ case 0x366: ++ case 0x367: ++ case 0x368: ++ case 0x369: ++ case 0x36a: ++ case 0x36b: ++ case 0x36c: ++ case 0x36d: ++ case 0x36e: ++ case 0x36f: ++ return code - 0x363 + 18; ++ case 0x483: ++ case 0x484: ++ case 0x485: ++ case 0x486: ++ case 0x487: ++ return code - 0x483 + 31; ++ case 0x592: ++ case 0x593: ++ case 0x594: ++ case 0x595: ++ return code - 0x592 + 36; ++ case 0x597: ++ case 0x598: ++ case 0x599: ++ return code - 0x597 + 40; ++ case 0x59c: ++ case 0x59d: ++ case 0x59e: ++ case 0x59f: ++ case 0x5a0: ++ case 0x5a1: ++ return code - 0x59c + 43; ++ case 0x5a8: ++ case 0x5a9: ++ return code - 0x5a8 + 49; ++ case 0x5ab: ++ case 0x5ac: ++ return code - 0x5ab + 51; ++ case 0x5af: ++ return code - 0x5af + 53; ++ case 0x5c4: ++ return code - 0x5c4 + 54; ++ case 0x610: ++ case 0x611: ++ case 0x612: ++ case 0x613: ++ case 0x614: ++ case 0x615: ++ case 0x616: ++ case 0x617: ++ return code - 0x610 + 55; ++ case 0x657: ++ case 0x658: ++ case 0x659: ++ case 0x65a: ++ case 0x65b: ++ return code - 0x657 + 63; ++ case 0x65d: ++ case 0x65e: ++ return code - 0x65d + 68; ++ case 0x6d6: ++ case 0x6d7: ++ case 0x6d8: ++ case 0x6d9: ++ case 0x6da: ++ case 0x6db: ++ case 0x6dc: ++ return code - 0x6d6 + 70; ++ case 0x6df: ++ case 0x6e0: ++ case 0x6e1: ++ case 0x6e2: ++ return code - 0x6df + 77; ++ case 0x6e4: ++ return code - 0x6e4 + 81; ++ case 0x6e7: ++ case 0x6e8: ++ return code - 0x6e7 + 82; ++ case 0x6eb: ++ case 0x6ec: ++ return code - 0x6eb + 84; ++ case 0x730: ++ return code - 0x730 + 86; ++ case 0x732: ++ case 0x733: ++ return code - 0x732 + 87; ++ case 0x735: ++ case 0x736: ++ return code - 0x735 + 89; ++ case 0x73a: ++ return code - 0x73a + 91; ++ case 0x73d: ++ return code - 0x73d + 92; ++ case 0x73f: ++ case 0x740: ++ case 0x741: ++ return code - 0x73f + 93; ++ case 0x743: ++ return code - 0x743 + 96; ++ case 0x745: ++ return code - 0x745 + 97; ++ case 0x747: ++ return code - 0x747 + 98; ++ case 0x749: ++ case 0x74a: ++ return code - 0x749 + 99; ++ case 0x7eb: ++ case 0x7ec: ++ case 0x7ed: ++ case 0x7ee: ++ case 0x7ef: ++ case 0x7f0: ++ case 0x7f1: ++ return code - 0x7eb + 101; ++ case 0x7f3: ++ return code - 0x7f3 + 108; ++ case 0x816: ++ case 0x817: ++ case 0x818: ++ case 0x819: ++ return code - 0x816 + 109; ++ case 0x81b: ++ case 0x81c: ++ case 0x81d: ++ case 0x81e: ++ case 0x81f: ++ case 0x820: ++ case 0x821: ++ case 0x822: ++ case 0x823: ++ return code - 0x81b + 113; ++ case 0x825: ++ case 0x826: ++ case 0x827: ++ return code - 0x825 + 122; ++ case 0x829: ++ case 0x82a: ++ case 0x82b: ++ case 0x82c: ++ case 0x82d: ++ return code - 0x829 + 125; ++ case 0x951: ++ return code - 0x951 + 130; ++ case 0x953: ++ case 0x954: ++ return code - 0x953 + 131; ++ case 0xf82: ++ case 0xf83: ++ return code - 0xf82 + 133; ++ case 0xf86: ++ case 0xf87: ++ return code - 0xf86 + 135; ++ case 0x135d: ++ case 0x135e: ++ case 0x135f: ++ return code - 0x135d + 137; ++ case 0x17dd: ++ return code - 0x17dd + 140; ++ case 0x193a: ++ return code - 0x193a + 141; ++ case 0x1a17: ++ return code - 0x1a17 + 142; ++ case 0x1a75: ++ case 0x1a76: ++ case 0x1a77: ++ case 0x1a78: ++ case 0x1a79: ++ case 0x1a7a: ++ case 0x1a7b: ++ case 0x1a7c: ++ return code - 0x1a75 + 143; ++ case 0x1b6b: ++ return code - 0x1b6b + 151; ++ case 0x1b6d: ++ case 0x1b6e: ++ case 0x1b6f: ++ case 0x1b70: ++ case 0x1b71: ++ case 0x1b72: ++ case 0x1b73: ++ return code - 0x1b6d + 152; ++ case 0x1cd0: ++ case 0x1cd1: ++ case 0x1cd2: ++ return code - 0x1cd0 + 159; ++ case 0x1cda: ++ case 0x1cdb: ++ return code - 0x1cda + 162; ++ case 0x1ce0: ++ return code - 0x1ce0 + 164; ++ case 0x1dc0: ++ case 0x1dc1: ++ return code - 0x1dc0 + 165; ++ case 0x1dc3: ++ case 0x1dc4: ++ case 0x1dc5: ++ case 0x1dc6: ++ case 0x1dc7: ++ case 0x1dc8: ++ case 0x1dc9: ++ return code - 0x1dc3 + 167; ++ case 0x1dcb: ++ case 0x1dcc: ++ return code - 0x1dcb + 174; ++ case 0x1dd1: ++ case 0x1dd2: ++ case 0x1dd3: ++ case 0x1dd4: ++ case 0x1dd5: ++ case 0x1dd6: ++ case 0x1dd7: ++ case 0x1dd8: ++ case 0x1dd9: ++ case 0x1dda: ++ case 0x1ddb: ++ case 0x1ddc: ++ case 0x1ddd: ++ case 0x1dde: ++ case 0x1ddf: ++ case 0x1de0: ++ case 0x1de1: ++ case 0x1de2: ++ case 0x1de3: ++ case 0x1de4: ++ case 0x1de5: ++ case 0x1de6: ++ return code - 0x1dd1 + 176; ++ case 0x1dfe: ++ return code - 0x1dfe + 198; ++ case 0x20d0: ++ case 0x20d1: ++ return code - 0x20d0 + 199; ++ case 0x20d4: ++ case 0x20d5: ++ case 0x20d6: ++ case 0x20d7: ++ return code - 0x20d4 + 201; ++ case 0x20db: ++ case 0x20dc: ++ return code - 0x20db + 205; ++ case 0x20e1: ++ return code - 0x20e1 + 207; ++ case 0x20e7: ++ return code - 0x20e7 + 208; ++ case 0x20e9: ++ return code - 0x20e9 + 209; ++ case 0x20f0: ++ return code - 0x20f0 + 210; ++ case 0x2cef: ++ case 0x2cf0: ++ case 0x2cf1: ++ return code - 0x2cef + 211; ++ case 0x2de0: ++ case 0x2de1: ++ case 0x2de2: ++ case 0x2de3: ++ case 0x2de4: ++ case 0x2de5: ++ case 0x2de6: ++ case 0x2de7: ++ case 0x2de8: ++ case 0x2de9: ++ case 0x2dea: ++ case 0x2deb: ++ case 0x2dec: ++ case 0x2ded: ++ case 0x2dee: ++ case 0x2def: ++ case 0x2df0: ++ case 0x2df1: ++ case 0x2df2: ++ case 0x2df3: ++ case 0x2df4: ++ case 0x2df5: ++ case 0x2df6: ++ case 0x2df7: ++ case 0x2df8: ++ case 0x2df9: ++ case 0x2dfa: ++ case 0x2dfb: ++ case 0x2dfc: ++ case 0x2dfd: ++ case 0x2dfe: ++ case 0x2dff: ++ return code - 0x2de0 + 214; ++ case 0xa66f: ++ return code - 0xa66f + 246; ++ case 0xa67c: ++ case 0xa67d: ++ return code - 0xa67c + 247; ++ case 0xa6f0: ++ case 0xa6f1: ++ return code - 0xa6f0 + 249; ++ case 0xa8e0: ++ case 0xa8e1: ++ case 0xa8e2: ++ case 0xa8e3: ++ case 0xa8e4: ++ case 0xa8e5: ++ case 0xa8e6: ++ case 0xa8e7: ++ case 0xa8e8: ++ case 0xa8e9: ++ case 0xa8ea: ++ case 0xa8eb: ++ case 0xa8ec: ++ case 0xa8ed: ++ case 0xa8ee: ++ case 0xa8ef: ++ case 0xa8f0: ++ case 0xa8f1: ++ return code - 0xa8e0 + 251; ++ case 0xaab0: ++ return code - 0xaab0 + 269; ++ case 0xaab2: ++ case 0xaab3: ++ return code - 0xaab2 + 270; ++ case 0xaab7: ++ case 0xaab8: ++ return code - 0xaab7 + 272; ++ case 0xaabe: ++ case 0xaabf: ++ return code - 0xaabe + 274; ++ case 0xaac1: ++ return code - 0xaac1 + 276; ++ case 0xfe20: ++ case 0xfe21: ++ case 0xfe22: ++ case 0xfe23: ++ case 0xfe24: ++ case 0xfe25: ++ case 0xfe26: ++ return code - 0xfe20 + 277; ++ case 0x10a0f: ++ return code - 0x10a0f + 284; ++ case 0x10a38: ++ return code - 0x10a38 + 285; ++ case 0x1d185: ++ case 0x1d186: ++ case 0x1d187: ++ case 0x1d188: ++ case 0x1d189: ++ return code - 0x1d185 + 286; ++ case 0x1d1aa: ++ case 0x1d1ab: ++ case 0x1d1ac: ++ case 0x1d1ad: ++ return code - 0x1d1aa + 291; ++ case 0x1d242: ++ case 0x1d243: ++ case 0x1d244: ++ return code - 0x1d242 + 295; ++ } ++ return 0; ++} +diff --git a/st.c b/st.c +index 57c6e96..f1c5299 100644 +--- a/st.c ++++ b/st.c +@@ -19,6 +19,7 @@ + + #include "st.h" + #include "win.h" ++#include "graphics.h" + + #if defined(__linux) + #include <pty.h> +@@ -36,6 +37,10 @@ + #define STR_BUF_SIZ ESC_BUF_SIZ + #define STR_ARG_SIZ ESC_ARG_SIZ + ++/* PUA character used as an image placeholder */ ++#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE ++#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE ++ + /* macros */ + #define IS_SET(flag) ((term.mode & (flag)) != 0) + #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +@@ -113,6 +118,8 @@ typedef struct { + typedef struct { + int row; /* nb row */ + int col; /* nb col */ ++ int pixw; /* width of the text area in pixels */ ++ int pixh; /* height of the text area in pixels */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ +@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); + static char utf8encodebyte(Rune, size_t); + static size_t utf8validate(Rune *, size_t); + +-static char *base64dec(const char *); + static char base64dec_getc(const char **); + + static ssize_t xwrite(int, const char *, size_t); +@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; + static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; + static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + ++/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 ++ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ ++uint16_t diacritic_to_num(uint32_t code); ++ + ssize_t + xwrite(int fd, const char *s, size_t len) + { +@@ -616,6 +626,12 @@ getsel(void) + if (gp->mode & ATTR_WDUMMY) + continue; + ++ if (gp->mode & ATTR_IMAGE) { ++ // TODO: Copy diacritics as well ++ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); ++ continue; ++ } ++ + ptr += utf8encode(gp->u, ptr); + } + +@@ -819,7 +835,11 @@ ttyread(void) + { + static char buf[BUFSIZ]; + static int buflen = 0; +- int ret, written; ++ static int already_processing = 0; ++ int ret, written = 0; ++ ++ if (buflen >= LEN(buf)) ++ return 0; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); +@@ -831,7 +851,24 @@ ttyread(void) + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; +- written = twrite(buf, buflen, 0); ++ if (already_processing) { ++ /* Avoid recursive call to twrite() */ ++ return ret; ++ } ++ already_processing = 1; ++ while (1) { ++ int buflen_before_processing = buflen; ++ written += twrite(buf + written, buflen - written, 0); ++ // If buflen changed during the call to twrite, there is ++ // new data, and we need to keep processing, otherwise ++ // we can exit. This will not loop forever because the ++ // buffer is limited, and we don't clean it in this ++ // loop, so at some point ttywrite will have to drop ++ // some data. ++ if (buflen_before_processing == buflen) ++ break; ++ } ++ already_processing = 0; + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) +@@ -874,6 +911,7 @@ ttywriteraw(const char *s, size_t n) + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; ++ int retries_left = 100; + + /* + * Remember that we are using a pty, which might be a modem line. +@@ -882,6 +920,9 @@ ttywriteraw(const char *s, size_t n) + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { ++ if (retries_left-- <= 0) ++ goto too_many_retries; ++ + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); +@@ -923,11 +964,16 @@ ttywriteraw(const char *s, size_t n) + + write_error: + die("write error on tty: %s\n", strerror(errno)); ++too_many_retries: ++ fprintf(stderr, "Could not write %zu bytes to tty\n", n); + } + + void + ttyresize(int tw, int th) + { ++ term.pixw = tw; ++ term.pixh = th; ++ + struct winsize w; + + w.ws_row = term.row; +@@ -1015,7 +1061,8 @@ treset(void) + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, +- .bg = defaultbg ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); +@@ -1038,7 +1085,9 @@ treset(void) + void + tnew(int col, int row) + { +- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; ++ term = (Term){.c = {.attr = {.fg = defaultfg, ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR}}}; + tresize(col, row); + treset(); + } +@@ -1215,9 +1264,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + ++ if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE && ++ tgetisclassicplaceholder(&term.line[y][x])) { ++ // This is a workaround: don't overwrite classic placement ++ // placeholders with space symbols (unlike Unicode placeholders ++ // which must be overwritten by anything). ++ term.line[y][x].bg = attr->bg; ++ term.dirty[y] = 1; ++ return; ++ } ++ + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; ++ ++ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) { ++ term.line[y][x].u = 0; ++ term.line[y][x].mode |= ATTR_IMAGE; ++ } + } + + void +@@ -1244,12 +1308,104 @@ tclearregion(int x1, int y1, int x2, int y2) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; ++ gp->decor = term.c.attr.decor; + gp->mode = 0; + gp->u = ' '; + } + } + } + ++/// Fills a rectangle area with an image placeholder. The starting point is the ++/// cursor. Adds empty lines if needed. The placeholder will be marked as ++/// classic. ++void ++tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id, ++ int cols, int rows, char do_not_move_cursor) ++{ ++ for (int row = 0; row < rows; ++row) { ++ int y = term.c.y; ++ term.dirty[y] = 1; ++ for (int col = 0; col < cols; ++col) { ++ int x = term.c.x + col; ++ if (x >= term.col) ++ break; ++ Glyph *gp = &term.line[y][x]; ++ if (selected(x, y)) ++ selclear(); ++ gp->mode = ATTR_IMAGE; ++ gp->u = 0; ++ tsetimgrow(gp, row + 1); ++ tsetimgcol(gp, col + 1); ++ tsetimgid(gp, image_id); ++ tsetimgplacementid(gp, placement_id); ++ tsetimgdiacriticcount(gp, 3); ++ tsetisclassicplaceholder(gp, 1); ++ } ++ // If moving the cursor is not allowed and this is the last line ++ // of the terminal, we are done. ++ if (do_not_move_cursor && y == term.row - 1) ++ break; ++ // Move the cursor down, maybe creating a new line. The x is ++ // preserved (we never change term.c.x in the loop above). ++ if (row != rows - 1) ++ tnewline(/*first_col=*/0); ++ } ++ if (do_not_move_cursor) { ++ // Return the cursor to the original position. ++ tmoveto(term.c.x, term.c.y - rows + 1); ++ } else { ++ // Move the cursor beyond the last column, as required by the ++ // protocol. If the cursor goes beyond the screen edge, insert a ++ // newline to match the behavior of kitty. ++ if (term.c.x + cols >= term.col) ++ tnewline(/*first_col=*/1); ++ else ++ tmoveto(term.c.x + cols, term.c.y); ++ } ++} ++ ++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id, ++ uint32_t placement_id, int col, ++ int row, char is_classic), ++ void *data) { ++ for (int row = 0; row < term.row; ++row) { ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(gp); ++ uint32_t placement_id = tgetimgplacementid(gp); ++ int ret = ++ callback(data, tgetimgid(gp), ++ tgetimgplacementid(gp), ++ tgetimgcol(gp), tgetimgrow(gp), ++ tgetisclassicplaceholder(gp)); ++ if (ret == 1) { ++ term.dirty[row] = 1; ++ gp->mode = 0; ++ gp->u = ' '; ++ } ++ } ++ } ++ } ++} ++ ++void gr_schedule_image_redraw_by_id(uint32_t image_id) { ++ for (int row = 0; row < term.row; ++row) { ++ if (term.dirty[row]) ++ continue; ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ uint32_t cell_image_id = tgetimgid(gp); ++ if (cell_image_id == image_id) { ++ term.dirty[row] = 1; ++ break; ++ } ++ } ++ } ++ } ++} ++ + void + tdeletechar(int n) + { +@@ -1368,6 +1524,7 @@ tsetattr(const int *attr, int l) + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; ++ term.c.attr.decor = DECOR_DEFAULT_COLOR; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; +@@ -1380,6 +1537,20 @@ tsetattr(const int *attr, int l) + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; ++ if (i + 1 < l) { ++ idx = attr[++i]; ++ if (BETWEEN(idx, 1, 5)) { ++ tsetdecorstyle(&term.c.attr, idx); ++ } else if (idx == 0) { ++ term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); ++ } else { ++ fprintf(stderr, ++ "erresc: unknown underline " ++ "style %d\n", ++ idx); ++ } ++ } + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ +@@ -1403,6 +1574,7 @@ tsetattr(const int *attr, int l) + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; +@@ -1430,6 +1602,13 @@ tsetattr(const int *attr, int l) + case 49: + term.c.attr.bg = defaultbg; + break; ++ case 58: ++ if ((idx = tdefcolor(attr, &i, l)) >= 0) ++ tsetdecorcolor(&term.c.attr, idx); ++ break; ++ case 59: ++ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR); ++ break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; +@@ -1813,6 +1992,39 @@ csihandle(void) + goto unknown; + } + break; ++ case '>': ++ switch (csiescseq.mode[1]) { ++ case 'q': /* XTVERSION -- Print terminal name and version */ ++ len = snprintf(buf, sizeof(buf), ++ "\033P>|st-graphics(%s)\033\\", VERSION); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; ++ case 't': /* XTWINOPS -- Window manipulation */ ++ switch (csiescseq.arg[0]) { ++ case 14: /* Report text area size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[4;%i;%it", ++ term.pixh, term.pixw); ++ ttywrite(buf, len, 0); ++ break; ++ case 16: /* Report character cell size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[6;%i;%it", ++ term.pixh / term.row, ++ term.pixw / term.col); ++ ttywrite(buf, len, 0); ++ break; ++ case 18: /* Report the size of the text area in characters. */ ++ len = snprintf(buf, sizeof(buf), "\033[8;%i;%it", ++ term.row, term.col); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; + } + } + +@@ -1962,8 +2174,26 @@ strhandle(void) + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; +- case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ ++ if (gr_parse_command(strescseq.buf, strescseq.len)) { ++ GraphicsCommandResult *res = &graphics_command_result; ++ if (res->create_placeholder) { ++ tcreateimgplaceholder( ++ res->placeholder.image_id, ++ res->placeholder.placement_id, ++ res->placeholder.columns, ++ res->placeholder.rows, ++ res->placeholder.do_not_move_cursor); ++ } ++ if (res->response[0]) ++ ttywrite(res->response, strlen(res->response), ++ 0); ++ if (res->redraw) ++ tfulldirt(); ++ return; ++ } ++ return; ++ case 'P': /* DCS -- Device Control String */ + case '^': /* PM -- Privacy Message */ + return; + } +@@ -2469,6 +2699,33 @@ check_control_code: + if (selected(term.c.x, term.c.y)) + selclear(); + ++ if (width == 0) { ++ // It's probably a combining char. Combining characters are not ++ // supported, so we just ignore them, unless it denotes the row and ++ // column of an image character. ++ if (term.c.y <= 0 && term.c.x <= 0) ++ return; ++ else if (term.c.x == 0) ++ gp = &term.line[term.c.y-1][term.col-1]; ++ else if (term.c.state & CURSOR_WRAPNEXT) ++ gp = &term.line[term.c.y][term.c.x]; ++ else ++ gp = &term.line[term.c.y][term.c.x-1]; ++ uint16_t num = diacritic_to_num(u); ++ if (num && (gp->mode & ATTR_IMAGE)) { ++ unsigned diaccount = tgetimgdiacriticcount(gp); ++ if (diaccount == 0) ++ tsetimgrow(gp, num); ++ else if (diaccount == 1) ++ tsetimgcol(gp, num); ++ else if (diaccount == 2) ++ tsetimg4thbyteplus1(gp, num); ++ tsetimgdiacriticcount(gp, diaccount + 1); ++ } ++ term.lastc = u; ++ return; ++ } ++ + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; +@@ -2635,6 +2892,8 @@ drawregion(int x1, int y1, int x2, int y2) + { + int y; + ++ xstartimagedraw(term.dirty, term.row); ++ + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; +@@ -2642,6 +2901,8 @@ drawregion(int x1, int y1, int x2, int y2) + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } ++ ++ xfinishimagedraw(); + } + + void +@@ -2676,3 +2937,9 @@ redraw(void) + tfulldirt(); + draw(); + } ++ ++Glyph ++getglyphat(int col, int row) ++{ ++ return term.line[row][col]; ++} +diff --git a/st.h b/st.h +index fd3b0d8..c5dd731 100644 +--- a/st.h ++++ b/st.h +@@ -12,7 +12,7 @@ + #define DEFAULT(a, b) (a) = (a) ? (a) : (b) + #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) + #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ +- (a).bg != (b).bg) ++ (a).bg != (b).bg || (a).decor != (b).decor) + #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) + #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) +@@ -20,6 +20,10 @@ + #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) + #define IS_TRUECOL(x) (1 << 24 & (x)) + ++// This decor color indicates that the fg color should be used. Note that it's ++// not a 24-bit color because the 25-th bit is not set. ++#define DECOR_DEFAULT_COLOR 0x0ffffff ++ + enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, +@@ -34,6 +38,7 @@ enum glyph_attribute { + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, ++ ATTR_IMAGE = 1 << 14, + }; + + enum selection_mode { +@@ -52,6 +57,14 @@ enum selection_snap { + SNAP_LINE = 2 + }; + ++enum underline_style { ++ UNDERLINE_STRAIGHT = 1, ++ UNDERLINE_DOUBLE = 2, ++ UNDERLINE_CURLY = 3, ++ UNDERLINE_DOTTED = 4, ++ UNDERLINE_DASHED = 5, ++}; ++ + typedef unsigned char uchar; + typedef unsigned int uint; + typedef unsigned long ulong; +@@ -65,6 +78,7 @@ typedef struct { + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ ++ uint32_t decor; /* decoration (like underline) */ + } Glyph; + + typedef Glyph *Line; +@@ -105,6 +119,8 @@ void selextend(int, int, int, int); + int selected(int, int); + char *getsel(void); + ++Glyph getglyphat(int, int); ++ + size_t utf8encode(Rune, char *); + + void *xmalloc(size_t); +@@ -124,3 +140,69 @@ extern unsigned int tabspaces; + extern unsigned int defaultfg; + extern unsigned int defaultbg; + extern unsigned int defaultcs; ++ ++// Accessors to decoration properties stored in `decor`. ++// The 25-th bit is used to indicate if it's a 24-bit color. ++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; } ++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; } ++static inline void tsetdecorcolor(Glyph *g, uint32_t color) { ++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff); ++} ++static inline void tsetdecorstyle(Glyph *g, uint32_t style) { ++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25); ++} ++ ++ ++// Some accessors to image placeholder properties stored in `u`: ++// - row (1-base) - 9 bits ++// - column (1-base) - 9 bits ++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified, ++// don't forget to subtract 1). ++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits ++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit ++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; } ++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; } ++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; } ++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; } ++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; } ++static inline void tsetimgrow(Glyph *g, uint32_t row) { ++ g->u = (g->u & ~0x1ff) | (row & 0x1ff); ++} ++static inline void tsetimgcol(Glyph *g, uint32_t col) { ++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9); ++} ++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) { ++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18); ++} ++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) { ++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27); ++} ++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) { ++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29); ++} ++ ++/// Returns the full image id. This is a naive implementation, if the most ++/// significant byte is not specified, it's assumed to be 0 instead of inferring ++/// it from the cells to the left. ++static inline uint32_t tgetimgid(Glyph *g) { ++ uint32_t msb = tgetimgid4thbyteplus1(g); ++ if (msb != 0) ++ --msb; ++ return (msb << 24) | (g->fg & 0xFFFFFF); ++} ++ ++/// Sets the full image id. ++static inline void tsetimgid(Glyph *g, uint32_t id) { ++ g->fg = (id & 0xFFFFFF) | (1 << 24); ++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1); ++} ++ ++static inline uint32_t tgetimgplacementid(Glyph *g) { ++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR) ++ return 0; ++ return g->decor & 0xFFFFFF; ++} ++ ++static inline void tsetimgplacementid(Glyph *g, uint32_t id) { ++ g->decor = (id & 0xFFFFFF) | (1 << 24); ++} +diff --git a/st.info b/st.info +index efab2cf..ded76c1 100644 +--- a/st.info ++++ b/st.info +@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, ++ Smulx=\E[4:%p1%dm, + + st| simpleterm, + use=st-mono, +@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, ++# Underline colors ++ Su, ++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m, ++ Setulc1=\E[58:5:%p1%dm, ++ ol=\E[59m, + + st-meta| simpleterm with meta key, + use=st, +diff --git a/win.h b/win.h +index 6de960d..31b3fff 100644 +--- a/win.h ++++ b/win.h +@@ -39,3 +39,6 @@ void xsetpointermotion(int); + void xsetsel(char *); + int xstartdraw(void); + void xximspot(int, int); ++ ++void xstartimagedraw(int *dirty, int rows); ++void xfinishimagedraw(); +diff --git a/x.c b/x.c +index d73152b..6f1bf8c 100644 +--- a/x.c ++++ b/x.c +@@ -4,6 +4,8 @@ + #include <limits.h> + #include <locale.h> + #include <signal.h> ++#include <stdio.h> ++#include <stdlib.h> + #include <sys/select.h> + #include <time.h> + #include <unistd.h> +@@ -19,6 +21,7 @@ char *argv0; + #include "arg.h" + #include "st.h" + #include "win.h" ++#include "graphics.h" + + /* types used in config.h */ + typedef struct { +@@ -59,6 +62,12 @@ static void zoom(const Arg *); + static void zoomabs(const Arg *); + static void zoomreset(const Arg *); + static void ttysend(const Arg *); ++static void previewimage(const Arg *); ++static void showimageinfo(const Arg *); ++static void togglegrdebug(const Arg *); ++static void dumpgrstate(const Arg *); ++static void unloadimages(const Arg *); ++static void toggleimages(const Arg *); + + /* config.h for applying patches and the configuration. */ + #include "config.h" +@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec; + typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ ++ int hborderpx, vborderpx; + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ +@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int); + static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); + static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); + static void xdrawglyph(Glyph, int, int); ++static void xdrawimages(Glyph, Line, int x1, int y1, int x2); ++static void xdrawoneimagecell(Glyph, int x, int y); + static void xclear(int, int, int, int); + static int xgeommasktogravity(int); + static int ximopen(Display *); +@@ -220,6 +232,7 @@ static DC dc; + static XWindow xw; + static XSelection xsel; + static TermWindow win; ++static unsigned int mouse_col = 0, mouse_row = 0; + + /* Font Ring Cache */ + enum { +@@ -328,10 +341,72 @@ ttysend(const Arg *arg) + ttywrite(arg->s, strlen(arg->s), 1); + } + ++void ++previewimage(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ gr_preview_image(image_id, arg->s); ++ } ++} ++ ++void ++showimageinfo(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ char stcommand[256] = {0}; ++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0); ++ if (len > sizeof(stcommand) - 1) { ++ fprintf(stderr, "Executable name too long: %s\n", ++ argv0); ++ return; ++ } ++ gr_show_image_info(image_id, tgetimgplacementid(&g), ++ tgetimgcol(&g), tgetimgrow(&g), ++ tgetisclassicplaceholder(&g), ++ tgetimgdiacriticcount(&g), argv0); ++ } ++} ++ ++void ++togglegrdebug(const Arg *arg) ++{ ++ graphics_debug_mode = (graphics_debug_mode + 1) % 3; ++ redraw(); ++} ++ ++void ++dumpgrstate(const Arg *arg) ++{ ++ gr_dump_state(); ++} ++ ++void ++unloadimages(const Arg *arg) ++{ ++ gr_unload_images_to_reduce_ram(); ++} ++ ++void ++toggleimages(const Arg *arg) ++{ ++ graphics_display_images = !graphics_display_images; ++ redraw(); ++} ++ + int + evcol(XEvent *e) + { +- int x = e->xbutton.x - borderpx; ++ int x = e->xbutton.x - win.hborderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; + } +@@ -339,7 +414,7 @@ evcol(XEvent *e) + int + evrow(XEvent *e) + { +- int y = e->xbutton.y - borderpx; ++ int y = e->xbutton.y - win.vborderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; + } +@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release) + /* ignore Button<N>mask for Button<N> - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + ++ mouse_col = evcol(e); ++ mouse_row = evrow(e); ++ + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && +@@ -739,6 +817,9 @@ cresize(int width, int height) + col = MAX(1, col); + row = MAX(1, row); + ++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100; ++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100; ++ + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +@@ -869,8 +950,8 @@ xhints(void) + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; +- sizeh->height_inc = win.ch; +- sizeh->width_inc = win.cw; ++ sizeh->height_inc = 1; ++ sizeh->width_inc = 1; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; +@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } +- defaultfontsize = usedfontsize; ++ if (defaultfontsize <= 0) ++ defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) +@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; +- if (fontsize == 0) ++ if (defaultfontsize <= 0 && fontsize == 0) + defaultfontsize = fontval; + } + +@@ -1152,8 +1234,8 @@ xinit(int cols, int rows) + xloadcols(); + + /* adjust fixed window geometry */ +- win.w = 2 * borderpx + cols * win.cw; +- win.h = 2 * borderpx + rows * win.ch; ++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw; ++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) +@@ -1240,12 +1322,15 @@ xinit(int cols, int rows) + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; ++ ++ // Initialize the graphics (image display) module. ++ gr_init(xw.dpy, xw.vis, xw.cmap); + } + + int + xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) + { +- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; ++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; +@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + if (mode == ATTR_WDUMMY) + continue; + ++ /* Draw spaces for image placeholders (images will be drawn ++ * separately). */ ++ if (mode & ATTR_IMAGE) ++ rune = ' '; ++ + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; +@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + return numspecs; + } + ++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen` ++ * is the length of the dash plus the length of the gap. `fraction` is the ++ * fraction of the dash length compared to `wavelen`. */ ++static void ++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w, ++ int wavelen, float fraction, int thick) ++{ ++ int dashw = MAX(1, fraction * wavelen); ++ for (int i = x - x % wavelen; i < x + w; i += wavelen) { ++ int startx = MAX(i, x); ++ int endx = MIN(i + dashw, x + w); ++ if (startx < endx) ++ XftDrawRect(xw.draw, color, startx, y, endx - startx, ++ thick); ++ } ++} ++ ++/* Draws an undercurl. `h` is the total height, including line thickness. */ ++static void ++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) ++{ ++ XGCValues gcvals = {.foreground = color->pixel, ++ .line_width = thick, ++ .line_style = LineSolid, ++ .cap_style = CapRound}; ++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw), ++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle, ++ &gcvals); ++ ++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h}; ++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted); ++ ++ int yoffset = thick / 2; ++ int segh = MAX(1, h - thick); ++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't ++ * look good without antialiasing. */ ++ int segw = segh; ++ int wavelen = MAX(1, segw * 2); ++ ++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) { ++ XPoint points[3] = {{.x = i, .y = y + yoffset}, ++ {.x = i + segw, .y = y + yoffset + segh}, ++ {.x = i + wavelen, .y = y + yoffset}}; ++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3, ++ CoordModeOrigin); ++ } ++ ++ XFreeGC(xw.dpy, gc); ++} ++ + void + xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) + { + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); +- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, ++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; +@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { +- xclear(0, (y == 0)? 0 : winy, borderpx, ++ xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + +- ((winy + win.ch >= borderpx + win.th)? win.h : 0)); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } +- if (winx + width >= borderpx + win.tw) { ++ if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, +- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) +- xclear(winx, 0, winx + width, borderpx); +- if (winy + win.ch >= borderpx + win.th) ++ xclear(winx, 0, winx + width, win.vborderpx); ++ if (winy + win.ch >= win.vborderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ +@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + +- /* Render the glyphs. */ +- XftDrawGlyphFontSpec(xw.draw, fg, specs, len); +- +- /* Render underline and strikethrough. */ ++ /* Decoration color. */ ++ Color decor; ++ uint32_t decorcolor = tgetdecorcolor(&base); ++ if (decorcolor == DECOR_DEFAULT_COLOR) { ++ decor = *fg; ++ } else if (IS_TRUECOL(decorcolor)) { ++ colfg.alpha = 0xffff; ++ colfg.red = TRUERED(decorcolor); ++ colfg.green = TRUEGREEN(decorcolor); ++ colfg.blue = TRUEBLUE(decorcolor); ++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor); ++ } else { ++ decor = dc.col[decorcolor]; ++ } ++ decor.color.alpha = 0xffff; ++ decor.pixel |= 0xff << 24; ++ ++ /* Float thickness, used as a base to compute other values. */ ++ float fthick = dc.font.height / 18.0; ++ /* Integer thickness in pixels. Must not be 0. */ ++ int thick = MAX(1, roundf(fthick)); ++ /* The default gap between the baseline and a single underline. */ ++ int gap = roundf(fthick * 2); ++ /* The total thickness of a double underline. */ ++ int doubleh = thick * 2 + ceilf(fthick * 0.5); ++ /* The total thickness of an undercurl. */ ++ int curlh = thick * 2 + roundf(fthick * 0.75); ++ ++ /* Render the underline before the glyphs. */ + if (base.mode & ATTR_UNDERLINE) { +- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, +- width, 1); ++ uint32_t style = tgetdecorstyle(&base); ++ int liney = winy + dc.font.ascent + gap; ++ /* Adjust liney to guarantee that a single underline fits. */ ++ liney -= MAX(0, liney + thick - (winy + win.ch)); ++ if (style == UNDERLINE_DOUBLE) { ++ liney -= MAX(0, liney + doubleh - (winy + win.ch)); ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ XftDrawRect(xw.draw, &decor, winx, ++ liney + doubleh - thick, width, thick); ++ } else if (style == UNDERLINE_DOTTED) { ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ thick * 2, 0.5, thick); ++ } else if (style == UNDERLINE_DASHED) { ++ int wavelen = MAX(2, win.cw * 0.9); ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ wavelen, 0.65, thick); ++ } else if (style == UNDERLINE_CURLY) { ++ liney -= MAX(0, liney + curlh - (winy + win.ch)); ++ xdrawundercurl(xw.draw, &decor, winx, liney, width, ++ curlh, thick); ++ } else { ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ } + } + ++ /* Render the glyphs. */ ++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len); ++ ++ /* Render strikethrough. Alway use the fg color. */ + if (base.mode & ATTR_STRUCK) { +- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, +- width, 1); ++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, ++ width, thick); + } + + /* Reset clip to none. */ +@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y) + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); ++ if (g.mode & ATTR_IMAGE) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ xdrawoneimagecell(g, x, y); ++ gr_finish_drawing(xw.buf); ++ } + } + + void +@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + if (IS_SET(MODE_HIDE)) + return; + ++ // If it's an image, just draw a ballot box for simplicity. ++ if (g.mode & ATTR_IMAGE) ++ g.u = 0x2610; ++ + /* + * Select the right color for the right mode. + */ +@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - \ ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + (cx + 1) * win.cw - 1, +- borderpx + cy * win.ch, ++ win.hborderpx + (cx + 1) * win.cw - 1, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - 1, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } + } + ++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming ++ * that they have the same attributes (and thus the same lower 24 bits of the ++ * image ID and the same placement ID). */ ++void ++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) { ++ int y_pix = win.vborderpx + y1 * win.ch; ++ uint32_t image_id_24bits = base.fg & 0xFFFFFF; ++ uint32_t placement_id = tgetimgplacementid(&base); ++ // Columns and rows are 1-based, 0 means unspecified. ++ int last_col = 0; ++ int last_row = 0; ++ int last_start_col = 0; ++ int last_start_x = x1; ++ // The most significant byte is also 1-base, subtract 1 before use. ++ uint32_t last_id_4thbyteplus1 = 0; ++ // We may need to inherit row/column/4th byte from the previous cell. ++ Glyph *prev = &line[x1 - 1]; ++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) && ++ (prev->fg & 0xFFFFFF) == image_id_24bits && ++ prev->decor == base.decor) { ++ last_row = tgetimgrow(prev); ++ last_col = tgetimgcol(prev); ++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev); ++ last_start_col = last_col + 1; ++ } ++ for (int x = x1; x < x2; ++x) { ++ Glyph *g = &line[x]; ++ uint32_t cur_row = tgetimgrow(g); ++ uint32_t cur_col = tgetimgcol(g); ++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g); ++ uint32_t num_diacritics = tgetimgdiacriticcount(g); ++ // If the row is not specified, assume it's the same as the row ++ // of the previous cell. Note that `cur_row` may contain a ++ // value imputed earlier, which will be preserved if `last_row` ++ // is zero (i.e. we don't know the row of the previous cell). ++ if (last_row && (num_diacritics == 0 || !cur_row)) ++ cur_row = last_row; ++ // If the column is not specified and the row is the same as the ++ // row of the previous cell, then assume that the column is the ++ // next one. ++ if (last_col && (num_diacritics <= 1 || !cur_col) && ++ cur_row == last_row) ++ cur_col = last_col + 1; ++ // If the additional id byte is not specified and the ++ // coordinates are consecutive, assume the byte is also the ++ // same. ++ if (last_id_4thbyteplus1 && ++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) && ++ cur_row == last_row && cur_col == last_col + 1) ++ cur_id_4thbyteplus1 = last_id_4thbyteplus1; ++ // If we couldn't infer row and column, start from the top left ++ // corner. ++ if (cur_row == 0) ++ cur_row = 1; ++ if (cur_col == 0) ++ cur_col = 1; ++ // If this cell breaks a contiguous stripe of image cells, draw ++ // that line and start a new one. ++ if (cur_col != last_col + 1 || cur_row != last_row || ++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) { ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ if (last_row != 0) { ++ int x_pix = ++ win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect( ++ xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, ++ last_row - 1, last_row, last_start_x, ++ y1, x_pix, y_pix, win.cw, win.ch, ++ base.mode & ATTR_REVERSE); ++ } ++ last_start_col = cur_col; ++ last_start_x = x; ++ } ++ last_row = cur_row; ++ last_col = cur_col; ++ last_id_4thbyteplus1 = cur_id_4thbyteplus1; ++ // Populate the missing glyph data to enable inheritance between ++ // runs and support the naive implementation of tgetimgid. ++ if (!tgetimgrow(g)) ++ tsetimgrow(g, cur_row); ++ // We cannot save this information if there are > 511 cols. ++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0) ++ tsetimgcol(g, cur_col); ++ if (!tgetimgid4thbyteplus1(g)) ++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1); ++ } ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ // Draw the last contiguous stripe. ++ if (last_row != 0) { ++ int x_pix = win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect(xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, last_row - 1, ++ last_row, last_start_x, y1, x_pix, y_pix, ++ win.cw, win.ch, base.mode & ATTR_REVERSE); ++ } ++} ++ ++/* Draw just one image cell without inheriting attributes from the left. */ ++void xdrawoneimagecell(Glyph g, int x, int y) { ++ if (!(g.mode & ATTR_IMAGE)) ++ return; ++ int x_pix = win.hborderpx + x * win.cw; ++ int y_pix = win.vborderpx + y * win.ch; ++ uint32_t row = tgetimgrow(&g) - 1; ++ uint32_t col = tgetimgcol(&g) - 1; ++ uint32_t placement_id = tgetimgplacementid(&g); ++ uint32_t image_id = tgetimgid(&g); ++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row, ++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch, ++ g.mode & ATTR_REVERSE); ++} ++ ++/* Prepare for image drawing. */ ++void xstartimagedraw(int *dirty, int rows) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ gr_mark_dirty_animations(dirty, rows); ++} ++ ++/* Draw all queued image cells. */ ++void xfinishimagedraw() { ++ gr_finish_drawing(xw.buf); ++} ++ + void + xsetenv(void) + { +@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + specs += i; + numspecs -= i; + i = 0; +@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2) + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (i > 0 && base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + } + + void +@@ -1907,6 +2238,7 @@ cmessage(XEvent *e) + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); ++ gr_deinit(); + exit(0); + } + } +@@ -1957,6 +2289,13 @@ run(void) + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + ++ /* Decrease the timeout if there are active animations. */ ++ if (graphics_next_redraw_delay != INT_MAX && ++ IS_SET(MODE_VISIBLE)) ++ timeout = timeout < 0 ? graphics_next_redraw_delay ++ : MIN(timeout, ++ graphics_next_redraw_delay); ++ + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; +-- +2.43.0 + diff --git a/rowcolumn-diacritics.txt b/rowcolumn-diacritics.txt new file mode 100644 index 0000000..9d8091e --- /dev/null +++ b/rowcolumn-diacritics.txt @@ -0,0 +1,310 @@ +# This file lists the diacritics used to indicate row/column numbers for +# Unicode terminal image placeholders. It is derived from UnicodeData.txt for +# Unicode 6.0.0 (chosen somewhat arbitrarily: it's old enough, but still +# contains more than 255 suitable combining chars) using the following +# command: +# +# cat UnicodeData.txt | grep "Mn;230;NSM;;" | grep -v "0300\|0301\|0302\|0303\|0304\|0306\|0307\|0308\|0309\|030A\|030B\|030C\|030F\|0311\|0313\|0314\|0342\|0653\|0654" +# +# That is, we use combining chars of the same combining class 230 (above the +# base character) that do not have decomposition mappings, and we also remove +# some characters that may be fused with other characters during normalization, +# like 0041 0300 -> 00C0 which is À (A with grave). +# +0305;COMBINING OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING OVERSCORE;;;; +030D;COMBINING VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL LINE ABOVE;;;; +030E;COMBINING DOUBLE VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE VERTICAL LINE ABOVE;;;; +0310;COMBINING CANDRABINDU;Mn;230;NSM;;;;;N;NON-SPACING CANDRABINDU;;;; +0312;COMBINING TURNED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING TURNED COMMA ABOVE;;;; +033D;COMBINING X ABOVE;Mn;230;NSM;;;;;N;NON-SPACING X ABOVE;;;; +033E;COMBINING VERTICAL TILDE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL TILDE;;;; +033F;COMBINING DOUBLE OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE OVERSCORE;;;; +0346;COMBINING BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +034A;COMBINING NOT TILDE ABOVE;Mn;230;NSM;;;;;N;;;;; +034B;COMBINING HOMOTHETIC ABOVE;Mn;230;NSM;;;;;N;;;;; +034C;COMBINING ALMOST EQUAL TO ABOVE;Mn;230;NSM;;;;;N;;;;; +0350;COMBINING RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +0351;COMBINING LEFT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +0352;COMBINING FERMATA;Mn;230;NSM;;;;;N;;;;; +0357;COMBINING RIGHT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +035B;COMBINING ZIGZAG ABOVE;Mn;230;NSM;;;;;N;;;;; +0363;COMBINING LATIN SMALL LETTER A;Mn;230;NSM;;;;;N;;;;; +0364;COMBINING LATIN SMALL LETTER E;Mn;230;NSM;;;;;N;;;;; +0365;COMBINING LATIN SMALL LETTER I;Mn;230;NSM;;;;;N;;;;; +0366;COMBINING LATIN SMALL LETTER O;Mn;230;NSM;;;;;N;;;;; +0367;COMBINING LATIN SMALL LETTER U;Mn;230;NSM;;;;;N;;;;; +0368;COMBINING LATIN SMALL LETTER C;Mn;230;NSM;;;;;N;;;;; +0369;COMBINING LATIN SMALL LETTER D;Mn;230;NSM;;;;;N;;;;; +036A;COMBINING LATIN SMALL LETTER H;Mn;230;NSM;;;;;N;;;;; +036B;COMBINING LATIN SMALL LETTER M;Mn;230;NSM;;;;;N;;;;; +036C;COMBINING LATIN SMALL LETTER R;Mn;230;NSM;;;;;N;;;;; +036D;COMBINING LATIN SMALL LETTER T;Mn;230;NSM;;;;;N;;;;; +036E;COMBINING LATIN SMALL LETTER V;Mn;230;NSM;;;;;N;;;;; +036F;COMBINING LATIN SMALL LETTER X;Mn;230;NSM;;;;;N;;;;; +0483;COMBINING CYRILLIC TITLO;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING TITLO;;;; +0484;COMBINING CYRILLIC PALATALIZATION;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PALATALIZATION;;;; +0485;COMBINING CYRILLIC DASIA PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING DASIA PNEUMATA;;;; +0486;COMBINING CYRILLIC PSILI PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PSILI PNEUMATA;;;; +0487;COMBINING CYRILLIC POKRYTIE;Mn;230;NSM;;;;;N;;;;; +0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;; +0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;; +0594;HEBREW ACCENT ZAQEF QATAN;Mn;230;NSM;;;;;N;;;;; +0595;HEBREW ACCENT ZAQEF GADOL;Mn;230;NSM;;;;;N;;;;; +0597;HEBREW ACCENT REVIA;Mn;230;NSM;;;;;N;;;;; +0598;HEBREW ACCENT ZARQA;Mn;230;NSM;;;;;N;;;;; +0599;HEBREW ACCENT PASHTA;Mn;230;NSM;;;;;N;;;;; +059C;HEBREW ACCENT GERESH;Mn;230;NSM;;;;;N;;;;; +059D;HEBREW ACCENT GERESH MUQDAM;Mn;230;NSM;;;;;N;;;;; +059E;HEBREW ACCENT GERSHAYIM;Mn;230;NSM;;;;;N;;;;; +059F;HEBREW ACCENT QARNEY PARA;Mn;230;NSM;;;;;N;;;;; +05A0;HEBREW ACCENT TELISHA GEDOLA;Mn;230;NSM;;;;;N;;;;; +05A1;HEBREW ACCENT PAZER;Mn;230;NSM;;;;;N;;;;; +05A8;HEBREW ACCENT QADMA;Mn;230;NSM;;;;;N;;;;; +05A9;HEBREW ACCENT TELISHA QETANA;Mn;230;NSM;;;;;N;;;;; +05AB;HEBREW ACCENT OLE;Mn;230;NSM;;;;;N;;;;; +05AC;HEBREW ACCENT ILUY;Mn;230;NSM;;;;;N;;;;; +05AF;HEBREW MARK MASORA CIRCLE;Mn;230;NSM;;;;;N;;;;; +05C4;HEBREW MARK UPPER DOT;Mn;230;NSM;;;;;N;;;;; +0610;ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM;Mn;230;NSM;;;;;N;;;;; +0611;ARABIC SIGN ALAYHE ASSALLAM;Mn;230;NSM;;;;;N;;;;; +0612;ARABIC SIGN RAHMATULLAH ALAYHE;Mn;230;NSM;;;;;N;;;;; +0613;ARABIC SIGN RADI ALLAHOU ANHU;Mn;230;NSM;;;;;N;;;;; +0614;ARABIC SIGN TAKHALLUS;Mn;230;NSM;;;;;N;;;;; +0615;ARABIC SMALL HIGH TAH;Mn;230;NSM;;;;;N;;;;; +0616;ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH;Mn;230;NSM;;;;;N;;;;; +0617;ARABIC SMALL HIGH ZAIN;Mn;230;NSM;;;;;N;;;;; +0657;ARABIC INVERTED DAMMA;Mn;230;NSM;;;;;N;;;;; +0658;ARABIC MARK NOON GHUNNA;Mn;230;NSM;;;;;N;;;;; +0659;ARABIC ZWARAKAY;Mn;230;NSM;;;;;N;;;;; +065A;ARABIC VOWEL SIGN SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065B;ARABIC VOWEL SIGN INVERTED SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065D;ARABIC REVERSED DAMMA;Mn;230;NSM;;;;;N;;;;; +065E;ARABIC FATHA WITH TWO DOTS;Mn;230;NSM;;;;;N;;;;; +06D6;ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D7;ARABIC SMALL HIGH LIGATURE QAF WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D8;ARABIC SMALL HIGH MEEM INITIAL FORM;Mn;230;NSM;;;;;N;;;;; +06D9;ARABIC SMALL HIGH LAM ALEF;Mn;230;NSM;;;;;N;;;;; +06DA;ARABIC SMALL HIGH JEEM;Mn;230;NSM;;;;;N;;;;; +06DB;ARABIC SMALL HIGH THREE DOTS;Mn;230;NSM;;;;;N;;;;; +06DC;ARABIC SMALL HIGH SEEN;Mn;230;NSM;;;;;N;;;;; +06DF;ARABIC SMALL HIGH ROUNDED ZERO;Mn;230;NSM;;;;;N;;;;; +06E0;ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO;Mn;230;NSM;;;;;N;;;;; +06E1;ARABIC SMALL HIGH DOTLESS HEAD OF KHAH;Mn;230;NSM;;;;;N;;;;; +06E2;ARABIC SMALL HIGH MEEM ISOLATED FORM;Mn;230;NSM;;;;;N;;;;; +06E4;ARABIC SMALL HIGH MADDA;Mn;230;NSM;;;;;N;;;;; +06E7;ARABIC SMALL HIGH YEH;Mn;230;NSM;;;;;N;;;;; +06E8;ARABIC SMALL HIGH NOON;Mn;230;NSM;;;;;N;;;;; +06EB;ARABIC EMPTY CENTRE HIGH STOP;Mn;230;NSM;;;;;N;;;;; +06EC;ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE;Mn;230;NSM;;;;;N;;;;; +0730;SYRIAC PTHAHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0732;SYRIAC PTHAHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0733;SYRIAC ZQAPHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0735;SYRIAC ZQAPHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0736;SYRIAC RBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073A;SYRIAC HBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073D;SYRIAC ESASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073F;SYRIAC RWAHA;Mn;230;NSM;;;;;N;;;;; +0740;SYRIAC FEMININE DOT;Mn;230;NSM;;;;;N;;;;; +0741;SYRIAC QUSHSHAYA;Mn;230;NSM;;;;;N;;;;; +0743;SYRIAC TWO VERTICAL DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0745;SYRIAC THREE DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0747;SYRIAC OBLIQUE LINE ABOVE;Mn;230;NSM;;;;;N;;;;; +0749;SYRIAC MUSIC;Mn;230;NSM;;;;;N;;;;; +074A;SYRIAC BARREKH;Mn;230;NSM;;;;;N;;;;; +07EB;NKO COMBINING SHORT HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07EC;NKO COMBINING SHORT LOW TONE;Mn;230;NSM;;;;;N;;;;; +07ED;NKO COMBINING SHORT RISING TONE;Mn;230;NSM;;;;;N;;;;; +07EE;NKO COMBINING LONG DESCENDING TONE;Mn;230;NSM;;;;;N;;;;; +07EF;NKO COMBINING LONG HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07F0;NKO COMBINING LONG LOW TONE;Mn;230;NSM;;;;;N;;;;; +07F1;NKO COMBINING LONG RISING TONE;Mn;230;NSM;;;;;N;;;;; +07F3;NKO COMBINING DOUBLE DOT ABOVE;Mn;230;NSM;;;;;N;;;;; +0816;SAMARITAN MARK IN;Mn;230;NSM;;;;;N;;;;; +0817;SAMARITAN MARK IN-ALAF;Mn;230;NSM;;;;;N;;;;; +0818;SAMARITAN MARK OCCLUSION;Mn;230;NSM;;;;;N;;;;; +0819;SAMARITAN MARK DAGESH;Mn;230;NSM;;;;;N;;;;; +081B;SAMARITAN MARK EPENTHETIC YUT;Mn;230;NSM;;;;;N;;;;; +081C;SAMARITAN VOWEL SIGN LONG E;Mn;230;NSM;;;;;N;;;;; +081D;SAMARITAN VOWEL SIGN E;Mn;230;NSM;;;;;N;;;;; +081E;SAMARITAN VOWEL SIGN OVERLONG AA;Mn;230;NSM;;;;;N;;;;; +081F;SAMARITAN VOWEL SIGN LONG AA;Mn;230;NSM;;;;;N;;;;; +0820;SAMARITAN VOWEL SIGN AA;Mn;230;NSM;;;;;N;;;;; +0821;SAMARITAN VOWEL SIGN OVERLONG A;Mn;230;NSM;;;;;N;;;;; +0822;SAMARITAN VOWEL SIGN LONG A;Mn;230;NSM;;;;;N;;;;; +0823;SAMARITAN VOWEL SIGN A;Mn;230;NSM;;;;;N;;;;; +0825;SAMARITAN VOWEL SIGN SHORT A;Mn;230;NSM;;;;;N;;;;; +0826;SAMARITAN VOWEL SIGN LONG U;Mn;230;NSM;;;;;N;;;;; +0827;SAMARITAN VOWEL SIGN U;Mn;230;NSM;;;;;N;;;;; +0829;SAMARITAN VOWEL SIGN LONG I;Mn;230;NSM;;;;;N;;;;; +082A;SAMARITAN VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +082B;SAMARITAN VOWEL SIGN O;Mn;230;NSM;;;;;N;;;;; +082C;SAMARITAN VOWEL SIGN SUKUN;Mn;230;NSM;;;;;N;;;;; +082D;SAMARITAN MARK NEQUDAA;Mn;230;NSM;;;;;N;;;;; +0951;DEVANAGARI STRESS SIGN UDATTA;Mn;230;NSM;;;;;N;;;;; +0953;DEVANAGARI GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +0954;DEVANAGARI ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +0F82;TIBETAN SIGN NYI ZLA NAA DA;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU WITH ORNAMENT;;;; +0F83;TIBETAN SIGN SNA LDAN;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU;;;; +0F86;TIBETAN SIGN LCI RTAGS;Mn;230;NSM;;;;;N;;;;; +0F87;TIBETAN SIGN YANG RTAGS;Mn;230;NSM;;;;;N;;;;; +135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;; +17DD;KHMER SIGN ATTHACAN;Mn;230;NSM;;;;;N;;;;; +193A;LIMBU SIGN KEMPHRENG;Mn;230;NSM;;;;;N;;;;; +1A17;BUGINESE VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +1A75;TAI THAM SIGN TONE-1;Mn;230;NSM;;;;;N;;;;; +1A76;TAI THAM SIGN TONE-2;Mn;230;NSM;;;;;N;;;;; +1A77;TAI THAM SIGN KHUEN TONE-3;Mn;230;NSM;;;;;N;;;;; +1A78;TAI THAM SIGN KHUEN TONE-4;Mn;230;NSM;;;;;N;;;;; +1A79;TAI THAM SIGN KHUEN TONE-5;Mn;230;NSM;;;;;N;;;;; +1A7A;TAI THAM SIGN RA HAAM;Mn;230;NSM;;;;;N;;;;; +1A7B;TAI THAM SIGN MAI SAM;Mn;230;NSM;;;;;N;;;;; +1A7C;TAI THAM SIGN KHUEN-LUE KARAN;Mn;230;NSM;;;;;N;;;;; +1B6B;BALINESE MUSICAL SYMBOL COMBINING TEGEH;Mn;230;NSM;;;;;N;;;;; +1B6D;BALINESE MUSICAL SYMBOL COMBINING KEMPUL;Mn;230;NSM;;;;;N;;;;; +1B6E;BALINESE MUSICAL SYMBOL COMBINING KEMPLI;Mn;230;NSM;;;;;N;;;;; +1B6F;BALINESE MUSICAL SYMBOL COMBINING JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B70;BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B71;BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B72;BALINESE MUSICAL SYMBOL COMBINING BENDE;Mn;230;NSM;;;;;N;;;;; +1B73;BALINESE MUSICAL SYMBOL COMBINING GONG;Mn;230;NSM;;;;;N;;;;; +1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;; +1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;; +1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;; +1CDA;VEDIC TONE DOUBLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CDB;VEDIC TONE TRIPLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CE0;VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA;Mn;230;NSM;;;;;N;;;;; +1DC0;COMBINING DOTTED GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC1;COMBINING DOTTED ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC3;COMBINING SUSPENSION MARK;Mn;230;NSM;;;;;N;;;;; +1DC4;COMBINING MACRON-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DC5;COMBINING GRAVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC6;COMBINING MACRON-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC7;COMBINING ACUTE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC8;COMBINING GRAVE-ACUTE-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC9;COMBINING ACUTE-GRAVE-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DCB;COMBINING BREVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DCC;COMBINING MACRON-BREVE;Mn;230;NSM;;;;;N;;;;; +1DD1;COMBINING UR ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD2;COMBINING US ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD3;COMBINING LATIN SMALL LETTER FLATTENED OPEN A ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD4;COMBINING LATIN SMALL LETTER AE;Mn;230;NSM;;;;;N;;;;; +1DD5;COMBINING LATIN SMALL LETTER AO;Mn;230;NSM;;;;;N;;;;; +1DD6;COMBINING LATIN SMALL LETTER AV;Mn;230;NSM;;;;;N;;;;; +1DD7;COMBINING LATIN SMALL LETTER C CEDILLA;Mn;230;NSM;;;;;N;;;;; +1DD8;COMBINING LATIN SMALL LETTER INSULAR D;Mn;230;NSM;;;;;N;;;;; +1DD9;COMBINING LATIN SMALL LETTER ETH;Mn;230;NSM;;;;;N;;;;; +1DDA;COMBINING LATIN SMALL LETTER G;Mn;230;NSM;;;;;N;;;;; +1DDB;COMBINING LATIN LETTER SMALL CAPITAL G;Mn;230;NSM;;;;;N;;;;; +1DDC;COMBINING LATIN SMALL LETTER K;Mn;230;NSM;;;;;N;;;;; +1DDD;COMBINING LATIN SMALL LETTER L;Mn;230;NSM;;;;;N;;;;; +1DDE;COMBINING LATIN LETTER SMALL CAPITAL L;Mn;230;NSM;;;;;N;;;;; +1DDF;COMBINING LATIN LETTER SMALL CAPITAL M;Mn;230;NSM;;;;;N;;;;; +1DE0;COMBINING LATIN SMALL LETTER N;Mn;230;NSM;;;;;N;;;;; +1DE1;COMBINING LATIN LETTER SMALL CAPITAL N;Mn;230;NSM;;;;;N;;;;; +1DE2;COMBINING LATIN LETTER SMALL CAPITAL R;Mn;230;NSM;;;;;N;;;;; +1DE3;COMBINING LATIN SMALL LETTER R ROTUNDA;Mn;230;NSM;;;;;N;;;;; +1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;; +1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;; +1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;; +1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;; +20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;; +20D4;COMBINING ANTICLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING ANTICLOCKWISE ARROW ABOVE;;;; +20D5;COMBINING CLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING CLOCKWISE ARROW ABOVE;;;; +20D6;COMBINING LEFT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT ARROW ABOVE;;;; +20D7;COMBINING RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT ARROW ABOVE;;;; +20DB;COMBINING THREE DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING THREE DOTS ABOVE;;;; +20DC;COMBINING FOUR DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING FOUR DOTS ABOVE;;;; +20E1;COMBINING LEFT RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT RIGHT ARROW ABOVE;;;; +20E7;COMBINING ANNUITY SYMBOL;Mn;230;NSM;;;;;N;;;;; +20E9;COMBINING WIDE BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +20F0;COMBINING ASTERISK ABOVE;Mn;230;NSM;;;;;N;;;;; +2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;; +2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;; +2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;; +2DE0;COMBINING CYRILLIC LETTER BE;Mn;230;NSM;;;;;N;;;;; +2DE1;COMBINING CYRILLIC LETTER VE;Mn;230;NSM;;;;;N;;;;; +2DE2;COMBINING CYRILLIC LETTER GHE;Mn;230;NSM;;;;;N;;;;; +2DE3;COMBINING CYRILLIC LETTER DE;Mn;230;NSM;;;;;N;;;;; +2DE4;COMBINING CYRILLIC LETTER ZHE;Mn;230;NSM;;;;;N;;;;; +2DE5;COMBINING CYRILLIC LETTER ZE;Mn;230;NSM;;;;;N;;;;; +2DE6;COMBINING CYRILLIC LETTER KA;Mn;230;NSM;;;;;N;;;;; +2DE7;COMBINING CYRILLIC LETTER EL;Mn;230;NSM;;;;;N;;;;; +2DE8;COMBINING CYRILLIC LETTER EM;Mn;230;NSM;;;;;N;;;;; +2DE9;COMBINING CYRILLIC LETTER EN;Mn;230;NSM;;;;;N;;;;; +2DEA;COMBINING CYRILLIC LETTER O;Mn;230;NSM;;;;;N;;;;; +2DEB;COMBINING CYRILLIC LETTER PE;Mn;230;NSM;;;;;N;;;;; +2DEC;COMBINING CYRILLIC LETTER ER;Mn;230;NSM;;;;;N;;;;; +2DED;COMBINING CYRILLIC LETTER ES;Mn;230;NSM;;;;;N;;;;; +2DEE;COMBINING CYRILLIC LETTER TE;Mn;230;NSM;;;;;N;;;;; +2DEF;COMBINING CYRILLIC LETTER HA;Mn;230;NSM;;;;;N;;;;; +2DF0;COMBINING CYRILLIC LETTER TSE;Mn;230;NSM;;;;;N;;;;; +2DF1;COMBINING CYRILLIC LETTER CHE;Mn;230;NSM;;;;;N;;;;; +2DF2;COMBINING CYRILLIC LETTER SHA;Mn;230;NSM;;;;;N;;;;; +2DF3;COMBINING CYRILLIC LETTER SHCHA;Mn;230;NSM;;;;;N;;;;; +2DF4;COMBINING CYRILLIC LETTER FITA;Mn;230;NSM;;;;;N;;;;; +2DF5;COMBINING CYRILLIC LETTER ES-TE;Mn;230;NSM;;;;;N;;;;; +2DF6;COMBINING CYRILLIC LETTER A;Mn;230;NSM;;;;;N;;;;; +2DF7;COMBINING CYRILLIC LETTER IE;Mn;230;NSM;;;;;N;;;;; +2DF8;COMBINING CYRILLIC LETTER DJERV;Mn;230;NSM;;;;;N;;;;; +2DF9;COMBINING CYRILLIC LETTER MONOGRAPH UK;Mn;230;NSM;;;;;N;;;;; +2DFA;COMBINING CYRILLIC LETTER YAT;Mn;230;NSM;;;;;N;;;;; +2DFB;COMBINING CYRILLIC LETTER YU;Mn;230;NSM;;;;;N;;;;; +2DFC;COMBINING CYRILLIC LETTER IOTIFIED A;Mn;230;NSM;;;;;N;;;;; +2DFD;COMBINING CYRILLIC LETTER LITTLE YUS;Mn;230;NSM;;;;;N;;;;; +2DFE;COMBINING CYRILLIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;; +2DFF;COMBINING CYRILLIC LETTER IOTIFIED BIG YUS;Mn;230;NSM;;;;;N;;;;; +A66F;COMBINING CYRILLIC VZMET;Mn;230;NSM;;;;;N;;;;; +A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;; +A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;; +A6F0;BAMUM COMBINING MARK KOQNDON;Mn;230;NSM;;;;;N;;;;; +A6F1;BAMUM COMBINING MARK TUKWENTIS;Mn;230;NSM;;;;;N;;;;; +A8E0;COMBINING DEVANAGARI DIGIT ZERO;Mn;230;NSM;;;;;N;;;;; +A8E1;COMBINING DEVANAGARI DIGIT ONE;Mn;230;NSM;;;;;N;;;;; +A8E2;COMBINING DEVANAGARI DIGIT TWO;Mn;230;NSM;;;;;N;;;;; +A8E3;COMBINING DEVANAGARI DIGIT THREE;Mn;230;NSM;;;;;N;;;;; +A8E4;COMBINING DEVANAGARI DIGIT FOUR;Mn;230;NSM;;;;;N;;;;; +A8E5;COMBINING DEVANAGARI DIGIT FIVE;Mn;230;NSM;;;;;N;;;;; +A8E6;COMBINING DEVANAGARI DIGIT SIX;Mn;230;NSM;;;;;N;;;;; +A8E7;COMBINING DEVANAGARI DIGIT SEVEN;Mn;230;NSM;;;;;N;;;;; +A8E8;COMBINING DEVANAGARI DIGIT EIGHT;Mn;230;NSM;;;;;N;;;;; +A8E9;COMBINING DEVANAGARI DIGIT NINE;Mn;230;NSM;;;;;N;;;;; +A8EA;COMBINING DEVANAGARI LETTER A;Mn;230;NSM;;;;;N;;;;; +A8EB;COMBINING DEVANAGARI LETTER U;Mn;230;NSM;;;;;N;;;;; +A8EC;COMBINING DEVANAGARI LETTER KA;Mn;230;NSM;;;;;N;;;;; +A8ED;COMBINING DEVANAGARI LETTER NA;Mn;230;NSM;;;;;N;;;;; +A8EE;COMBINING DEVANAGARI LETTER PA;Mn;230;NSM;;;;;N;;;;; +A8EF;COMBINING DEVANAGARI LETTER RA;Mn;230;NSM;;;;;N;;;;; +A8F0;COMBINING DEVANAGARI LETTER VI;Mn;230;NSM;;;;;N;;;;; +A8F1;COMBINING DEVANAGARI SIGN AVAGRAHA;Mn;230;NSM;;;;;N;;;;; +AAB0;TAI VIET MAI KANG;Mn;230;NSM;;;;;N;;;;; +AAB2;TAI VIET VOWEL I;Mn;230;NSM;;;;;N;;;;; +AAB3;TAI VIET VOWEL UE;Mn;230;NSM;;;;;N;;;;; +AAB7;TAI VIET MAI KHIT;Mn;230;NSM;;;;;N;;;;; +AAB8;TAI VIET VOWEL IA;Mn;230;NSM;;;;;N;;;;; +AABE;TAI VIET VOWEL AM;Mn;230;NSM;;;;;N;;;;; +AABF;TAI VIET TONE MAI EK;Mn;230;NSM;;;;;N;;;;; +AAC1;TAI VIET TONE MAI THO;Mn;230;NSM;;;;;N;;;;; +FE20;COMBINING LIGATURE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE21;COMBINING LIGATURE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE22;COMBINING DOUBLE TILDE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE23;COMBINING DOUBLE TILDE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;; +10A0F;KHAROSHTHI SIGN VISARGA;Mn;230;NSM;;;;;N;;;;; +10A38;KHAROSHTHI SIGN BAR ABOVE;Mn;230;NSM;;;;;N;;;;; +1D185;MUSICAL SYMBOL COMBINING DOIT;Mn;230;NSM;;;;;N;;;;; +1D186;MUSICAL SYMBOL COMBINING RIP;Mn;230;NSM;;;;;N;;;;; +1D187;MUSICAL SYMBOL COMBINING FLIP;Mn;230;NSM;;;;;N;;;;; +1D188;MUSICAL SYMBOL COMBINING SMEAR;Mn;230;NSM;;;;;N;;;;; +1D189;MUSICAL SYMBOL COMBINING BEND;Mn;230;NSM;;;;;N;;;;; +1D1AA;MUSICAL SYMBOL COMBINING DOWN BOW;Mn;230;NSM;;;;;N;;;;; +1D1AB;MUSICAL SYMBOL COMBINING UP BOW;Mn;230;NSM;;;;;N;;;;; +1D1AC;MUSICAL SYMBOL COMBINING HARMONIC;Mn;230;NSM;;;;;N;;;;; +1D1AD;MUSICAL SYMBOL COMBINING SNAP PIZZICATO;Mn;230;NSM;;;;;N;;;;; +1D242;COMBINING GREEK MUSICAL TRISEME;Mn;230;NSM;;;;;N;;;;; +1D243;COMBINING GREEK MUSICAL TETRASEME;Mn;230;NSM;;;;;N;;;;; +1D244;COMBINING GREEK MUSICAL PENTASEME;Mn;230;NSM;;;;;N;;;;; diff --git a/rowcolumn_diacritics.sh b/rowcolumn_diacritics.sh new file mode 100644 index 0000000..1c29d4b --- /dev/null +++ b/rowcolumn_diacritics.sh @@ -0,0 +1 @@ +ROWCOLUMN_DIACRITICS=("\U305" "\U30d" "\U30e" "\U310" "\U312" "\U33d" "\U33e" "\U33f" "\U346" "\U34a" "\U34b" "\U34c" "\U350" "\U351" "\U352" "\U357" "\U35b" "\U363" "\U364" "\U365" "\U366" "\U367" "\U368" "\U369" "\U36a" "\U36b" "\U36c" "\U36d" "\U36e" "\U36f" "\U483" "\U484" "\U485" "\U486" "\U487" "\U592" "\U593" "\U594" "\U595" "\U597" "\U598" "\U599" "\U59c" "\U59d" "\U59e" "\U59f" "\U5a0" "\U5a1" "\U5a8" "\U5a9" "\U5ab" "\U5ac" "\U5af" "\U5c4" "\U610" "\U611" "\U612" "\U613" "\U614" "\U615" "\U616" "\U617" "\U657" "\U658" "\U659" "\U65a" "\U65b" "\U65d" "\U65e" "\U6d6" "\U6d7" "\U6d8" "\U6d9" "\U6da" "\U6db" "\U6dc" "\U6df" "\U6e0" "\U6e1" "\U6e2" "\U6e4" "\U6e7" "\U6e8" "\U6eb" "\U6ec" "\U730" "\U732" "\U733" "\U735" "\U736" "\U73a" "\U73d" "\U73f" "\U740" "\U741" "\U743" "\U745" "\U747" "\U749" "\U74a" "\U7eb" "\U7ec" "\U7ed" "\U7ee" "\U7ef" "\U7f0" "\U7f1" "\U7f3" "\U816" "\U817" "\U818" "\U819" "\U81b" "\U81c" "\U81d" "\U81e" "\U81f" "\U820" "\U821" "\U822" "\U823" "\U825" "\U826" "\U827" "\U829" "\U82a" "\U82b" "\U82c" "\U82d" "\U951" "\U953" "\U954" "\Uf82" "\Uf83" "\Uf86" "\Uf87" "\U135d" "\U135e" "\U135f" "\U17dd" "\U193a" "\U1a17" "\U1a75" "\U1a76" "\U1a77" "\U1a78" "\U1a79" "\U1a7a" "\U1a7b" "\U1a7c" "\U1b6b" "\U1b6d" "\U1b6e" "\U1b6f" "\U1b70" "\U1b71" "\U1b72" "\U1b73" "\U1cd0" "\U1cd1" "\U1cd2" "\U1cda" "\U1cdb" "\U1ce0" "\U1dc0" "\U1dc1" "\U1dc3" "\U1dc4" "\U1dc5" "\U1dc6" "\U1dc7" "\U1dc8" "\U1dc9" "\U1dcb" "\U1dcc" "\U1dd1" "\U1dd2" "\U1dd3" "\U1dd4" "\U1dd5" "\U1dd6" "\U1dd7" "\U1dd8" "\U1dd9" "\U1dda" "\U1ddb" "\U1ddc" "\U1ddd" "\U1dde" "\U1ddf" "\U1de0" "\U1de1" "\U1de2" "\U1de3" "\U1de4" "\U1de5" "\U1de6" "\U1dfe" "\U20d0" "\U20d1" "\U20d4" "\U20d5" "\U20d6" "\U20d7" "\U20db" "\U20dc" "\U20e1" "\U20e7" "\U20e9" "\U20f0" "\U2cef" "\U2cf0" "\U2cf1" "\U2de0" "\U2de1" "\U2de2" "\U2de3" "\U2de4" "\U2de5" "\U2de6" "\U2de7" "\U2de8" "\U2de9" "\U2dea" "\U2deb" "\U2dec" "\U2ded" "\U2dee" "\U2def" "\U2df0" "\U2df1" "\U2df2" "\U2df3" "\U2df4" "\U2df5" "\U2df6" "\U2df7" "\U2df8" "\U2df9" "\U2dfa" "\U2dfb" "\U2dfc" "\U2dfd" "\U2dfe" "\U2dff" "\Ua66f" "\Ua67c" "\Ua67d" "\Ua6f0" "\Ua6f1" "\Ua8e0" "\Ua8e1" "\Ua8e2" "\Ua8e3" "\Ua8e4" "\Ua8e5" "\Ua8e6" "\Ua8e7" "\Ua8e8" "\Ua8e9" "\Ua8ea" "\Ua8eb" "\Ua8ec" "\Ua8ed" "\Ua8ee" "\Ua8ef" "\Ua8f0" "\Ua8f1" "\Uaab0" "\Uaab2" "\Uaab3" "\Uaab7" "\Uaab8" "\Uaabe" "\Uaabf" "\Uaac1" "\Ufe20" "\Ufe21" "\Ufe22" "\Ufe23" "\Ufe24" "\Ufe25" "\Ufe26" "\U10a0f" "\U10a38" "\U1d185" "\U1d186" "\U1d187" "\U1d188" "\U1d189" "\U1d1aa" "\U1d1ab" "\U1d1ac" "\U1d1ad" "\U1d242" "\U1d243" "\U1d244" ) diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c new file mode 100644 index 0000000..829c0fc --- /dev/null +++ b/rowcolumn_diacritics_helpers.c @@ -0,0 +1,391 @@ +#include <stdint.h> + +uint16_t diacritic_to_num(uint32_t code) +{ + switch (code) { + case 0x305: + return code - 0x305 + 1; + case 0x30d: + case 0x30e: + return code - 0x30d + 2; + case 0x310: + return code - 0x310 + 4; + case 0x312: + return code - 0x312 + 5; + case 0x33d: + case 0x33e: + case 0x33f: + return code - 0x33d + 6; + case 0x346: + return code - 0x346 + 9; + case 0x34a: + case 0x34b: + case 0x34c: + return code - 0x34a + 10; + case 0x350: + case 0x351: + case 0x352: + return code - 0x350 + 13; + case 0x357: + return code - 0x357 + 16; + case 0x35b: + return code - 0x35b + 17; + case 0x363: + case 0x364: + case 0x365: + case 0x366: + case 0x367: + case 0x368: + case 0x369: + case 0x36a: + case 0x36b: + case 0x36c: + case 0x36d: + case 0x36e: + case 0x36f: + return code - 0x363 + 18; + case 0x483: + case 0x484: + case 0x485: + case 0x486: + case 0x487: + return code - 0x483 + 31; + case 0x592: + case 0x593: + case 0x594: + case 0x595: + return code - 0x592 + 36; + case 0x597: + case 0x598: + case 0x599: + return code - 0x597 + 40; + case 0x59c: + case 0x59d: + case 0x59e: + case 0x59f: + case 0x5a0: + case 0x5a1: + return code - 0x59c + 43; + case 0x5a8: + case 0x5a9: + return code - 0x5a8 + 49; + case 0x5ab: + case 0x5ac: + return code - 0x5ab + 51; + case 0x5af: + return code - 0x5af + 53; + case 0x5c4: + return code - 0x5c4 + 54; + case 0x610: + case 0x611: + case 0x612: + case 0x613: + case 0x614: + case 0x615: + case 0x616: + case 0x617: + return code - 0x610 + 55; + case 0x657: + case 0x658: + case 0x659: + case 0x65a: + case 0x65b: + return code - 0x657 + 63; + case 0x65d: + case 0x65e: + return code - 0x65d + 68; + case 0x6d6: + case 0x6d7: + case 0x6d8: + case 0x6d9: + case 0x6da: + case 0x6db: + case 0x6dc: + return code - 0x6d6 + 70; + case 0x6df: + case 0x6e0: + case 0x6e1: + case 0x6e2: + return code - 0x6df + 77; + case 0x6e4: + return code - 0x6e4 + 81; + case 0x6e7: + case 0x6e8: + return code - 0x6e7 + 82; + case 0x6eb: + case 0x6ec: + return code - 0x6eb + 84; + case 0x730: + return code - 0x730 + 86; + case 0x732: + case 0x733: + return code - 0x732 + 87; + case 0x735: + case 0x736: + return code - 0x735 + 89; + case 0x73a: + return code - 0x73a + 91; + case 0x73d: + return code - 0x73d + 92; + case 0x73f: + case 0x740: + case 0x741: + return code - 0x73f + 93; + case 0x743: + return code - 0x743 + 96; + case 0x745: + return code - 0x745 + 97; + case 0x747: + return code - 0x747 + 98; + case 0x749: + case 0x74a: + return code - 0x749 + 99; + case 0x7eb: + case 0x7ec: + case 0x7ed: + case 0x7ee: + case 0x7ef: + case 0x7f0: + case 0x7f1: + return code - 0x7eb + 101; + case 0x7f3: + return code - 0x7f3 + 108; + case 0x816: + case 0x817: + case 0x818: + case 0x819: + return code - 0x816 + 109; + case 0x81b: + case 0x81c: + case 0x81d: + case 0x81e: + case 0x81f: + case 0x820: + case 0x821: + case 0x822: + case 0x823: + return code - 0x81b + 113; + case 0x825: + case 0x826: + case 0x827: + return code - 0x825 + 122; + case 0x829: + case 0x82a: + case 0x82b: + case 0x82c: + case 0x82d: + return code - 0x829 + 125; + case 0x951: + return code - 0x951 + 130; + case 0x953: + case 0x954: + return code - 0x953 + 131; + case 0xf82: + case 0xf83: + return code - 0xf82 + 133; + case 0xf86: + case 0xf87: + return code - 0xf86 + 135; + case 0x135d: + case 0x135e: + case 0x135f: + return code - 0x135d + 137; + case 0x17dd: + return code - 0x17dd + 140; + case 0x193a: + return code - 0x193a + 141; + case 0x1a17: + return code - 0x1a17 + 142; + case 0x1a75: + case 0x1a76: + case 0x1a77: + case 0x1a78: + case 0x1a79: + case 0x1a7a: + case 0x1a7b: + case 0x1a7c: + return code - 0x1a75 + 143; + case 0x1b6b: + return code - 0x1b6b + 151; + case 0x1b6d: + case 0x1b6e: + case 0x1b6f: + case 0x1b70: + case 0x1b71: + case 0x1b72: + case 0x1b73: + return code - 0x1b6d + 152; + case 0x1cd0: + case 0x1cd1: + case 0x1cd2: + return code - 0x1cd0 + 159; + case 0x1cda: + case 0x1cdb: + return code - 0x1cda + 162; + case 0x1ce0: + return code - 0x1ce0 + 164; + case 0x1dc0: + case 0x1dc1: + return code - 0x1dc0 + 165; + case 0x1dc3: + case 0x1dc4: + case 0x1dc5: + case 0x1dc6: + case 0x1dc7: + case 0x1dc8: + case 0x1dc9: + return code - 0x1dc3 + 167; + case 0x1dcb: + case 0x1dcc: + return code - 0x1dcb + 174; + case 0x1dd1: + case 0x1dd2: + case 0x1dd3: + case 0x1dd4: + case 0x1dd5: + case 0x1dd6: + case 0x1dd7: + case 0x1dd8: + case 0x1dd9: + case 0x1dda: + case 0x1ddb: + case 0x1ddc: + case 0x1ddd: + case 0x1dde: + case 0x1ddf: + case 0x1de0: + case 0x1de1: + case 0x1de2: + case 0x1de3: + case 0x1de4: + case 0x1de5: + case 0x1de6: + return code - 0x1dd1 + 176; + case 0x1dfe: + return code - 0x1dfe + 198; + case 0x20d0: + case 0x20d1: + return code - 0x20d0 + 199; + case 0x20d4: + case 0x20d5: + case 0x20d6: + case 0x20d7: + return code - 0x20d4 + 201; + case 0x20db: + case 0x20dc: + return code - 0x20db + 205; + case 0x20e1: + return code - 0x20e1 + 207; + case 0x20e7: + return code - 0x20e7 + 208; + case 0x20e9: + return code - 0x20e9 + 209; + case 0x20f0: + return code - 0x20f0 + 210; + case 0x2cef: + case 0x2cf0: + case 0x2cf1: + return code - 0x2cef + 211; + case 0x2de0: + case 0x2de1: + case 0x2de2: + case 0x2de3: + case 0x2de4: + case 0x2de5: + case 0x2de6: + case 0x2de7: + case 0x2de8: + case 0x2de9: + case 0x2dea: + case 0x2deb: + case 0x2dec: + case 0x2ded: + case 0x2dee: + case 0x2def: + case 0x2df0: + case 0x2df1: + case 0x2df2: + case 0x2df3: + case 0x2df4: + case 0x2df5: + case 0x2df6: + case 0x2df7: + case 0x2df8: + case 0x2df9: + case 0x2dfa: + case 0x2dfb: + case 0x2dfc: + case 0x2dfd: + case 0x2dfe: + case 0x2dff: + return code - 0x2de0 + 214; + case 0xa66f: + return code - 0xa66f + 246; + case 0xa67c: + case 0xa67d: + return code - 0xa67c + 247; + case 0xa6f0: + case 0xa6f1: + return code - 0xa6f0 + 249; + case 0xa8e0: + case 0xa8e1: + case 0xa8e2: + case 0xa8e3: + case 0xa8e4: + case 0xa8e5: + case 0xa8e6: + case 0xa8e7: + case 0xa8e8: + case 0xa8e9: + case 0xa8ea: + case 0xa8eb: + case 0xa8ec: + case 0xa8ed: + case 0xa8ee: + case 0xa8ef: + case 0xa8f0: + case 0xa8f1: + return code - 0xa8e0 + 251; + case 0xaab0: + return code - 0xaab0 + 269; + case 0xaab2: + case 0xaab3: + return code - 0xaab2 + 270; + case 0xaab7: + case 0xaab8: + return code - 0xaab7 + 272; + case 0xaabe: + case 0xaabf: + return code - 0xaabe + 274; + case 0xaac1: + return code - 0xaac1 + 276; + case 0xfe20: + case 0xfe21: + case 0xfe22: + case 0xfe23: + case 0xfe24: + case 0xfe25: + case 0xfe26: + return code - 0xfe20 + 277; + case 0x10a0f: + return code - 0x10a0f + 284; + case 0x10a38: + return code - 0x10a38 + 285; + case 0x1d185: + case 0x1d186: + case 0x1d187: + case 0x1d188: + case 0x1d189: + return code - 0x1d185 + 286; + case 0x1d1aa: + case 0x1d1ab: + case 0x1d1ac: + case 0x1d1ad: + return code - 0x1d1aa + 291; + case 0x1d242: + case 0x1d243: + case 0x1d244: + return code - 0x1d242 + 295; + } + return 0; +} @@ -19,6 +19,7 @@ #include "st.h" #include "win.h" +#include "graphics.h" #if defined(__linux) #include <pty.h> @@ -35,7 +36,10 @@ #define ESC_ARG_SIZ 16 #define STR_BUF_SIZ ESC_BUF_SIZ #define STR_ARG_SIZ ESC_ARG_SIZ -#define HISTSIZE 2000 + +/* 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) @@ -43,9 +47,10 @@ #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]) + +#define TSCREEN term.screen[IS_SET(MODE_ALTSCREEN)] +#define TLINEOFFSET(y) (((y) + TSCREEN.cur - TSCREEN.off + TSCREEN.size) % TSCREEN.size) +#define TLINE(y) (TSCREEN.buffer[TLINEOFFSET(y)]) enum term_mode { MODE_WRAP = 1 << 0, @@ -113,15 +118,23 @@ typedef struct { int alt; } Selection; +/* Screen lines */ +typedef struct { + Line* buffer; /* ring buffer */ + int size; /* size of buffer */ + int cur; /* start of active screen */ + int off; /* scrollback line offset */ + TCursor sc; /* saved cursor */ +} LineBuffer; + /* Internal representation of the screen */ typedef struct { int row; /* nb row */ 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 pixw; /* width of the text area in pixels */ + int pixh; /* height of the text area in pixels */ + LineBuffer screen[2]; /* screen and alternate screen */ + int linelen; /* allocated line length */ int *dirty; /* dirtyness of lines */ TCursor c; /* cursor */ int ocx; /* old cursor col */ @@ -192,8 +205,8 @@ static void tnewline(int); static void tputtab(int); static void tputc(Rune); static void treset(void); -static void tscrollup(int, int, int); -static void tscrolldown(int, int, int); +static void tscrollup(int, int); +static void tscrolldown(int, int); static void tsetattr(const int *, int); static void tsetchar(Rune, const Glyph *, int, int); static void tsetdirt(int, int); @@ -210,6 +223,8 @@ static void tdeftran(char); static void tstrsequence(uchar); static void drawregion(int, int, int, int); +static void clearline(Line, Glyph, int, int); +static Line ensureline(Line); static void selnormalize(void); static void selscroll(int, int); @@ -220,7 +235,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); @@ -239,6 +253,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) { @@ -415,11 +433,12 @@ int tlinelen(int y) { int i = term.col; + Line line = TLINE(y); - if (TLINE(y)[i - 1].mode & ATTR_WRAP) + if (line[i - 1].mode & ATTR_WRAP) return i; - while (i > 0 && TLINE(y)[i - 1].u == ' ') + while (i > 0 && line[i - 1].u == ' ') --i; return i; @@ -623,6 +642,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); } @@ -722,11 +747,14 @@ sigchld(int a) int stat; pid_t p; - if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + if ((p = waitpid(-1, &stat, WNOHANG)) < 0) die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); - if (pid != p) + if (pid != p) { + /* reinstall sigchld handler */ + signal(SIGCHLD, sigchld); return; + } if (WIFEXITED(stat) && WEXITSTATUS(stat)) die("child exited with status %d\n", WEXITSTATUS(stat)); @@ -810,7 +838,7 @@ ttynew(const char *line, char *cmd, const char *out, char **args) break; default: #ifdef __OpenBSD__ - if (pledge("stdio rpath tty proc", NULL) == -1) + if (pledge("stdio rpath tty proc exec", NULL) == -1) die("pledge\n"); #endif close(s); @@ -826,7 +854,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); @@ -838,7 +870,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) @@ -851,9 +900,6 @@ 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); @@ -884,6 +930,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. @@ -892,6 +939,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); @@ -933,11 +983,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; @@ -959,12 +1014,15 @@ int tattrset(int attr) { int i, j; + int y = TLINEOFFSET(0); for (i = 0; i < term.row-1; i++) { + Line line = TSCREEN.buffer[y]; for (j = 0; j < term.col-1; j++) { - if (term.line[i][j].mode & attr) + if (line[j].mode & attr) return 1; } + y = (y+1) % TSCREEN.size; } return 0; @@ -986,14 +1044,17 @@ void tsetdirtattr(int attr) { int i, j; + int y = TLINEOFFSET(0); for (i = 0; i < term.row-1; i++) { + Line line = TSCREEN.buffer[y]; for (j = 0; j < term.col-1; j++) { - if (term.line[i][j].mode & attr) { + if (line[j].mode & attr) { tsetdirt(i, i); break; } } + y = (y+1) % TSCREEN.size; } } @@ -1006,27 +1067,22 @@ tfulldirt(void) void tcursor(int mode) { - static TCursor c[2]; - int alt = IS_SET(MODE_ALTSCREEN); - if (mode == CURSOR_SAVE) { - c[alt] = term.c; + TSCREEN.sc = term.c; } else if (mode == CURSOR_LOAD) { - term.c = c[alt]; - tmoveto(c[alt].x, c[alt].y); + term.c = TSCREEN.sc; + tmoveto(term.c.x, term.c.y); } } void treset(void) { - uint i; - - term.c = (TCursor){{ - .mode = ATTR_NULL, - .fg = defaultfg, - .bg = defaultbg - }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + int i, j; + Glyph g = (Glyph){.mode = ATTR_NULL, + .fg = defaultfg, + .bg = defaultbg, + .decor = DECOR_DEFAULT_COLOR}; memset(term.tabs, 0, term.col * sizeof(*term.tabs)); for (i = tabspaces; i < term.col; i += tabspaces) @@ -1038,17 +1094,38 @@ treset(void) term.charset = 0; for (i = 0; i < 2; i++) { - tmoveto(0, 0); - tcursor(CURSOR_SAVE); - tclearregion(0, 0, term.col-1, term.row-1); - tswapscreen(); + term.screen[i].sc = (TCursor){{ + .fg = defaultfg, + .bg = defaultbg, + .decor = DECOR_DEFAULT_COLOR + }}; + term.screen[i].cur = 0; + term.screen[i].off = 0; + for (j = 0; j < term.row; ++j) { + if (term.col != term.linelen) + term.screen[i].buffer[j] = xrealloc(term.screen[i].buffer[j], term.col * sizeof(Glyph)); + clearline(term.screen[i].buffer[j], g, 0, term.col); + } + for (j = term.row; j < term.screen[i].size; ++j) { + free(term.screen[i].buffer[j]); + term.screen[i].buffer[j] = NULL; + } } + tcursor(CURSOR_LOAD); + term.linelen = term.col; + tfulldirt(); } void tnew(int col, int row) { - term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + int i; + term = (Term){}; + term.screen[0].buffer = xmalloc(HISTSIZE * sizeof(Line)); + term.screen[0].size = HISTSIZE; + term.screen[1].buffer = NULL; + for (i = 0; i < HISTSIZE; ++i) term.screen[0].buffer[i] = NULL; + tresize(col, row); treset(); } @@ -1056,104 +1133,108 @@ tnew(int col, int row) void tswapscreen(void) { - Line *tmp = term.line; - - term.line = term.alt; - term.alt = tmp; term.mode ^= MODE_ALTSCREEN; tfulldirt(); } void -kscrolldown(const Arg* a) +kscrollup(const Arg *a) { int n = a->i; - if (n < 0) - n = term.row + n; - - if (n > term.scr) - n = term.scr; + if (IS_SET(MODE_ALTSCREEN)) + return; - if (term.scr > 0) { - term.scr -= n; - selscroll(0, -n); - tfulldirt(); - } + if (n < 0) n = (-n) * term.row; + if (n > TSCREEN.size - term.row - TSCREEN.off) n = TSCREEN.size - term.row - TSCREEN.off; + while (!TLINE(-n)) --n; + TSCREEN.off += n; + selscroll(0, n); + tfulldirt(); } void -kscrollup(const Arg* a) +kscrolldown(const Arg *a) { + int n = a->i; - if (n < 0) - n = term.row + n; + if (IS_SET(MODE_ALTSCREEN)) + return; - if (term.scr <= HISTSIZE-n) { - term.scr += n; - selscroll(0, n); - tfulldirt(); - } + if (n < 0) n = (-n) * term.row; + if (n > TSCREEN.off) n = TSCREEN.off; + TSCREEN.off -= n; + selscroll(0, -n); + tfulldirt(); } void -tscrolldown(int orig, int n, int copyhist) +tscrolldown(int orig, int n) { 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; + /* Ensure that lines are allocated */ + for (i = -n; i < 0; i++) { + TLINE(i) = ensureline(TLINE(i)); } - tsetdirt(orig, term.bot-n); - tclearregion(0, term.bot-n+1, term.col-1, term.bot); - - for (i = term.bot; i >= orig+n; i--) { - temp = term.line[i]; - term.line[i] = term.line[i-n]; - term.line[i-n] = temp; + /* Shift non-scrolling areas in ring buffer */ + for (i = term.bot+1; i < term.row; i++) { + temp = TLINE(i); + TLINE(i) = TLINE(i-n); + TLINE(i-n) = temp; + } + for (i = 0; i < orig; i++) { + temp = TLINE(i); + TLINE(i) = TLINE(i-n); + TLINE(i-n) = temp; } - if (term.scr == 0) - selscroll(orig, n); + /* Scroll buffer */ + TSCREEN.cur = (TSCREEN.cur + TSCREEN.size - n) % TSCREEN.size; + /* Clear lines that have entered the view */ + tclearregion(0, orig, term.linelen-1, orig+n-1); + /* Redraw portion of the screen that has scrolled */ + tsetdirt(orig+n-1, term.bot); + selscroll(orig, n); } void -tscrollup(int orig, int n, int copyhist) +tscrollup(int orig, int n) { 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; + /* Ensure that lines are allocated */ + for (i = term.row; i < term.row + n; i++) { + TLINE(i) = ensureline(TLINE(i)); } - 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); - - for (i = orig; i <= term.bot-n; i++) { - temp = term.line[i]; - term.line[i] = term.line[i+n]; - term.line[i+n] = temp; + /* Shift non-scrolling areas in ring buffer */ + for (i = orig-1; i >= 0; i--) { + temp = TLINE(i); + TLINE(i) = TLINE(i+n); + TLINE(i+n) = temp; + } + for (i = term.row-1; i >term.bot; i--) { + temp = TLINE(i); + TLINE(i) = TLINE(i+n); + TLINE(i+n) = temp; } - if (term.scr == 0) - selscroll(orig, -n); + /* Scroll buffer */ + TSCREEN.cur = (TSCREEN.cur + n) % TSCREEN.size; + /* Clear lines that have entered the view */ + tclearregion(0, term.bot-n+1, term.linelen-1, term.bot); + /* Redraw portion of the screen that has scrolled */ + tsetdirt(orig, term.bot-n+1); + selscroll(orig, -n); } void @@ -1182,7 +1263,7 @@ tnewline(int first_col) int y = term.c.y; if (y == term.bot) { - tscrollup(term.top, 1, 1); + tscrollup(term.top, 1); } else { y++; } @@ -1194,6 +1275,7 @@ csiparse(void) { char *p = csiescseq.buf, *np; long int v; + int sep = ';'; /* colon or semi-colon, but not both */ csiescseq.narg = 0; if (*p == '?') { @@ -1211,7 +1293,9 @@ csiparse(void) v = -1; csiescseq.arg[csiescseq.narg++] = v; p = np; - if (*p != ';' || csiescseq.narg == ESC_ARG_SIZ) + if (sep == ';' && *p == ':') + sep = ':'; /* allow override to colon once */ + if (*p != sep || csiescseq.narg == ESC_ARG_SIZ) break; p++; } @@ -1256,6 +1340,7 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */ "│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */ }; + Line line = TLINE(y); /* * The table is proudly stolen from rxvt. @@ -1264,25 +1349,43 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41]) utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ); - if (term.line[y][x].mode & ATTR_WIDE) { + if (line[x].mode & ATTR_WIDE) { if (x+1 < term.col) { - term.line[y][x+1].u = ' '; - term.line[y][x+1].mode &= ~ATTR_WDUMMY; + line[x+1].u = ' '; + line[x+1].mode &= ~ATTR_WDUMMY; } - } else if (term.line[y][x].mode & ATTR_WDUMMY) { - term.line[y][x-1].u = ' '; - term.line[y][x-1].mode &= ~ATTR_WIDE; + } else if (line[x].mode & ATTR_WDUMMY) { + line[x-1].u = ' '; + line[x-1].mode &= ~ATTR_WIDE; + } + + if (u == ' ' && line[x].mode & ATTR_IMAGE && + tgetisclassicplaceholder(&line[x])) { + // This is a workaround: don't overwrite classic placement + // placeholders with space symbols (unlike Unicode placeholders + // which must be overwritten by anything). + line[x].bg = attr->bg; + term.dirty[y] = 1; + return; } term.dirty[y] = 1; - term.line[y][x] = *attr; - term.line[y][x].u = u; + line[x] = *attr; + line[x].u = u; + + if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) { + line[x].u = 0; + line[x].mode |= ATTR_IMAGE; + } + + if (isboxdraw(u)) + line[x].mode |= ATTR_BOXDRAW; } void tclearregion(int x1, int y1, int x2, int y2) { - int x, y, temp; + int x, y, L, S, temp; Glyph *gp; if (x1 > x2) @@ -1290,22 +1393,122 @@ tclearregion(int x1, int y1, int x2, int y2) if (y1 > y2) temp = y1, y1 = y2, y2 = temp; - LIMIT(x1, 0, term.col-1); - LIMIT(x2, 0, term.col-1); + LIMIT(x1, 0, term.linelen-1); + LIMIT(x2, 0, term.linelen-1); LIMIT(y1, 0, term.row-1); LIMIT(y2, 0, term.row-1); + L = TLINEOFFSET(y1); for (y = y1; y <= y2; y++) { term.dirty[y] = 1; for (x = x1; x <= x2; x++) { - gp = &term.line[y][x]; + gp = &TSCREEN.buffer[L][x]; if (selected(x, y)) selclear(); gp->fg = term.c.attr.fg; gp->bg = term.c.attr.bg; + gp->decor = term.c.attr.decor; gp->mode = 0; gp->u = ' '; } + L = (L + 1) % TSCREEN.size; + } +} + +/// 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, + Glyph *text_underneath) { + 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 = &TLINE(y)[x]; + if (selected(x, y)) + selclear(); + if (text_underneath) { + Glyph *to_save = gp; + // If there is already a classic placeholder, + // use the text underneath it. This will leave + // holes in images, but at least we are + // guaranteed to restore the original text. + if (gp->mode & ATTR_IMAGE && + tgetisclassicplaceholder(gp)) { + Glyph *under = + gr_get_glyph_underneath_image( + tgetimgid(gp), + tgetimgplacementid(gp), + tgetimgcol(gp), + tgetimgrow(gp)); + if (under) + to_save = under; + } + text_underneath[cols * row + col] = *to_save; + } + 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, Glyph *gp), + void *data) { + for (int row = 0; row < term.row; ++row) { + for (int col = 0; col < term.col; ++col) { + Glyph *gp = &TLINE(row)[col]; + if (gp->mode & ATTR_IMAGE) { + if (callback(data, gp)) + term.dirty[row] = 1; + } + } + } +} + +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 = &TLINE(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; + } + } + } } } @@ -1320,7 +1523,7 @@ tdeletechar(int n) dst = term.c.x; src = term.c.x + n; size = term.col - src; - line = term.line[term.c.y]; + line = TLINE(term.c.y); memmove(&line[dst], &line[src], size * sizeof(Glyph)); tclearregion(term.col-n, term.c.y, term.col-1, term.c.y); @@ -1337,7 +1540,7 @@ tinsertblank(int n) dst = term.c.x + n; src = term.c.x; size = term.col - dst; - line = term.line[term.c.y]; + line = TLINE(term.c.y); memmove(&line[dst], &line[src], size * sizeof(Glyph)); tclearregion(src, term.c.y, dst - 1, term.c.y); @@ -1347,14 +1550,14 @@ void tinsertblankline(int n) { if (BETWEEN(term.c.y, term.top, term.bot)) - tscrolldown(term.c.y, n, 0); + tscrolldown(term.c.y, n); } void tdeleteline(int n) { if (BETWEEN(term.c.y, term.top, term.bot)) - tscrollup(term.c.y, n, 0); + tscrollup(term.c.y, n); } int32_t @@ -1427,6 +1630,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; @@ -1439,6 +1643,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 */ @@ -1462,6 +1680,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; @@ -1489,6 +1708,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; @@ -1764,7 +1990,7 @@ csihandle(void) } break; case 1: /* above */ - if (term.c.y > 1) + if (term.c.y > 0) tclearregion(0, 0, term.col-1, term.c.y-1); tclearregion(0, term.c.y, term.c.x, term.c.y); break; @@ -1792,11 +2018,11 @@ csihandle(void) case 'S': /* SU -- Scroll <n> line up */ if (csiescseq.priv) break; DEFAULT(csiescseq.arg[0], 1); - tscrollup(term.top, csiescseq.arg[0], 0); + tscrollup(term.top, csiescseq.arg[0]); break; case 'T': /* SD -- Scroll <n> line down */ DEFAULT(csiescseq.arg[0], 1); - tscrolldown(term.top, csiescseq.arg[0], 0); + tscrolldown(term.top, csiescseq.arg[0]); break; case 'L': /* IL -- Insert <n> blank lines */ DEFAULT(csiescseq.arg[0], 1); @@ -1860,7 +2086,11 @@ csihandle(void) tcursor(CURSOR_SAVE); break; case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ - tcursor(CURSOR_LOAD); + if (csiescseq.priv) { + goto unknown; + } else { + tcursor(CURSOR_LOAD); + } break; case ' ': switch (csiescseq.mode[1]) { @@ -1872,6 +2102,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; } } @@ -2021,8 +2284,27 @@ 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, + res->placeholder.text_underneath); + } + 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; } @@ -2054,6 +2336,59 @@ strparse(void) } void +externalpipe(const Arg *arg) +{ + int to[2]; + char buf[UTF_SIZ]; + void (*oldsigpipe)(int); + Glyph *bp, *end; + int lastpos, n, newline; + + if (pipe(to) == -1) + return; + + switch (fork()) { + case -1: + close(to[0]); + close(to[1]); + return; + case 0: + dup2(to[0], STDIN_FILENO); + close(to[0]); + close(to[1]); + execvp(((char **)arg->v)[0], (char **)arg->v); + fprintf(stderr, "st: execvp %s\n", ((char **)arg->v)[0]); + perror("failed"); + exit(0); + } + + close(to[0]); + /* ignore sigpipe for now, in case child exists early */ + oldsigpipe = signal(SIGPIPE, SIG_IGN); + newline = 0; + for (n = 0; n < term.row; n++) { + bp = TLINE(n); + lastpos = MIN(tlinelen(n) + 1, term.col) - 1; + if (lastpos < 0) + break; + end = &bp[lastpos + 1]; + for (; bp < end; ++bp) + if (xwrite(to[1], buf, utf8encode(bp->u, buf)) < 0) + break; + if ((newline = TLINE(n)[lastpos].mode & ATTR_WRAP)) + continue; + if (xwrite(to[1], "\n", 1) < 0) + break; + newline = 0; + } + if (newline) + (void)xwrite(to[1], "\n", 1); + close(to[1]); + /* restore */ + signal(SIGPIPE, oldsigpipe); +} + +void strdump(void) { size_t i; @@ -2141,7 +2476,7 @@ tdumpline(int n) char buf[UTF_SIZ]; const Glyph *bp, *end; - bp = &term.line[n][0]; + bp = &TLINE(n)[0]; end = &bp[MIN(tlinelen(n), term.col) - 1]; if (bp != end || bp->u != ' ') { for ( ; bp <= end; ++bp) @@ -2368,7 +2703,7 @@ eschandle(uchar ascii) return 0; case 'D': /* IND -- Linefeed */ if (term.c.y == term.bot) { - tscrollup(term.top, 1, 1); + tscrollup(term.top, 1); } else { tmoveto(term.c.x, term.c.y+1); } @@ -2381,7 +2716,7 @@ eschandle(uchar ascii) break; case 'M': /* RI -- Reverse index */ if (term.c.y == term.top) { - tscrolldown(term.top, 1, 1); + tscrolldown(term.top, 1); } else { tmoveto(term.c.x, term.c.y-1); } @@ -2528,11 +2863,46 @@ check_control_code: if (selected(term.c.x, term.c.y)) selclear(); - gp = &term.line[term.c.y][term.c.x]; + // wcwidth is broken on some systems, set the width to 0 if it's a known + // diacritic used for images. + uint16_t num = diacritic_to_num(u); + if (num != 0) + width = 0; + // Set the width to 1 if it's an image placeholder character. + if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) + width = 1; + + 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 = &TLINE(term.c.y-1)[term.col-1]; + else if (term.c.state & CURSOR_WRAPNEXT) + gp = &TLINE(term.c.y)[term.c.x]; + else + gp = &TLINE(term.c.y)[term.c.x-1]; + 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 = &TLINE(term.c.y)[term.c.x]; if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { gp->mode |= ATTR_WRAP; tnewline(1); - gp = &term.line[term.c.y][term.c.x]; + gp = &TLINE(term.c.y)[term.c.x]; } if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) { @@ -2545,7 +2915,7 @@ check_control_code: tnewline(1); else tmoveto(term.col - width, term.c.y); - gp = &term.line[term.c.y][term.c.x]; + gp = &TLINE(term.c.y)[term.c.x]; } tsetchar(u, &term.c.attr, term.c.x, term.c.y); @@ -2576,6 +2946,11 @@ twrite(const char *buf, int buflen, int show_ctrl) Rune u; int n; + if (TSCREEN.off) { + TSCREEN.off = 0; + tfulldirt(); + } + for (n = 0; n < buflen; n += charsize) { if (IS_SET(MODE_UTF8)) { /* process a complete utf8 char */ @@ -2602,64 +2977,85 @@ twrite(const char *buf, int buflen, int show_ctrl) } void +clearline(Line line, Glyph g, int x, int xend) +{ + int i; + g.mode = 0; + g.u = ' '; + for (i = x; i < xend; ++i) { + line[i] = g; + } +} + +Line +ensureline(Line line) +{ + if (!line) { + line = xmalloc(term.linelen * sizeof(Glyph)); + } + return line; +} + +void tresize(int col, int row) { int i, j; int minrow = MIN(row, term.row); int mincol = MIN(col, term.col); + int linelen = MAX(col, term.linelen); int *bp; - TCursor c; - if (col < 1 || row < 1) { + if (col < 1 || row < 1 || row > HISTSIZE) { fprintf(stderr, "tresize: error resizing to %dx%d\n", col, row); return; } - /* - * slide screen to keep cursor where we expect it - - * tscrollup would work here, but we can optimize to - * memmove because we're freeing the earlier lines - */ - for (i = 0; i <= term.c.y - row; i++) { - free(term.line[i]); - free(term.alt[i]); + /* Shift buffer to keep the cursor where we expect it */ + if (row <= term.c.y) { + term.screen[0].cur = (term.screen[0].cur - row + term.c.y + 1) % term.screen[0].size; + } + + /* Resize and clear line buffers as needed */ + if (linelen > term.linelen) { + for (i = 0; i < term.screen[0].size; ++i) { + if (term.screen[0].buffer[i]) { + term.screen[0].buffer[i] = xrealloc(term.screen[0].buffer[i], linelen * sizeof(Glyph)); + clearline(term.screen[0].buffer[i], term.c.attr, term.linelen, linelen); + } + } + for (i = 0; i < minrow; ++i) { + term.screen[1].buffer[i] = xrealloc(term.screen[1].buffer[i], linelen * sizeof(Glyph)); + clearline(term.screen[1].buffer[i], term.c.attr, term.linelen, linelen); + } + } + /* Allocate all visible lines for regular line buffer */ + for (j = term.screen[0].cur, i = 0; i < row; ++i, j = (j + 1) % term.screen[0].size) + { + if (!term.screen[0].buffer[j]) { + term.screen[0].buffer[j] = xmalloc(linelen * sizeof(Glyph)); + } + if (i >= term.row) { + clearline(term.screen[0].buffer[j], term.c.attr, 0, linelen); + } } - /* ensure that both src and dst are not NULL */ - if (i > 0) { - memmove(term.line, term.line + i, row * sizeof(Line)); - memmove(term.alt, term.alt + i, row * sizeof(Line)); + /* Resize alt screen */ + term.screen[1].cur = 0; + term.screen[1].size = row; + for (i = row; i < term.row; ++i) { + free(term.screen[1].buffer[i]); } - for (i += row; i < term.row; i++) { - free(term.line[i]); - free(term.alt[i]); + term.screen[1].buffer = xrealloc(term.screen[1].buffer, row * sizeof(Line)); + for (i = term.row; i < row; ++i) { + term.screen[1].buffer[i] = xmalloc(linelen * sizeof(Glyph)); + clearline(term.screen[1].buffer[i], term.c.attr, 0, linelen); } /* resize to new height */ - term.line = xrealloc(term.line, row * sizeof(Line)); - term.alt = xrealloc(term.alt, row * sizeof(Line)); 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)); - term.alt[i] = xrealloc(term.alt[i], col * sizeof(Glyph)); - } - - /* allocate any new rows */ - for (/* i = minrow */; i < row; i++) { - term.line[i] = xmalloc(col * sizeof(Glyph)); - term.alt[i] = xmalloc(col * sizeof(Glyph)); - } + /* fix tabstops */ if (col > term.col) { bp = term.tabs + term.col; @@ -2669,26 +3065,16 @@ tresize(int col, int row) for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) *bp = 1; } + /* update terminal size */ term.col = col; term.row = row; + term.linelen = linelen; /* reset scrolling region */ tsetscroll(0, row-1); /* make use of the LIMIT in tmoveto */ tmoveto(term.c.x, term.c.y); - /* Clearing both screens (it makes dirty all lines) */ - c = term.c; - for (i = 0; i < 2; i++) { - if (mincol < col && 0 < minrow) { - tclearregion(mincol, 0, col - 1, minrow - 1); - } - if (0 < col && minrow < row) { - tclearregion(0, minrow, col - 1, row - 1); - } - tswapscreen(); - tcursor(CURSOR_LOAD); - } - term.c = c; + tfulldirt(); } void @@ -2700,15 +3086,20 @@ resettitle(void) void drawregion(int x1, int y1, int x2, int y2) { - int y; + int y, L; - for (y = y1; y < y2; y++) { - if (!term.dirty[y]) - continue; + xstartimagedraw(term.dirty, term.row); - term.dirty[y] = 0; - xdrawline(TLINE(y), x1, y, x2); + L = TLINEOFFSET(y1); + for (y = y1; y < y2; y++) { + if (term.dirty[y]) { + term.dirty[y] = 0; + xdrawline(TSCREEN.buffer[L], x1, y, x2); + } + L = (L + 1) % TSCREEN.size; } + + xfinishimagedraw(); } void @@ -2722,16 +3113,16 @@ draw(void) /* adjust cursor position */ LIMIT(term.ocx, 0, term.col-1); LIMIT(term.ocy, 0, term.row-1); - if (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY) + if (TLINE(term.ocy)[term.ocx].mode & ATTR_WDUMMY) term.ocx--; - if (term.line[term.c.y][cx].mode & ATTR_WDUMMY) + if (TLINE(term.c.y)[cx].mode & ATTR_WDUMMY) cx--; drawregion(0, 0, term.col, term.row); - 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.line[term.ocy], term.col); + if (TSCREEN.off == 0) + xdrawcursor(cx, term.c.y, TLINE(term.c.y)[cx], + term.ocx, term.ocy, TLINE(term.ocy)[term.ocx], + TLINE(term.ocy), term.col); term.ocx = cx; term.ocy = term.c.y; xfinishdraw(); @@ -2745,3 +3136,9 @@ redraw(void) tfulldirt(); draw(); } + +Glyph +getglyphat(int col, int row) +{ + return TLINE(row)[col]; +} @@ -13,13 +13,18 @@ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) #define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \ (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))) #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) #define IS_TRUECOL(x) (1 << 24 & (x)) +#define HISTSIZE 2000 + +// 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, @@ -34,7 +39,15 @@ enum glyph_attribute { ATTR_WRAP = 1 << 8, ATTR_WIDE = 1 << 9, ATTR_WDUMMY = 1 << 10, + ATTR_BOXDRAW = 1 << 11, ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, + ATTR_IMAGE = 1 << 14, +}; + +enum drawing_mode { + DRAW_NONE = 0, + DRAW_BG = 1 << 0, + DRAW_FG = 1 << 1, }; enum selection_mode { @@ -53,6 +66,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; @@ -66,6 +87,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; @@ -82,8 +104,7 @@ void die(const char *, ...); void redraw(void); void draw(void); -void kscrolldown(const Arg *); -void kscrollup(const Arg *); +void externalpipe(const Arg *); void printscreen(const Arg *); void printsel(const Arg *); void sendbreak(const Arg *); @@ -108,12 +129,22 @@ 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); void *xrealloc(void *, size_t); char *xstrdup(const char *); +int isboxdraw(Rune); +ushort boxdrawindex(const Glyph *); +#ifdef XFT_VERSION +/* only exposed to x.c, otherwise we'll need Xft.h for the types */ +void boxdraw_xinit(Display *, Colormap, XftDraw *, Visual *); +void drawboxes(int, int, int, int, XftColor *, XftColor *, const XftGlyphFontSpec *, int); +#endif + /* config.h globals */ extern char *utmp; extern char *scroll; @@ -127,4 +158,70 @@ extern unsigned int tabspaces; extern unsigned int defaultfg; extern unsigned int defaultbg; extern unsigned int defaultcs; -extern float alpha; +extern const int boxdraw, boxdraw_bold, boxdraw_braille; + +// 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); +} @@ -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, @@ -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(); @@ -4,6 +4,8 @@ #include <limits.h> #include <locale.h> #include <signal.h> +#include <stdio.h> +#include <stdlib.h> #include <sys/select.h> #include <time.h> #include <unistd.h> @@ -19,6 +21,7 @@ char *argv0; #include "arg.h" #include "st.h" #include "win.h" +#include "graphics.h" #include "hb.h" /* types used in config.h */ @@ -60,6 +63,14 @@ static void zoom(const Arg *); static void zoomabs(const Arg *); static void zoomreset(const Arg *); static void ttysend(const Arg *); +void kscrollup(const Arg *); +void kscrolldown(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" @@ -82,6 +93,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 */ @@ -145,8 +157,10 @@ typedef struct { static inline ushort sixd_to_16bit(int); static void xresetfontsettings(ushort mode, Font **font, int *frcflags); static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); -static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int); +static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, 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 *); @@ -223,6 +237,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 { @@ -246,7 +261,6 @@ static char *usedfont = NULL; static double usedfontsize = 0; static double defaultfontsize = 0; -static char *opt_alpha = NULL; static char *opt_class = NULL; static char **opt_cmd = NULL; static char *opt_embed = NULL; @@ -332,10 +346,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; } @@ -343,7 +419,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; } @@ -456,6 +532,9 @@ mouseaction(XEvent *e, uint release) /* ignore Button<N>mask for Button<N> - it's set on release */ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + mouse_col = evcol(e); + mouse_row = evrow(e); + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { if (ms->release == release && ms->button == e->xbutton.button && @@ -743,6 +822,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); @@ -817,9 +899,6 @@ xloadcols(void) die("could not allocate color %d\n", i); } - /* set alpha value of bg color */ - if (opt_alpha) - alpha = strtof(opt_alpha, NULL); dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); dc.col[defaultbg].pixel &= 0x00FFFFFF; dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; @@ -853,6 +932,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; } @@ -880,8 +965,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; @@ -1025,7 +1110,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)) @@ -1035,7 +1121,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; } @@ -1145,7 +1231,7 @@ xinit(int cols, int rows) { XGCValues gcvalues; Cursor cursor; - Window parent; + Window parent, root; pid_t thispid = getpid(); XColor xmousefg, xmousebg; XWindowAttributes attr; @@ -1155,17 +1241,19 @@ xinit(int cols, int rows) die("can't open display\n"); xw.scr = XDefaultScreen(xw.dpy); - if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) { - parent = XRootWindow(xw.dpy, xw.scr); - xw.depth = 32; + 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; } - XMatchVisualInfo(xw.dpy, xw.scr, xw.depth, TrueColor, &vis); - xw.vis = vis.visual; - /* font */ if (!FcInit()) die("could not init fontconfig.\n"); @@ -1178,8 +1266,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) @@ -1198,11 +1286,15 @@ xinit(int cols, int rows) win.w, win.h, 0, xw.depth, InputOutput, xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity | CWEventMask | CWColormap, &xw.attrs); + if (parent != root) + XReparentWindow(xw.dpy, xw.win, parent, xw.l, xw.t); memset(&gcvalues, 0, sizeof(gcvalues)); gcvalues.graphics_exposures = False; - xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, xw.depth); - dc.gc = XCreateGC(xw.dpy, xw.buf, GCGraphicsExposures, &gcvalues); + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + xw.depth); XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); @@ -1259,6 +1351,11 @@ 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); + + boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis); } void @@ -1280,135 +1377,230 @@ xresetfontsettings(ushort mode, Font **font, int *frcflags) 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; - ushort mode = glyphs[0].mode & ~ATTR_WRAP; + 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; - float runewidth = win.cw * ((glyphs[0].mode & ATTR_WIDE) ? 2.0f : 1.0f); + float runewidth = win.cw; Rune rune; FT_UInt glyphidx; FcResult fcres; FcPattern *fcpattern, *fontpattern; FcFontSet *fcsets[] = { NULL }; FcCharSet *fccharset; - int f, code_idx, numspecs = 0; + int i, f, length = 0, start = 0, numspecs = 0; float cluster_xp = xp, cluster_yp = yp; HbTransformData shaped = { 0 }; /* Initial values. */ + mode = prevmode = glyphs[0].mode & ~ATTR_WRAP; xresetfontsettings(mode, &font, &frcflags); - /* Shape the segment. */ - hbtransform(&shaped, font->match, glyphs, 0, len); - xp = winx; yp = winy + font->ascent; - cluster_xp = xp; cluster_yp = yp; + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + mode = glyphs[i].mode & ~ATTR_WRAP; - for (code_idx = 0; code_idx < shaped.count; code_idx++) { - int idx = shaped.glyphs[code_idx].cluster; - - if (glyphs[idx].mode & ATTR_WDUMMY) + /* Skip dummy wide-character spacing. */ + if (mode & ATTR_WDUMMY && i < (len - 1)) continue; - /* Advance the drawing cursor if we've moved to a new cluster */ - if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) { - xp += runewidth; - cluster_xp = xp; - cluster_yp = yp; - } - - if (shaped.glyphs[code_idx].codepoint != 0) { - /* If symbol is found, put it into the specs. */ - specs[numspecs].font = font->match; - specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint; - specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.); - specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.); - cluster_xp += shaped.positions[code_idx].x_advance / 64.; - cluster_yp += shaped.positions[code_idx].y_advance / 64.; - numspecs++; - } else { - /* If it's not found, try to fetch it through the font cache. */ - rune = glyphs[idx].u; - for (f = 0; f < frclen; f++) { - glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); - /* Everything correct. */ - if (glyphidx && frc[f].flags == frcflags) - break; - /* We got a default font for a not found glyph. */ - if (!glyphidx && frc[f].flags == frcflags - && frc[f].unicodep == rune) { - break; - } + if ( + prevmode != mode + || ATTRCMP(glyphs[start], glyphs[i]) + || selected(x + i, y) != selected(x + start, y) + || i == (len - 1) + ) { + /* Handle 1-character wide segments and end of line */ + length = i - start; + if (i == start) { + length = 1; + } else if (i == (len - 1)) { + length = (i - start + 1); } - /* Nothing was found. Use fontconfig to find matching font. */ - if (f >= frclen) { - if (!font->set) - font->set = FcFontSort(0, font->pattern, - 1, 0, &fcres); - fcsets[0] = font->set; - - /* - * Nothing was found in the cache. Now use - * some dozen of Fontconfig calls to get the - * font for one single character. - * - * Xft and fontconfig are design failures. - */ - fcpattern = FcPatternDuplicate(font->pattern); - fccharset = FcCharSetCreate(); - - FcCharSetAddChar(fccharset, rune); - FcPatternAddCharSet(fcpattern, FC_CHARSET, - fccharset); - FcPatternAddBool(fcpattern, FC_SCALABLE, 1); - - FcConfigSubstitute(0, fcpattern, - FcMatchPattern); - FcDefaultSubstitute(fcpattern); - - fontpattern = FcFontSetMatch(0, fcsets, 1, - fcpattern, &fcres); - - /* Allocate memory for the new cache entry. */ - if (frclen >= frccap) { - frccap += 16; - frc = xrealloc(frc, frccap * sizeof(Fontcache)); + /* Shape the segment. */ + hbtransform(&shaped, font->match, glyphs, start, length); + runewidth = win.cw * ((glyphs[start].mode & ATTR_WIDE) ? 2.0f : 1.0f); + cluster_xp = xp; cluster_yp = yp; + for (int code_idx = 0; code_idx < shaped.count; code_idx++) { + int idx = shaped.glyphs[code_idx].cluster; + + if (glyphs[start + idx].mode & ATTR_WDUMMY) + continue; + + /* Draw spaces for image placeholders (images + * will be drawn separately). */ + if (glyphs[start + idx].mode & ATTR_IMAGE) + continue; + + /* Advance the drawing cursor if we've moved to a new cluster */ + if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) { + xp += runewidth; + cluster_xp = xp; + cluster_yp = yp; + runewidth = win.cw * ((glyphs[start + idx].mode & ATTR_WIDE) ? 2.0f : 1.0f); } - frc[frclen].font = XftFontOpenPattern(xw.dpy, - fontpattern); - if (!frc[frclen].font) - die("XftFontOpenPattern failed seeking fallback font: %s\n", - strerror(errno)); - frc[frclen].flags = frcflags; - frc[frclen].unicodep = rune; - - glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + if (glyphs[start + idx].mode & ATTR_BOXDRAW) { + /* minor shoehorning: boxdraw uses only this ushort */ + specs[numspecs].font = font->match; + specs[numspecs].glyph = boxdrawindex(&glyphs[start + idx]); + specs[numspecs].x = xp; + specs[numspecs].y = yp; + numspecs++; + } else if (shaped.glyphs[code_idx].codepoint != 0) { + /* If symbol is found, put it into the specs. */ + specs[numspecs].font = font->match; + specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint; + specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.); + specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.); + cluster_xp += shaped.positions[code_idx].x_advance / 64.; + cluster_yp += shaped.positions[code_idx].y_advance / 64.; + numspecs++; + } else { + /* If it's not found, try to fetch it through the font cache. */ + rune = glyphs[start + idx].u; + for (f = 0; f < frclen; f++) { + glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); + /* Everything correct. */ + if (glyphidx && frc[f].flags == frcflags) + break; + /* We got a default font for a not found glyph. */ + if (!glyphidx && frc[f].flags == frcflags + && frc[f].unicodep == rune) { + break; + } + } + + /* Nothing was found. Use fontconfig to find matching font. */ + if (f >= frclen) { + if (!font->set) + font->set = FcFontSort(0, font->pattern, 1, 0, &fcres); + fcsets[0] = font->set; + + /* + * Nothing was found in the cache. Now use + * some dozen of Fontconfig calls to get the + * font for one single character. + * + * Xft and fontconfig are design failures. + */ + fcpattern = FcPatternDuplicate(font->pattern); + fccharset = FcCharSetCreate(); + + FcCharSetAddChar(fccharset, rune); + FcPatternAddCharSet(fcpattern, FC_CHARSET, + fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, 1); + + FcConfigSubstitute(0, fcpattern, + FcMatchPattern); + FcDefaultSubstitute(fcpattern); + + fontpattern = FcFontSetMatch(0, fcsets, 1, + fcpattern, &fcres); + + /* Allocate memory for the new cache entry. */ + if (frclen >= frccap) { + frccap += 16; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + frc[frclen].font = XftFontOpenPattern(xw.dpy, + fontpattern); + if (!frc[frclen].font) + die("XftFontOpenPattern failed seeking fallback font: %s\n", + strerror(errno)); + frc[frclen].flags = frcflags; + frc[frclen].unicodep = rune; + + glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + + f = frclen; + frclen++; + + FcPatternDestroy(fcpattern); + FcCharSetDestroy(fccharset); + } + + specs[numspecs].font = frc[f].font; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + numspecs++; + } + } - f = frclen; - frclen++; + /* Cleanup and get ready for next segment. */ + hbcleanup(&shaped); + start = i; - FcPatternDestroy(fcpattern); - FcCharSetDestroy(fccharset); + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; + xresetfontsettings(mode, &font, &frcflags); + yp = winy + font->ascent; } - - specs[numspecs].font = frc[f].font; - specs[numspecs].glyph = glyphidx; - specs[numspecs].x = (short)xp; - specs[numspecs].y = (short)yp; - numspecs++; } } - /* Cleanup and get ready for next segment. */ hbcleanup(&shaped); 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) +xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int charlen, int dmode) { - 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; @@ -1497,46 +1689,105 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i fg = bg; /* Intelligent cleaning up of the borders. */ - if (x == 0) { - xclear(0, (y == 0)? 0 : winy, borderpx, - winy + win.ch + - ((winy + win.ch >= borderpx + win.th)? win.h : 0)); - } - if (winx + width >= borderpx + win.tw) { - xclear(winx + width, (y == 0)? 0 : winy, win.w, - ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); + if (dmode & DRAW_BG) { + if (x == 0) { + xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + + ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } + if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, + ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) + 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. */ + XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); } - if (y == 0) - xclear(winx, 0, winx + width, borderpx); - if (winy + win.ch >= borderpx + win.th) - xclear(winx, winy + win.ch, winx + width, win.h); - /* Clean up the region we want to draw to. */ - XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + if (!(dmode & DRAW_FG)) + return; - /* Set the clip region because Xft is sometimes dirty. */ + /* Set the clip region only for the underline. */ r.x = 0; r.y = 0; r.height = win.ch; r.width = width; XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); - /* Render the glyphs. */ - XftDrawGlyphFontSpec(xw.draw, fg, specs, len); - - /* Render underline and strikethrough. */ - if (base.mode & ATTR_UNDERLINE) { - XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, - width, 1); + /* 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]; } - - if (base.mode & ATTR_STRUCK) { - XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, - width, 1); + 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) { + 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); + } } /* Reset clip to none. */ XftDrawSetClip(xw.draw, 0); + + if (base.mode & ATTR_BOXDRAW) { + drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len); + } else { + /* 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 / 3, + width, thick); + } } void @@ -1546,7 +1797,12 @@ xdrawglyph(Glyph g, int x, int y) XftGlyphFontSpec *specs = xw.specbuf; numspecs = xmakeglyphfontspecs(specs, &g, 1, x, y); - xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1); + xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1, DRAW_BG | DRAW_FG); + if (g.mode & ATTR_IMAGE) { + gr_start_drawing(xw.buf, win.cw, win.ch); + xdrawoneimagecell(g, x, y); + gr_finish_drawing(xw.buf); + } } void @@ -1560,15 +1816,21 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int le /* Redraw the line where cursor was previously. * It will restore the ligatures broken by the cursor. */ + gr_start_drawing(xw.buf, win.cw, win.ch); xdrawline(line, 0, oy, len); + gr_finish_drawing(xw.buf); 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. */ - g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE; + g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE|ATTR_BOXDRAW; if (IS_SET(MODE_REVERSE)) { g.mode |= ATTR_REVERSE; @@ -1605,39 +1867,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int le 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) { @@ -1692,29 +2082,38 @@ xdrawline(Line line, int x1, int y1, int x2) { int i, x, ox, numspecs; Glyph base, new; - XftGlyphFontSpec *specs = xw.specbuf; - - i = ox = 0; - for (x = x1; x < x2; x++) { - new = line[x]; - if (new.mode == ATTR_WDUMMY) - continue; - if (selected(x, y1)) - new.mode ^= ATTR_REVERSE; - if ((i > 0) && ATTRCMP(base, new)) { - numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1); - xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox); - i = 0; + XftGlyphFontSpec *specs; + + /* Draw line in 2 passes: background and foreground. This way wide + * glyphs won't get truncated (#223) */ + for (int dmode = DRAW_BG; dmode <= DRAW_FG; dmode <<= 1) { + specs = xw.specbuf; + i = ox = 0; + for (x = x1; x < x2; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1); + xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox, dmode); + if (base.mode & ATTR_IMAGE && dmode == DRAW_FG) + xdrawimages(base, line, ox, y1, x); + i = 0; + } + if (i == 0) { + ox = x; + base = new; + } + i++; } - if (i == 0) { - ox = x; - base = new; + if (i > 0) { + numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1); + xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox, dmode); } - i++; - } - if (i > 0) { - numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1); - xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox); + if (i > 0 && base.mode & ATTR_IMAGE && dmode == DRAW_FG) + xdrawimages(base, line, ox, y1, x); } } @@ -1940,6 +2339,7 @@ cmessage(XEvent *e) } } else if (e->xclient.data.l[0] == xw.wmdeletewin) { ttyhangup(); + gr_deinit(); exit(0); } } @@ -1990,6 +2390,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; @@ -2081,7 +2488,8 @@ main(int argc, char *argv[]) allowaltscreen = 0; break; case 'A': - opt_alpha = EARGF(usage()); + alpha = strtof(EARGF(usage()), NULL); + LIMIT(alpha, 0.0, 1.0); break; case 'c': opt_class = EARGF(usage()); |