From b5ba691d333d3e54139b3554394e693a6873ea8e Mon Sep 17 00:00:00 2001 From: vxclutch Date: Tue, 26 May 2026 17:08:11 -0400 Subject: [PATCH] save --- Makefile | 7 +- config.def.h | 49 +- config.def.h.orig | 510 ++ config.h | 509 ++ config.mk | 5 +- graphics.c | 4393 +++++++++ graphics.h | 112 + graphics.o | Bin 0 -> 94904 bytes icat-mini.sh | 875 ++ khash.h | 627 ++ kvec.h | 90 + patches/st-alpha-20240814-a0274bc.diff | 129 + patches/st-kitty-graphics-20251230-0.9.3.diff | 8033 +++++++++++++++++ rowcolumn_diacritics_helpers.c | 391 + rowcolumn_diacritics_helpers.o | Bin 0 -> 16176 bytes st | Bin 0 -> 191880 bytes st.c | 303 +- st.c.orig | 2705 ++++++ st.h | 84 +- st.info | 6 + st.o | Bin 0 -> 88920 bytes win.h | 3 + x.c | 455 +- x.c.orig | 2447 +++++ x.o | Bin 0 -> 89208 bytes 25 files changed, 21666 insertions(+), 67 deletions(-) create mode 100644 config.def.h.orig create mode 100644 config.h create mode 100644 graphics.c create mode 100644 graphics.h create mode 100644 graphics.o create mode 100755 icat-mini.sh create mode 100644 khash.h create mode 100644 kvec.h create mode 100644 patches/st-alpha-20240814-a0274bc.diff create mode 100644 patches/st-kitty-graphics-20251230-0.9.3.diff create mode 100644 rowcolumn_diacritics_helpers.c create mode 100644 rowcolumn_diacritics_helpers.o create mode 100755 st create mode 100644 st.c.orig create mode 100644 st.o create mode 100644 x.c.orig create mode 100644 x.o diff --git a/Makefile b/Makefile index 15db421..413e7ab 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 @@ -15,8 +15,9 @@ 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 +st.o: config.h st.h win.h graphics.h +x.o: arg.h config.h st.h win.h graphics.h +graphics.c: graphics.h khash.h kvec.h st.h $(OBJ): config.h config.mk diff --git a/config.def.h b/config.def.h index 2cd740a..3601e41 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; @@ -93,6 +101,9 @@ char *termname = "st-256color"; */ unsigned int tabspaces = 8; +/* bg opacity */ +float alpha = 0.8; + /* Terminal colors (16 first used in escape sequence) */ static const char *colorname[] = { /* 8 normal colors */ @@ -163,6 +174,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 +203,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 +222,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 +236,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.def.h.orig b/config.def.h.orig new file mode 100644 index 0000000..4aadbbc --- /dev/null +++ b/config.def.h.orig @@ -0,0 +1,510 @@ +/* See LICENSE file for copyright and license details. */ + +/* + * appearance + * + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ +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 + * 2: scroll and/or utmp + * 3: SHELL environment variable + * 4: value of shell in /etc/passwd + * 5: value of shell in config.h + */ +static char *shell = "/bin/sh"; +char *utmp = NULL; +/* scroll program: to enable use a string like "scroll" */ +char *scroll = NULL; +char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + +/* identification sequence returned in DA and DECID */ +/* By default, use the same one as kitty. */ +char *vtiden = "\033[?62c"; + +/* Kerning / character bounding-box multipliers */ +static float cwscale = 1.0; +static float chscale = 1.0; + +/* + * word delimiter string + * + * More advanced example: L" `'\"()[]{}" + */ +wchar_t *worddelimiters = L" "; + +/* selection timeouts (in milliseconds) */ +static unsigned int doubleclicktimeout = 300; +static unsigned int tripleclicktimeout = 600; + +/* alt screens */ +int allowaltscreen = 1; + +/* allow certain non-interactive (insecure) window operations such as: + setting the clipboard text */ +int allowwindowops = 0; + +/* + * draw latency range in ms - from new content/keypress/etc until drawing. + * within this range, st draws when content stops arriving (idle). mostly it's + * near minlatency, but it waits longer for slow updates to avoid partial draw. + * low minlatency will tear/flicker more, as it can "detect" idle too early. + */ +static double minlatency = 2; +static double maxlatency = 33; + +/* + * blinking timeout (set to 0 to disable blinking) for the terminal blinking + * attribute. + */ +static unsigned int blinktimeout = 800; + +/* + * thickness of underline and bar cursors + */ +static unsigned int cursorthickness = 2; + +/* + * bell volume. It must be a value between -100 and 100. Use 0 for disabling + * it + */ +static int bellvolume = 0; + +/* default TERM value */ +char *termname = "st-256color"; + +/* + * spaces per tab + * + * When you are changing this value, don't forget to adapt the »it« value in + * the st.info and appropriately install the st.info in the environment where + * you use this st version. + * + * it#$tabspaces, + * + * Secondly make sure your kernel is not expanding tabs. When running `stty + * -a` »tab0« should appear. You can tell the terminal to not expand tabs by + * running following command: + * + * stty tabs + */ +unsigned int tabspaces = 8; + +/* Terminal colors (16 first used in escape sequence) */ +static const char *colorname[] = { + /* 8 normal colors */ + "black", + "red3", + "green3", + "yellow3", + "blue2", + "magenta3", + "cyan3", + "gray90", + + /* 8 bright colors */ + "gray50", + "red", + "green", + "yellow", + "#5c5cff", + "magenta", + "cyan", + "white", + + [255] = 0, + + /* more colors can be added after 255 to use with DefaultXX */ + "#cccccc", + "#555555", + "gray90", /* default foreground colour */ + "black", /* default background colour */ +}; + + +/* + * Default colors (colorname index) + * foreground, background, cursor, reverse cursor + */ +unsigned int defaultfg = 258; +unsigned int defaultbg = 259; +unsigned int defaultcs = 256; +static unsigned int defaultrcs = 257; + +/* + * Default shape of cursor + * 2: Block ("█") + * 4: Underline ("_") + * 6: Bar ("|") + * 7: Snowman ("☃") + */ +static unsigned int cursorshape = 2; + +/* + * Default columns and rows numbers + */ + +static unsigned int cols = 80; +static unsigned int rows = 24; + +/* + * Default colour and shape of the mouse cursor + */ +static unsigned int mouseshape = XC_xterm; +static unsigned int mousefg = 7; +static unsigned int mousebg = 0; + +/* + * Color used to display font attributes when fontconfig selected a font which + * doesn't match the ones requested. + */ +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"} }, + { ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} }, + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, +}; + +static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, + { ControlMask, XK_Print, toggleprinter, {.i = 0} }, + { ShiftMask, XK_Print, printscreen, {.i = 0} }, + { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, + { 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_F1, togglegrdebug, {.i = 0} }, + { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, + { TERMMOD, XK_F7, unloadimages, {.i = 0} }, + { TERMMOD, XK_F8, toggleimages, {.i = 0} }, +}; + +/* + * Special keys (change & recompile st.info accordingly) + * + * Mask value: + * * Use XK_ANY_MOD to match the key no matter modifiers state + * * Use XK_NO_MOD to match the key alone (no modifiers) + * appkey value: + * * 0: no value + * * > 0: keypad application mode enabled + * * = 2: term.numlock = 1 + * * < 0: keypad application mode disabled + * appcursor value: + * * 0: no value + * * > 0: cursor application mode enabled + * * < 0: cursor application mode disabled + * + * Be careful with the order of the definitions because st searches in + * this table sequentially, so any XK_ANY_MOD must be in the last + * position for a key. + */ + +/* + * If you want keys other than the X11 function keys (0xFD00 - 0xFFFF) + * to be mapped below, add them to this array. + */ +static KeySym mappedkeys[] = { -1 }; + +/* + * State bits to ignore when matching key or button events. By default, + * numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored. + */ +static uint ignoremod = Mod2Mask|XK_SWITCH_MOD; + +/* + * This is the huge key array which defines all compatibility to the Linux + * world. Please decide about changes wisely. + */ +static Key key[] = { + /* keysym mask string appkey appcursor */ + { XK_KP_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0}, + { XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0}, + { XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0}, + { XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0}, + { XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0}, + { XK_KP_End, ControlMask, "\033[J", -1, 0}, + { XK_KP_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_KP_End, ShiftMask, "\033[K", -1, 0}, + { XK_KP_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_KP_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[L", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_KP_Delete, ControlMask, "\033[M", -1, 0}, + { XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_KP_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0}, + { XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0}, + { XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0}, + { XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0}, + { XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0}, + { XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0}, + { XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0}, + { XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0}, + { XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0}, + { XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0}, + { XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0}, + { XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0}, + { XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0}, + { XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0}, + { XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0}, + { XK_Up, ShiftMask, "\033[1;2A", 0, 0}, + { XK_Up, Mod1Mask, "\033[1;3A", 0, 0}, + { XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0}, + { XK_Up, ControlMask, "\033[1;5A", 0, 0}, + { XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0}, + { XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0}, + { XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0}, + { XK_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_Down, ShiftMask, "\033[1;2B", 0, 0}, + { XK_Down, Mod1Mask, "\033[1;3B", 0, 0}, + { XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0}, + { XK_Down, ControlMask, "\033[1;5B", 0, 0}, + { XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0}, + { XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0}, + { XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0}, + { XK_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_Left, ShiftMask, "\033[1;2D", 0, 0}, + { XK_Left, Mod1Mask, "\033[1;3D", 0, 0}, + { XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0}, + { XK_Left, ControlMask, "\033[1;5D", 0, 0}, + { XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0}, + { XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0}, + { XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0}, + { XK_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_Right, ShiftMask, "\033[1;2C", 0, 0}, + { XK_Right, Mod1Mask, "\033[1;3C", 0, 0}, + { XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0}, + { XK_Right, ControlMask, "\033[1;5C", 0, 0}, + { XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0}, + { XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0}, + { XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0}, + { XK_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0}, + { XK_Return, Mod1Mask, "\033\r", 0, 0}, + { XK_Return, XK_ANY_MOD, "\r", 0, 0}, + { XK_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_Insert, ControlMask, "\033[L", -1, 0}, + { XK_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_Delete, ControlMask, "\033[M", -1, 0}, + { XK_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_BackSpace, XK_NO_MOD, "\177", 0, 0}, + { XK_BackSpace, Mod1Mask, "\033\177", 0, 0}, + { XK_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_End, ControlMask, "\033[J", -1, 0}, + { XK_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_End, ShiftMask, "\033[K", -1, 0}, + { XK_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_Prior, ControlMask, "\033[5;5~", 0, 0}, + { XK_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_Next, ControlMask, "\033[6;5~", 0, 0}, + { XK_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_F1, XK_NO_MOD, "\033OP" , 0, 0}, + { XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0}, + { XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0}, + { XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0}, + { XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0}, + { XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0}, + { XK_F2, XK_NO_MOD, "\033OQ" , 0, 0}, + { XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0}, + { XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0}, + { XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0}, + { XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0}, + { XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0}, + { XK_F3, XK_NO_MOD, "\033OR" , 0, 0}, + { XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0}, + { XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0}, + { XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0}, + { XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0}, + { XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0}, + { XK_F4, XK_NO_MOD, "\033OS" , 0, 0}, + { XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0}, + { XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0}, + { XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0}, + { XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0}, + { XK_F5, XK_NO_MOD, "\033[15~", 0, 0}, + { XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0}, + { XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0}, + { XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0}, + { XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0}, + { XK_F6, XK_NO_MOD, "\033[17~", 0, 0}, + { XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0}, + { XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0}, + { XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0}, + { XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0}, + { XK_F7, XK_NO_MOD, "\033[18~", 0, 0}, + { XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0}, + { XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0}, + { XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0}, + { XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0}, + { XK_F8, XK_NO_MOD, "\033[19~", 0, 0}, + { XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0}, + { XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0}, + { XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0}, + { XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0}, + { XK_F9, XK_NO_MOD, "\033[20~", 0, 0}, + { XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0}, + { XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0}, + { XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0}, + { XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0}, + { XK_F10, XK_NO_MOD, "\033[21~", 0, 0}, + { XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0}, + { XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0}, + { XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0}, + { XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0}, + { XK_F11, XK_NO_MOD, "\033[23~", 0, 0}, + { XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0}, + { XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0}, + { XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0}, + { XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0}, + { XK_F12, XK_NO_MOD, "\033[24~", 0, 0}, + { XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0}, + { XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0}, + { XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0}, + { XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0}, + { XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0}, + { XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0}, + { XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0}, + { XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0}, + { XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0}, + { XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0}, + { XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0}, + { XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0}, + { XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0}, + { XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0}, + { XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0}, + { XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0}, + { XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0}, + { XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0}, + { XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0}, + { XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0}, + { XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0}, + { XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0}, + { XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0}, + { XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0}, + { XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0}, + { XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0}, + { XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0}, +}; + +/* + * Selection types' masks. + * Use the same masks as usual. + * Button1Mask is always unset, to make masks match between ButtonPress. + * ButtonRelease and MotionNotify. + * If no match is found, regular selection is used. + */ +static uint selmasks[] = { + [SEL_RECTANGULAR] = Mod1Mask, +}; + +/* + * Printable characters in ASCII, used to estimate the advance width + * of single wide characters. + */ +static char ascii_printable[] = + " !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~"; diff --git a/config.h b/config.h new file mode 100644 index 0000000..2ebfe3e --- /dev/null +++ b/config.h @@ -0,0 +1,509 @@ +/* See LICENSE file for copyright and license details. */ + +/* + * appearance + * + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ +static char *font = "ComicShannsMono Nerd Font:pixelsize=25: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 + * 2: scroll and/or utmp + * 3: SHELL environment variable + * 4: value of shell in /etc/passwd + * 5: value of shell in config.h + */ +static char *shell = "/bin/sh"; +char *utmp = NULL; +/* scroll program: to enable use a string like "scroll" */ +char *scroll = NULL; +char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + +/* identification sequence returned in DA and DECID */ +/* By default, use the same one as kitty. */ +char *vtiden = "\033[?62c"; + +/* Kerning / character bounding-box multipliers */ +static float cwscale = 1.0; +static float chscale = 1.0; + +/* + * word delimiter string + * + * More advanced example: L" `'\"()[]{}" + */ +wchar_t *worddelimiters = L" "; + +/* selection timeouts (in milliseconds) */ +static unsigned int doubleclicktimeout = 300; +static unsigned int tripleclicktimeout = 600; + +/* alt screens */ +int allowaltscreen = 1; + +/* allow certain non-interactive (insecure) window operations such as: + setting the clipboard text */ +int allowwindowops = 0; + +/* + * draw latency range in ms - from new content/keypress/etc until drawing. + * within this range, st draws when content stops arriving (idle). mostly it's + * near minlatency, but it waits longer for slow updates to avoid partial draw. + * low minlatency will tear/flicker more, as it can "detect" idle too early. + */ +static double minlatency = 2; +static double maxlatency = 33; + +/* + * blinking timeout (set to 0 to disable blinking) for the terminal blinking + * attribute. + */ +static unsigned int blinktimeout = 800; + +/* + * thickness of underline and bar cursors + */ +static unsigned int cursorthickness = 2; + +/* + * bell volume. It must be a value between -100 and 100. Use 0 for disabling + * it + */ +static int bellvolume = 0; + +/* default TERM value */ +char *termname = "st-256color"; + +/* + * spaces per tab + * + * When you are changing this value, don't forget to adapt the »it« value in + * the st.info and appropriately install the st.info in the environment where + * you use this st version. + * + * it#$tabspaces, + * + * Secondly make sure your kernel is not expanding tabs. When running `stty + * -a` »tab0« should appear. You can tell the terminal to not expand tabs by + * running following command: + * + * stty tabs + */ +unsigned int tabspaces = 8; + +/* bg opacity */ +float alpha = 0.8; + +/* Terminal colors (16 first used in escape sequence) */ +static const char *colorname[] = { + /* 8 normal colors */ + [0] = "#000000", + [1] = "#b83019", + [2] = "#51bf37", + [3] = "#c6c43d", + [4] = "#0c24bf", + [5] = "#b93ec1", + [6] = "#53c2c5", + [7] = "#c7c7c7", + + /* 8 bright colors */ + [8] = "#676767", + [9] = "#ef766d", + [10] = "#8cf67a", + [11] = "#fefb7e", + [12] = "#6a71f6", + [13] = "#f07ef8", + [14] = "#8ef9fd", + [15] = "#feffff", + + /* special colors */ + [256] = "#000000", /* background */ + [257] = "#f7f7f7", /* foreground */ + [258] = "#c7c7c7", /* cursor */ +}; + +/* + * Default colors (colorname index) + * foreground, background, cursor, reverse cursor + */ +unsigned int defaultfg = 257; +unsigned int defaultbg = 256; +unsigned int defaultcs = 258; +unsigned int defaultrcs = 256; + +/* + * Default shape of cursor + * 2: Block ("█") + * 4: Underline ("_") + * 6: Bar ("|") + * 7: Snowman ("☃") + */ +static unsigned int cursorshape = 2; + +/* + * Default columns and rows numbers + */ + +static unsigned int cols = 80; +static unsigned int rows = 24; + +/* + * Default colour and shape of the mouse cursor + */ +static unsigned int mouseshape = XC_xterm; +static unsigned int mousefg = 7; +static unsigned int mousebg = 0; + +/* + * Color used to display font attributes when fontconfig selected a font which + * doesn't match the ones requested. + */ +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"} }, + { ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} }, + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, +}; + +static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, + { ControlMask, XK_Print, toggleprinter, {.i = 0} }, + { ShiftMask, XK_Print, printscreen, {.i = 0} }, + { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, + { 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_F1, togglegrdebug, {.i = 0} }, + { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, + { TERMMOD, XK_F7, unloadimages, {.i = 0} }, + { TERMMOD, XK_F8, toggleimages, {.i = 0} }, +}; + +/* + * Special keys (change & recompile st.info accordingly) + * + * Mask value: + * * Use XK_ANY_MOD to match the key no matter modifiers state + * * Use XK_NO_MOD to match the key alone (no modifiers) + * appkey value: + * * 0: no value + * * > 0: keypad application mode enabled + * * = 2: term.numlock = 1 + * * < 0: keypad application mode disabled + * appcursor value: + * * 0: no value + * * > 0: cursor application mode enabled + * * < 0: cursor application mode disabled + * + * Be careful with the order of the definitions because st searches in + * this table sequentially, so any XK_ANY_MOD must be in the last + * position for a key. + */ + +/* + * If you want keys other than the X11 function keys (0xFD00 - 0xFFFF) + * to be mapped below, add them to this array. + */ +static KeySym mappedkeys[] = { -1 }; + +/* + * State bits to ignore when matching key or button events. By default, + * numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored. + */ +static uint ignoremod = Mod2Mask|XK_SWITCH_MOD; + +/* + * This is the huge key array which defines all compatibility to the Linux + * world. Please decide about changes wisely. + */ +static Key key[] = { + /* keysym mask string appkey appcursor */ + { XK_KP_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0}, + { XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0}, + { XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0}, + { XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0}, + { XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0}, + { XK_KP_End, ControlMask, "\033[J", -1, 0}, + { XK_KP_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_KP_End, ShiftMask, "\033[K", -1, 0}, + { XK_KP_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_KP_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[L", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_KP_Delete, ControlMask, "\033[M", -1, 0}, + { XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_KP_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0}, + { XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0}, + { XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0}, + { XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0}, + { XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0}, + { XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0}, + { XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0}, + { XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0}, + { XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0}, + { XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0}, + { XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0}, + { XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0}, + { XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0}, + { XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0}, + { XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0}, + { XK_Up, ShiftMask, "\033[1;2A", 0, 0}, + { XK_Up, Mod1Mask, "\033[1;3A", 0, 0}, + { XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0}, + { XK_Up, ControlMask, "\033[1;5A", 0, 0}, + { XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0}, + { XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0}, + { XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0}, + { XK_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_Down, ShiftMask, "\033[1;2B", 0, 0}, + { XK_Down, Mod1Mask, "\033[1;3B", 0, 0}, + { XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0}, + { XK_Down, ControlMask, "\033[1;5B", 0, 0}, + { XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0}, + { XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0}, + { XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0}, + { XK_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_Left, ShiftMask, "\033[1;2D", 0, 0}, + { XK_Left, Mod1Mask, "\033[1;3D", 0, 0}, + { XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0}, + { XK_Left, ControlMask, "\033[1;5D", 0, 0}, + { XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0}, + { XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0}, + { XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0}, + { XK_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_Right, ShiftMask, "\033[1;2C", 0, 0}, + { XK_Right, Mod1Mask, "\033[1;3C", 0, 0}, + { XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0}, + { XK_Right, ControlMask, "\033[1;5C", 0, 0}, + { XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0}, + { XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0}, + { XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0}, + { XK_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0}, + { XK_Return, Mod1Mask, "\033\r", 0, 0}, + { XK_Return, XK_ANY_MOD, "\r", 0, 0}, + { XK_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_Insert, ControlMask, "\033[L", -1, 0}, + { XK_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_Delete, ControlMask, "\033[M", -1, 0}, + { XK_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_BackSpace, XK_NO_MOD, "\177", 0, 0}, + { XK_BackSpace, Mod1Mask, "\033\177", 0, 0}, + { XK_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_End, ControlMask, "\033[J", -1, 0}, + { XK_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_End, ShiftMask, "\033[K", -1, 0}, + { XK_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_Prior, ControlMask, "\033[5;5~", 0, 0}, + { XK_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_Next, ControlMask, "\033[6;5~", 0, 0}, + { XK_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_F1, XK_NO_MOD, "\033OP" , 0, 0}, + { XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0}, + { XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0}, + { XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0}, + { XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0}, + { XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0}, + { XK_F2, XK_NO_MOD, "\033OQ" , 0, 0}, + { XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0}, + { XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0}, + { XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0}, + { XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0}, + { XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0}, + { XK_F3, XK_NO_MOD, "\033OR" , 0, 0}, + { XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0}, + { XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0}, + { XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0}, + { XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0}, + { XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0}, + { XK_F4, XK_NO_MOD, "\033OS" , 0, 0}, + { XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0}, + { XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0}, + { XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0}, + { XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0}, + { XK_F5, XK_NO_MOD, "\033[15~", 0, 0}, + { XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0}, + { XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0}, + { XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0}, + { XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0}, + { XK_F6, XK_NO_MOD, "\033[17~", 0, 0}, + { XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0}, + { XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0}, + { XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0}, + { XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0}, + { XK_F7, XK_NO_MOD, "\033[18~", 0, 0}, + { XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0}, + { XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0}, + { XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0}, + { XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0}, + { XK_F8, XK_NO_MOD, "\033[19~", 0, 0}, + { XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0}, + { XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0}, + { XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0}, + { XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0}, + { XK_F9, XK_NO_MOD, "\033[20~", 0, 0}, + { XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0}, + { XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0}, + { XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0}, + { XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0}, + { XK_F10, XK_NO_MOD, "\033[21~", 0, 0}, + { XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0}, + { XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0}, + { XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0}, + { XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0}, + { XK_F11, XK_NO_MOD, "\033[23~", 0, 0}, + { XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0}, + { XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0}, + { XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0}, + { XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0}, + { XK_F12, XK_NO_MOD, "\033[24~", 0, 0}, + { XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0}, + { XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0}, + { XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0}, + { XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0}, + { XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0}, + { XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0}, + { XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0}, + { XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0}, + { XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0}, + { XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0}, + { XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0}, + { XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0}, + { XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0}, + { XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0}, + { XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0}, + { XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0}, + { XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0}, + { XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0}, + { XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0}, + { XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0}, + { XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0}, + { XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0}, + { XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0}, + { XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0}, + { XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0}, + { XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0}, + { XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0}, +}; + +/* + * Selection types' masks. + * Use the same masks as usual. + * Button1Mask is always unset, to make masks match between ButtonPress. + * ButtonRelease and MotionNotify. + * If no match is found, regular selection is used. + */ +static uint selmasks[] = { + [SEL_RECTANGULAR] = Mod1Mask, +}; + +/* + * Printable characters in ASCII, used to estimate the advance width + * of single wide characters. + */ +static char ascii_printable[] = + " !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~"; diff --git a/config.mk b/config.mk index 2fc854e..7b22243 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..801f5c9 --- /dev/null +++ b/graphics.c @@ -0,0 +1,4393 @@ +/* The MIT License + + Copyright (c) 2021-2024 Sergei Grechanik + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// This file implements a subset of the kitty graphics protocol. +// +//////////////////////////////////////////////////////////////////////////////// + +// A workaround for mac os to enable mkdtemp. +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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_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, + ERROR_MTIME_MISMATCH = 3, +}; + +const char *image_uploading_failure_strings[7] = { + "NO_ERROR", + "ERROR_OVER_SIZE_LIMIT", + "ERROR_CANNOT_OPEN_CACHED_FILE", + "ERROR_UNEXPECTED_SIZE", + "ERROR_CANNOT_COPY_FILE", + "ERROR_CANNOT_OPEN_SHM", + "ERROR_MTIME_MISMATCH", +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// 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 *) + +/// A transformation to apply to a pixmap before drawing it. +typedef struct PixmapTransformation { + /// The width and height of the pixmap. + int pixmap_w, pixmap_h; + /// The width and height of the transformed pixmap. + int dst_w, dst_h; + /// The offset relative to the top-left corner of the box of cells where + /// the transformed pixmap is drawn. + int dst_x, dst_y; +} PixmapTransformation; + +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; + /// Whether loading into ram is in progress. This is used to avoid + /// cyclic dependencies between frames. + char ram_loading_in_progress; + /// 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 original file name used with file transmission. Malloced. + char *original_filename; + /// The modification time of the original file used with file + /// transmission. + time_t original_file_mtime; + /// 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; + /// The transformation to apply to the pixmap before drawing it. + PixmapTransformation pixmap_transformation; + /// 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 void gr_try_restore_imagefile(ImageFrame *frame); +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; + +// Constants that are not important enough to expose in the config. + +/// The time after which an interrupted (with another command) direct +/// transmission cannot be resumed. +static Milliseconds graphics_direct_transmission_timeout_ms = 2000; + +//////////////////////////////////////////////////////////////////////////////// +// 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->pixmap_transformation.pixmap_w * + placement->pixmap_transformation.pixmap_h * 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); +} + +/// Closes the on-disk cache file of the frame `frame`. +static void gr_close_disk_cache_file(ImageFrame *frame) { + if (frame && frame->open_file) { + fclose(frame->open_file); + frame->open_file = NULL; + } +} + +/// 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. + gr_close_disk_cache_file(frame); + + 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); + if (frame->original_filename) + free(frame->original_filename); + }); + 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. +//////////////////////////////////////////////////////////////////////////////// + +/// Returns whether the original file of the frame is still available (exists, +/// is a regular file, has the same size and mtime). +static int gr_is_original_file_still_available(ImageFrame *frame) { + if (!frame->original_filename) + return 0; + struct stat st; + if (stat(frame->original_filename, &st) != 0) + return 0; + if (!S_ISREG(st.st_mode)) + return 0; + if (st.st_size == 0 || st.st_size > graphics_max_single_image_file_size) + return 0; + if (frame->expected_size && st.st_size != frame->expected_size) + return 0; + if (st.st_mtime != frame->original_file_mtime) + return 0; + return 1; +} + +/// 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) { + // In some cases the original image file may still be available, + // try to restore it. + gr_try_restore_imagefile(frame); + } + if (frame->disk_size == 0) { + if (frame->status != STATUS_RAM_LOADING_ERROR && + frame->status >= STATUS_UPLOADING_SUCCESS) { + 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->ram_loading_in_progress) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + 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->ram_loading_in_progress = 1; + + // 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) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + 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) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + 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; + } + } + + frame->ram_loading_in_progress = 0; + + // 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(); + + // Check that the size of the image we are loading does not exceed the + // limit. + if (frame_data_width * frame_data_height * 4 > + graphics_max_single_image_ram_size) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: image %u frame %u is too big too load: " + "%d x %d * 4 = %d > %u\n", + frame->image->image_id, frame->index, + frame_data_width, frame_data_height, + frame_data_width * frame_data_height * 4, + graphics_max_single_image_ram_size); + } + imlib_free_image(); + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + 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; + } + } +} + +/// Computes the pixmap transformation, which is essentially the destination +/// rectangle for drawing an image placement in a box of size `cols*cw` x +/// `rows*ch`. The destination rectangle depends on the parameters of the +/// placement, like scaling mode. The computed transformation is stored in the +/// placement. +void gr_compute_pixmap_transformation(ImagePlacement *placement) { + // Infer the placement size if needed. + gr_infer_placement_size_maybe(placement); + + // The size of the box in which the image is placed. + int box_w = (int)placement->cols * placement->scaled_cw; + int box_h = (int)placement->rows * placement->scaled_ch; + + 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 = box_w < src_w || box_h < src_h; + char mode = placement->scale_mode; + + PixmapTransformation *tr = &placement->pixmap_transformation; + + if (src_w <= 0 || src_h <= 0) { + tr->dst_x = tr->dst_y = tr->dst_w = tr->dst_h = 0; + } else if (mode == SCALE_MODE_FILL) { + tr->dst_x = tr->dst_y = 0; + tr->dst_w = box_w; + tr->dst_h = box_h; + } else if (mode == SCALE_MODE_NONE || + (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { + tr->dst_x = tr->dst_y = 0; + tr->dst_w = src_w; + tr->dst_h = 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); + } + if (box_w * src_h > src_w * box_h) { + // If the box is wider than the original image, fit to + // height. + tr->dst_h = box_h; + tr->dst_y = 0; + tr->dst_w = src_w * box_h / src_h; + tr->dst_x = (box_w - tr->dst_w) / 2; + } else { + // Otherwise, fit to width. + tr->dst_w = box_w; + tr->dst_x = 0; + tr->dst_h = src_h * box_w / src_w; + tr->dst_y = (box_h - tr->dst_h) / 2; + } + } + + // Make sure that the size of the destination image is non-zero. + tr->dst_w = MAX(1, tr->dst_w); + tr->dst_h = MAX(1, tr->dst_h); + + // Normally we want the pixmap to be exactly the size of the destination + // rectangle. + tr->pixmap_w = tr->dst_w; + tr->pixmap_h = tr->dst_h; + // However, if the pixmap would be larger than the source image, use the + // source image size. The upscaling will be done by XRender then. + if (tr->pixmap_w * tr->pixmap_h > src_w * src_h) { + tr->pixmap_w = MAX(1, src_w); + tr->pixmap_h = MAX(1, src_h); + } + + // If the pixmap would be over the limit, scale it down. + if (tr->pixmap_w * tr->pixmap_h * 4 > + graphics_max_single_image_ram_size) { + double scale = sqrt((double)graphics_max_single_image_ram_size / + (tr->pixmap_w * tr->pixmap_h * 4)); + tr->pixmap_w = MAX(1, (int)(tr->pixmap_w * scale)); + tr->pixmap_h = MAX(1, (int)(tr->pixmap_h * scale)); + } +} + +/// Creates and returns a scaled imlib image for the image placement. The +/// original imlib object must be loaded and the placement size must be inferred +/// by the caller (by calling `gr_compute_pixmap_transformation`). The caller is +/// responsible for freeing the returned image. +Imlib_Image gr_create_scaled_image_object(ImagePlacement *placement, + ImageFrame *frame) { + // The source rectangle (inside the original image). + 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; + // The pixmap dimensions. + int pixmap_w = placement->pixmap_transformation.pixmap_w; + int pixmap_h = placement->pixmap_transformation.pixmap_h; + + if (pixmap_w * pixmap_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", + placement->image->image_id, placement->placement_id, + pixmap_w, pixmap_h, graphics_max_single_image_ram_size); + return 0; + } + + if (pixmap_w == 0 || pixmap_h == 0) + fprintf(stderr, "warning: image of zero size\n"); + + imlib_context_set_image(frame->imlib_object); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + + return imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, + pixmap_w, pixmap_h); +} + +/// Creates a pixmap for the frame of an image placement. The pixmap contains +/// 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 unconditionally. + gr_touch_placement(placement); + if (frame) + gr_touch_frame(frame); + + // If cw or ch are different, unload all the pixmaps and recompute the + // pixmap geometry and transformation (shared for all pixmaps). + if (placement->scaled_cw != cw || placement->scaled_ch != ch) { + gr_unload_placement(placement); + placement->scaled_cw = cw; + placement->scaled_ch = ch; + gr_compute_pixmap_transformation(placement); + } + + // 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); + + if (placement->pixmap_transformation.pixmap_w == 0 || + placement->pixmap_transformation.pixmap_h == 0) { + GR_LOG("Not loading because the pixmap size is zero\n"); + return 0; + } + + // 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; + + // Create the scaled image. This is temporary, we will upload to the X + // server, and then delete immediately. + Imlib_Image scaled_image = + gr_create_scaled_image_object(placement, frame); + if (!scaled_image) + return 0; + imlib_context_set_image(scaled_image); + int pixmap_w = imlib_image_get_width(); + int pixmap_h = imlib_image_get_height(); + + // XRender needs the alpha channel premultiplied. + DATA32 *data = imlib_image_get_data(); + gr_premultiply_alpha(data, pixmap_w * pixmap_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, pixmap_w, pixmap_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, pixmap_w, pixmap_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, pixmap_w, pixmap_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->original_filename) { + fprintf_ind(file, ind, "original filename (sanitized): %s\n", + sanitized_filename(frame->original_filename)); + fprintf_ind(file, ind, "original file %s\n", + gr_is_original_file_still_available(frame) + ? "is still available" + : "is NOT available anymore"); + } + 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); + PixmapTransformation *tr = &placement->pixmap_transformation; + fprintf_ind(file, ind, "pixmap size: %ux%u\n", tr->pixmap_w, + tr->pixmap_h); + fprintf_ind(file, ind, "dst size: %ux%u offset: (%d, %d)\n", tr->dst_w, + tr->dst_h, tr->dst_x, tr->dst_y); + fprintf_ind(file, ind, "ram per frame: %u KiB\n", + gr_placement_single_frame_ram_size(placement) / 1024); + unsigned ram_size = gr_placement_current_ram_size(placement); + fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); +} + +/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) + return; + int frameidx = 1; + foreach_pixmap(*placement, pixmap, { + fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, + pixmap); + ++frameidx; + }); +} + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state() { + FILE *file = stderr; + int ind = 0; + fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); + fprintf_ind(file, ind, + "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " + "sizeof(ImagePlacement) = %lu\n", + sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); + fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); + fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); + fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", + images_ram_size / 1024); + fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", + images_disk_size / 1024); + + Milliseconds now = gr_now_ms(); + + int64_t images_ram_size_computed = 0; + int64_t images_disk_size_computed = 0; + + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + fprintf_ind(file, ind, "----------------\n"); + gr_dump_image_info(file, img, 0); + int64_t total_disk_size_computed = 0; + int total_duration_computed = 0; + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + if (frame->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + total_duration_computed += frame->gap; + images_disk_size_computed += frame->disk_size; + total_disk_size_computed += frame->disk_size; + if (frame->imlib_object) + images_ram_size_computed += + gr_frame_current_ram_size(frame); + }); + if (img->total_disk_size != total_disk_size_computed) { + fprintf_ind(file, ind, + " ERROR: total_disk_size is %u, but " + "computed value is %ld\n", + img->total_disk_size, total_disk_size_computed); + } + if (img->total_duration != total_duration_computed) { + fprintf_ind(file, ind, + " ERROR: total_duration is %d, but computed " + "value is %d\n", + img->total_duration, total_duration_computed); + } + kh_foreach_value(img->placements, placement, { + gr_dump_placement_info(file, placement, 4); + if (placement->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + fprintf_ind(file, 8, + "Pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 12); + unsigned ram_size = + gr_placement_current_ram_size(placement); + images_ram_size_computed += ram_size; + }); + }); + if (images_ram_size != images_ram_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_ram_size is %ld, but computed value " + "is %ld\n", + images_ram_size, images_ram_size_computed); + } + if (images_disk_size != images_disk_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_disk_size is %ld, but computed value " + "is %ld\n", + images_disk_size, images_disk_size_computed); + } + fprintf_ind(file, ind, "===========================================\n"); +} + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +// TODO: Currently we do this for the first frame only. Not sure what to do with +// animations. +void gr_preview_image(uint32_t image_id, const char *exec) { + char command[256]; + size_t len; + Image *img = gr_find_image(image_id); + if (img) { + ImageFrame *frame = &img->first_frame; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (frame->disk_size == 0) { + len = snprintf(command, 255, + "xmessage 'Image with id=%u is not " + "fully copied to %s'", + image_id, sanitized_filename(filename)); + } else { + len = snprintf(command, 255, "%s %s &", exec, + sanitized_filename(filename)); + } + } else { + len = snprintf(command, 255, + "xmessage 'Cannot find image with id=%u'", + image_id); + } + if (len > 255) { + fprintf(stderr, "error: command too long: %s\n", command); + snprintf(command, 255, "xmessage 'error: command too long'"); + } + if (system(command) != 0) { + fprintf(stderr, "error: could not execute command %s\n", + command); + } +} + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable) { + char filename[MAX_FILENAME_SIZE]; + snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); + FILE *file = fopen(filename, "w"); + if (!file) { + perror("fopen"); + return; + } + // Basic information about the cell. + fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); + fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); + fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); + fprintf(file, "classic/unicode placeholder = %s\n", + is_classic_placeholder ? "classic" : "unicode"); + fprintf(file, "original diacritic count = %d\n", diacritic_count); + // Information about the image and the placement. + Image *img = gr_find_image(image_id); + ImagePlacement *placement = gr_find_placement(img, placement_id); + gr_dump_image_info(file, img, 0); + gr_dump_placement_info(file, placement, 0); + // 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; + } + + // The coordinates and size (in pixels) of the src rectangle inside the + // box of cells (not the pixmap). + int src_x = rect->img_start_col * rect->cw; + int src_y = rect->img_start_row * rect->ch; + int src_w = (rect->img_end_col - rect->img_start_col) * rect->cw; + int src_h = (rect->img_end_row - rect->img_start_row) * rect->ch; + // The coordinates of the dst rectangle inside the window. The size is + // the same as the src size in the box (src_w, src_h). + int window_x = rect->screen_x_pix; + int window_y = rect->screen_y_pix; + + 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 = placement->pixmap_transformation.pixmap_w; + unsigned pixmap_h = placement->pixmap_transformation.pixmap_h; + 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); + // We use RepeatPad to avoid bilinear filtering halo. + XRenderPictureAttributes attrs = {0}; + attrs.repeat = RepeatPad; + Picture pixmap_pic = XRenderCreatePicture(disp, pixmap, pic_format, + CPRepeat, &attrs); + + // Since the pixmap may be of different size than the destination box of + // cells, we must apply a transformation to it. + // The XTransform structure describes a matrix to transform the + // destination picture coordinates (i.e. in the box) to the source + // coordinates (i.e. in the pixmap). We apply only scaling (translation + // will be applied to the src coordinates directly): + // + // pixmap_x = picture_x * pixmap_w / dst_w + // pixmap_y = picture_y * pixmap_h / dst_h + // + // Where dst_w, dst_h, pixmap_w, pixmap_h are from the placement's + // pixmap_transformation structure. + PixmapTransformation *tr = &placement->pixmap_transformation; + double xs = (double)tr->pixmap_w / MAX(tr->dst_w, 1); + double ys = (double)tr->pixmap_h / MAX(tr->dst_h, 1); + // clang-format off + XTransform xform = {{ + { XDoubleToFixed(xs), XDoubleToFixed( 0), XDoubleToFixed( 0) }, + { XDoubleToFixed( 0), XDoubleToFixed(ys), XDoubleToFixed( 0) }, + { XDoubleToFixed( 0), XDoubleToFixed( 0), XDoubleToFixed( 1) } + }}; + // clang-format on + XRenderSetPictureTransform(disp, pixmap_pic, &xform); + XRenderSetPictureFilter(disp, pixmap_pic, FilterBilinear, NULL, 0); + + // Do the translation: modify the src coordinates to make them + // coordinates into the picture rather than into the box. + src_x -= tr->dst_x; + src_y -= tr->dst_y; + // At this point src_x, src_y, src_w, src_h are coordinates of the + // source rectangle inside the picture (scaled pixmap). + + // Now do clipping. If src coordinates are negative, adjust the dst + // coordinates (into the window) and the width/height. We do clipping + // instead of using the values as is to avoid rendering the pad area + // outside the pixmap. + if (src_x < 0) { + window_x += -src_x; + src_w -= -src_x; + src_x = 0; + } + if (src_y < 0) { + window_y += -src_y; + src_h -= -src_y; + src_y = 0; + } + + // Adjust width and height if the src rectangle exceeds the picture. + src_w = MIN(src_w, tr->dst_w - src_x); + src_h = MIN(src_h, tr->dst_h - src_y); + + // 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; + if (src_w > 0 && src_h > 0) + XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, src_x, + src_y, src_x, src_y, window_x, window_y, src_w, + src_h); + + // 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); +} + +/// Closes the file currently being uploaded. This doesn't necessarily finish +/// the upload since the file may be reopened. +static void gr_close_current_upload_file() { + Image *img = gr_find_image(current_upload_image_id); + ImageFrame *frame = gr_get_frame(img, current_upload_frame_index); + gr_close_disk_cache_file(frame); +} + +/// Sets the current image and frame being uploaded. Closes the previous upload +/// file if it's changed. If `frame` is NULL, clears the current upload +/// image/frame. +static void gr_set_current_upload_frame(ImageFrame *frame) { + if (frame) { + if (current_upload_image_id != frame->image->image_id || + current_upload_frame_index != frame->index) { + gr_close_current_upload_file(); + } + current_upload_image_id = frame->image->image_id; + current_upload_frame_index = frame->index; + GR_LOG("Set current_upload_image_id = %u, " + "current_upload_frame_index = %u\n", + current_upload_image_id, current_upload_frame_index); + } else { + gr_close_current_upload_file(); + current_upload_image_id = 0; + current_upload_frame_index = 0; + GR_LOG("Set current_upload_image_id = 0\n"); + } +} + +/// Returns whether direct transmission continuation is allowed for the given +/// command and frame. +static int gr_transmission_continuation_is_allowed(GraphicsCommand *cmd, + ImageFrame *frame) { + if (!frame || frame->status != STATUS_UPLOADING) + return 0; + + // If it's the same image and frame as the current upload, allow it. + if (current_upload_image_id == frame->image->image_id && + current_upload_frame_index == frame->index) + return 1; + + // Otherwise it's a continuation of an interrupted upload. The kitty + // graphics protocol doesn't allow interleaving of direct transmission + // with other commands, so interrupted uploads must be aborted. However, + // we still allow it as an extension, because it's useful for + // protocol-unaware multiplexer. We check that there are no + // contradictions, and the time since the last upload activity is small. + + if (cmd->size && cmd->size != frame->expected_size) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of expected size mismatch\n"); + return 0; + } + if (cmd->format && cmd->format != frame->format) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of format mismatch\n"); + return 0; + } + if (cmd->compression && cmd->compression != frame->compression) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of compression mismatch\n"); + return 0; + } + if ((cmd->frame_pix_width && + cmd->frame_pix_width != frame->data_pix_width) || + (cmd->frame_pix_height && + cmd->frame_pix_height != frame->data_pix_height) || + (cmd->background_color && + cmd->background_color != frame->background_color) || + (cmd->background_frame && + cmd->background_frame != frame->background_frame_index) || + (cmd->gap && cmd->gap != frame->gap) || + (cmd->replace_instead_of_blending && + cmd->replace_instead_of_blending != !frame->blend)) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of frame parameters mismatch\n"); + return 0; + } + + Milliseconds now = gr_now_ms(); + if (now - frame->atime > graphics_direct_transmission_timeout_ms) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of time out\n"); + return 0; + } + + return 1; +} + +/// 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) { + gr_set_current_upload_frame(frame); + + if (frame->status != STATUS_UPLOADING) { + if (!more) + gr_reportuploaderror(frame); + gr_set_current_upload_frame(NULL); + 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); + gr_set_current_upload_frame(NULL); + 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); + gr_set_current_upload_frame(NULL); + return; + } + free(data); + + if (!more) { + gr_set_current_upload_frame(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); +} + +/// Copy the image file `frame->original_filename` to the cache directory. This +/// is done when the image is transmitted via file transfer, or when we have +/// evicted the image from the disk cache and need to restore it. +/// If `cmd` is not NULL, it's used to report errors, otherwise errors are only +/// printed to stderr. +static void gr_copy_imagefile(ImageFrame *frame, GraphicsCommand *cmd) { + GR_LOG("Copying image %s\n", + sanitized_filename(frame->original_filename)); + // Stat the file and check that it's a regular file and not too big. + struct stat st; + int stat_res = stat(frame->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) { + fprintf(stderr, "Could not load the file %s: %s\n", + sanitized_filename(frame->original_filename), + stat_error); + if (cmd) + gr_reporterror_cmd(cmd, "EBADF: %s", stat_error); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + return; + } + + // Check the expected size if specified. + if (frame->expected_size && frame->expected_size != st.st_size) { + fprintf(stderr, + "Could not load, the size doesn't match: %s expected " + "%u vs actual %ld\n", + sanitized_filename(frame->original_filename), + frame->expected_size, st.st_size); + // The file has unexpected size. + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + if (cmd) + gr_reportuploaderror(frame); + return; + } + + // If we know the original modification time, we are trying to restore + // the evicted image file. Check that the modification time matches. + if (frame->original_file_mtime && + frame->original_file_mtime != st.st_mtime) { + fprintf(stderr, "Could not load, the mtime doesn't match: %s\n", + sanitized_filename(frame->original_filename)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_MTIME_MISMATCH; + if (cmd) + gr_reportuploaderror(frame); + return; + } + + frame->original_file_mtime = st.st_mtime; + + 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(frame->original_filename, tmp_filename_symlink) || + system(command) != 0) { + fprintf(stderr, + "Could not copy the image " + "%s (symlink %s) to %s", + sanitized_filename(frame->original_filename), + tmp_filename_symlink, cache_filename); + if (cmd) + gr_reporterror_cmd(cmd, "EBADF: could not copy the " + "image to the cache dir"); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + // Delete the symlink. + unlink(tmp_filename_symlink); + return; + } + + // Delete the symlink. + unlink(tmp_filename_symlink); + // Set the status and update disk size variables. + 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; +} + +/// Tries to restore the image file for `frame` if the original file is still +/// available. +static void gr_try_restore_imagefile(ImageFrame *frame) { + if (frame->disk_size != 0) + return; + if (gr_is_original_file_still_available(frame)) + gr_copy_imagefile(frame, NULL); +} + +/// 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. + frame->original_filename = gr_base64dec(cmd->payload, NULL); + // Copy the file to the cache directory. + gr_copy_imagefile(frame, cmd); + if (frame->status == STATUS_UPLOADING_SUCCESS) { + // Everything seems fine, try to load and redraw + // existing instances. + gr_schedule_image_redraw(frame->image); + frame = gr_loadimage_and_report(frame); + } + // Delete the original file if it's temporary. + if (cmd->transmission_medium == 't') + gr_delete_tmp_file(frame->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 (gr_transmission_continuation_is_allowed(cmd, frame)) { + // 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. + gr_close_disk_cache_file(frame); + // 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; + } + + int was_transmission = 0; + ImageFrame *frame = NULL; + + switch (cmd->action) { + case 0: + // If no action is specified, it is data transmission. + case 't': + case 'q': + case 'f': + was_transmission = 1; + // 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': + was_transmission = 1; + // 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); + break; + } + + if (!was_transmission || + (cmd->transmission_medium && cmd->transmission_medium != 'd')) { + // If it wasn't a transmission command, or if the transmission + // wasn't direct, clear the current upload frame and close the + // file. (If it was a direct transmission, the current upload + // was handled inside `gr_append_data`.) + gr_set_current_upload_frame(NULL); + } +} + +/// 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 +#include +#include + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm); +/// Deinitialize the graphics module. +void gr_deinit(); + +/// Add an image rectangle to a list if rectangles to draw. This function may +/// actually draw some rectangles, or it may wait till more rectangles are +/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. +/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the +/// part of the image to draw (row/col indices are zero-based, ends are +/// excluded). +/// - `x_col` and `y_row` are the coordinates of the top-left corner of the +/// image in the terminal grid. +/// - `x_pix` and `y_pix` are the same but in pixels. +/// - `reverse` indicates whether colors should be inverted. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse); +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch); +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf); +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows); + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters (including '\0'). Returns 1 on success. +/// Additional informations is returned through `graphics_command_result`. +int gr_parse_command(char *buf, size_t len); + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +void gr_preview_image(uint32_t image_id, const char *command); + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable); + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state(); + +/// Unloads images to reduce RAM usage. +void gr_unload_images_to_reduce_ram(); + +/// Executes `callback` for each image cell. 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; diff --git a/graphics.o b/graphics.o new file mode 100644 index 0000000000000000000000000000000000000000..334f04e4df398f9ea98115460a0e1a4541f58d3b GIT binary patch literal 94904 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 literal 0 HcmV?d00001 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] + +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;;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 +# Where is a part of command that specifies the action, it will be +# repeated for every chunk (if the method is direct), and is the rest +# of the command that specifies the image parameters. and +# must not include the transmission method or ';'. +# Example: +# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" +gr_upload() { + arg_action="$1" + arg_command="$2" + arg_file="$3" + if [ "$uploading_method" = "file" ]; then + # base64-encode the filename + encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) + # 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 diff --git a/khash.h b/khash.h new file mode 100644 index 0000000..f75f347 --- /dev/null +++ b/khash.h @@ -0,0 +1,627 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include +#include +#include + +/* compiler specific configuration */ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash set containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ diff --git a/kvec.h b/kvec.h new file mode 100644 index 0000000..10f1c5b --- /dev/null +++ b/kvec.h @@ -0,0 +1,90 @@ +/* The MIT License + + Copyright (c) 2008, by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "kvec.h" +int main() { + kvec_t(int) array; + kv_init(array); + kv_push(int, array, 10); // append + kv_a(int, array, 20) = 5; // dynamic + kv_A(array, 20) = 4; // static + kv_destroy(array); + return 0; +} +*/ + +/* + 2008-09-22 (0.1.0): + + * The initial version. + +*/ + +#ifndef AC_KVEC_H +#define AC_KVEC_H + +#include + +#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) + +#define kvec_t(type) struct { size_t n, m; type *a; } +#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) +#define kv_destroy(v) free((v).a) +#define kv_A(v, i) ((v).a[(i)]) +#define kv_pop(v) ((v).a[--(v).n]) +#define kv_size(v) ((v).n) +#define kv_max(v) ((v).m) + +#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) + +#define kv_copy(type, v1, v0) do { \ + if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ + (v1).n = (v0).n; \ + memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ + } while (0) \ + +#define kv_push(type, v, x) do { \ + if ((v).n == (v).m) { \ + (v).m = (v).m? (v).m<<1 : 2; \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ + } \ + (v).a[(v).n++] = (x); \ + } while (0) + +#define kv_pushp(type, v) ((((v).n == (v).m)? \ + ((v).m = ((v).m? (v).m<<1 : 2), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : 0), ((v).a + ((v).n++))) + +#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ + ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ + : 0), (v).a[(i)]) + +#endif diff --git a/patches/st-alpha-20240814-a0274bc.diff b/patches/st-alpha-20240814-a0274bc.diff new file mode 100644 index 0000000..6913d19 --- /dev/null +++ b/patches/st-alpha-20240814-a0274bc.diff @@ -0,0 +1,129 @@ +diff --git a/config.def.h b/config.def.h +index 2cd740a..019a4e1 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -93,6 +93,9 @@ char *termname = "st-256color"; + */ + unsigned int tabspaces = 8; + ++/* bg opacity */ ++float alpha = 0.8; ++ + /* Terminal colors (16 first used in escape sequence) */ + static const char *colorname[] = { + /* 8 normal colors */ +diff --git a/x.c b/x.c +index d73152b..f32fd6c 100644 +--- a/x.c ++++ b/x.c +@@ -105,6 +105,7 @@ typedef struct { + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ ++ int depth; /* bit depth */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ + } XWindow; +@@ -752,7 +753,7 @@ xresize(int col, int row) + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.depth); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + +@@ -812,6 +813,10 @@ xloadcols(void) + else + die("could not allocate color %d\n", i); + } ++ ++ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); ++ dc.col[defaultbg].pixel &= 0x00FFFFFF; ++ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; + loaded = 1; + } + +@@ -842,6 +847,12 @@ xsetcolorname(int x, const char *name) + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + ++ if (x == defaultbg) { ++ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); ++ dc.col[defaultbg].pixel &= 0x00FFFFFF; ++ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; ++ } ++ + return 0; + } + +@@ -1134,11 +1145,25 @@ xinit(int cols, int rows) + Window parent, root; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; ++ XWindowAttributes attr; ++ XVisualInfo vis; + + if (!(xw.dpy = XOpenDisplay(NULL))) + die("can't open display\n"); + xw.scr = XDefaultScreen(xw.dpy); +- xw.vis = XDefaultVisual(xw.dpy, xw.scr); ++ ++ root = XRootWindow(xw.dpy, xw.scr); ++ if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) ++ parent = root; ++ ++ if (XMatchVisualInfo(xw.dpy, xw.scr, 32, TrueColor, &vis) != 0) { ++ xw.vis = vis.visual; ++ xw.depth = vis.depth; ++ } else { ++ XGetWindowAttributes(xw.dpy, parent, &attr); ++ xw.vis = attr.visual; ++ xw.depth = attr.depth; ++ } + + /* font */ + if (!FcInit()) +@@ -1148,7 +1173,7 @@ xinit(int cols, int rows) + xloadfonts(usedfont, 0); + + /* colors */ +- xw.cmap = XDefaultColormap(xw.dpy, xw.scr); ++ xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None); + xloadcols(); + + /* adjust fixed window geometry */ +@@ -1168,11 +1193,8 @@ xinit(int cols, int rows) + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + +- root = XRootWindow(xw.dpy, xw.scr); +- if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) +- parent = root; +- xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, +- win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, ++ xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t, ++ win.w, win.h, 0, xw.depth, InputOutput, + xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &xw.attrs); + if (parent != root) +@@ -1183,7 +1205,7 @@ xinit(int cols, int rows) + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.depth); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + +@@ -2047,6 +2069,10 @@ main(int argc, char *argv[]) + case 'a': + allowaltscreen = 0; + break; ++ case 'A': ++ alpha = strtof(EARGF(usage()), NULL); ++ LIMIT(alpha, 0.0, 1.0); ++ break; + case 'c': + opt_class = EARGF(usage()); + break; diff --git a/patches/st-kitty-graphics-20251230-0.9.3.diff b/patches/st-kitty-graphics-20251230-0.9.3.diff new file mode 100644 index 0000000..d53bb30 --- /dev/null +++ b/patches/st-kitty-graphics-20251230-0.9.3.diff @@ -0,0 +1,8033 @@ +From 16663a03b6d7522d171b9717151040f9fad67f9f Mon Sep 17 00:00:00 2001 +From: Sergei Grechanik +Date: Sat, 10 Jan 2026 16:09:05 -0800 +Subject: [PATCH] Kitty graphics protocol support eb8d6cb 2025-12-30 + +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 eb8d6cbc964a864e6391c4288aebe4ebe74bb5c7 (2025-12-30). + +Squashed on top of 6e970474743d57a5d8b054c41fd3bff2bc895742 (0.9.3) + +Note that the following files were excluded from the squash: + .clang-format + README.md + generate-rowcolumn-helpers.py + rowcolumn-diacritics.txt + rowcolumn_diacritics.sh + +Main changes since 2025-02-22: + * Restore evicted images from original files when available. + * Fix an issue that caused too many files to be opened during + concurrent direct uploads. + * Optimize scaled pixmap generation (don't store pad pixels; upscale + with XRender). + +Main changes since 2024-09-22: + * Restore text under classic placements on deletion. + * Support uploading via shared memory (t=s). + * Bug fixes. +--- + Makefile | 7 +- + config.def.h | 46 +- + config.mk | 5 +- + graphics.c | 4393 ++++++++++++++++++++++++++++++++ + graphics.h | 112 + + icat-mini.sh | 875 +++++++ + khash.h | 627 +++++ + kvec.h | 90 + + rowcolumn_diacritics_helpers.c | 391 +++ + st.c | 303 ++- + st.h | 84 +- + st.info | 6 + + win.h | 3 + + x.c | 411 ++- + 14 files changed, 7295 insertions(+), 58 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..413e7ab 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 +@@ -15,8 +15,9 @@ 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 ++st.o: config.h st.h win.h graphics.h ++x.o: arg.h config.h st.h win.h graphics.h ++graphics.c: graphics.h khash.h kvec.h st.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 2fc854e..7b22243 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..801f5c9 +--- /dev/null ++++ b/graphics.c +@@ -0,0 +1,4393 @@ ++/* The MIT License ++ ++ Copyright (c) 2021-2024 Sergei Grechanik ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// This file implements a subset of the kitty graphics protocol. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++// A workaround for mac os to enable mkdtemp. ++#ifdef __APPLE__ ++#define _DARWIN_C_SOURCE ++#endif ++ ++#define _POSIX_C_SOURCE 200809L ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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_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, ++ ERROR_MTIME_MISMATCH = 3, ++}; ++ ++const char *image_uploading_failure_strings[7] = { ++ "NO_ERROR", ++ "ERROR_OVER_SIZE_LIMIT", ++ "ERROR_CANNOT_OPEN_CACHED_FILE", ++ "ERROR_UNEXPECTED_SIZE", ++ "ERROR_CANNOT_COPY_FILE", ++ "ERROR_CANNOT_OPEN_SHM", ++ "ERROR_MTIME_MISMATCH", ++}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// 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 *) ++ ++/// A transformation to apply to a pixmap before drawing it. ++typedef struct PixmapTransformation { ++ /// The width and height of the pixmap. ++ int pixmap_w, pixmap_h; ++ /// The width and height of the transformed pixmap. ++ int dst_w, dst_h; ++ /// The offset relative to the top-left corner of the box of cells where ++ /// the transformed pixmap is drawn. ++ int dst_x, dst_y; ++} PixmapTransformation; ++ ++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; ++ /// Whether loading into ram is in progress. This is used to avoid ++ /// cyclic dependencies between frames. ++ char ram_loading_in_progress; ++ /// 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 original file name used with file transmission. Malloced. ++ char *original_filename; ++ /// The modification time of the original file used with file ++ /// transmission. ++ time_t original_file_mtime; ++ /// 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; ++ /// The transformation to apply to the pixmap before drawing it. ++ PixmapTransformation pixmap_transformation; ++ /// 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 void gr_try_restore_imagefile(ImageFrame *frame); ++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; ++ ++// Constants that are not important enough to expose in the config. ++ ++/// The time after which an interrupted (with another command) direct ++/// transmission cannot be resumed. ++static Milliseconds graphics_direct_transmission_timeout_ms = 2000; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// 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->pixmap_transformation.pixmap_w * ++ placement->pixmap_transformation.pixmap_h * 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); ++} ++ ++/// Closes the on-disk cache file of the frame `frame`. ++static void gr_close_disk_cache_file(ImageFrame *frame) { ++ if (frame && frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++} ++ ++/// 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. ++ gr_close_disk_cache_file(frame); ++ ++ 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); ++ if (frame->original_filename) ++ free(frame->original_filename); ++ }); ++ 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. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns whether the original file of the frame is still available (exists, ++/// is a regular file, has the same size and mtime). ++static int gr_is_original_file_still_available(ImageFrame *frame) { ++ if (!frame->original_filename) ++ return 0; ++ struct stat st; ++ if (stat(frame->original_filename, &st) != 0) ++ return 0; ++ if (!S_ISREG(st.st_mode)) ++ return 0; ++ if (st.st_size == 0 || st.st_size > graphics_max_single_image_file_size) ++ return 0; ++ if (frame->expected_size && st.st_size != frame->expected_size) ++ return 0; ++ if (st.st_mtime != frame->original_file_mtime) ++ return 0; ++ return 1; ++} ++ ++/// 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) { ++ // In some cases the original image file may still be available, ++ // try to restore it. ++ gr_try_restore_imagefile(frame); ++ } ++ if (frame->disk_size == 0) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR && ++ frame->status >= STATUS_UPLOADING_SUCCESS) { ++ 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->ram_loading_in_progress) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ 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->ram_loading_in_progress = 1; ++ ++ // 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) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ 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) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ 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; ++ } ++ } ++ ++ frame->ram_loading_in_progress = 0; ++ ++ // 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(); ++ ++ // Check that the size of the image we are loading does not exceed the ++ // limit. ++ if (frame_data_width * frame_data_height * 4 > ++ graphics_max_single_image_ram_size) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: image %u frame %u is too big too load: " ++ "%d x %d * 4 = %d > %u\n", ++ frame->image->image_id, frame->index, ++ frame_data_width, frame_data_height, ++ frame_data_width * frame_data_height * 4, ++ graphics_max_single_image_ram_size); ++ } ++ imlib_free_image(); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ 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; ++ } ++ } ++} ++ ++/// Computes the pixmap transformation, which is essentially the destination ++/// rectangle for drawing an image placement in a box of size `cols*cw` x ++/// `rows*ch`. The destination rectangle depends on the parameters of the ++/// placement, like scaling mode. The computed transformation is stored in the ++/// placement. ++void gr_compute_pixmap_transformation(ImagePlacement *placement) { ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ ++ // The size of the box in which the image is placed. ++ int box_w = (int)placement->cols * placement->scaled_cw; ++ int box_h = (int)placement->rows * placement->scaled_ch; ++ ++ 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 = box_w < src_w || box_h < src_h; ++ char mode = placement->scale_mode; ++ ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ ++ if (src_w <= 0 || src_h <= 0) { ++ tr->dst_x = tr->dst_y = tr->dst_w = tr->dst_h = 0; ++ } else if (mode == SCALE_MODE_FILL) { ++ tr->dst_x = tr->dst_y = 0; ++ tr->dst_w = box_w; ++ tr->dst_h = box_h; ++ } else if (mode == SCALE_MODE_NONE || ++ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { ++ tr->dst_x = tr->dst_y = 0; ++ tr->dst_w = src_w; ++ tr->dst_h = 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); ++ } ++ if (box_w * src_h > src_w * box_h) { ++ // If the box is wider than the original image, fit to ++ // height. ++ tr->dst_h = box_h; ++ tr->dst_y = 0; ++ tr->dst_w = src_w * box_h / src_h; ++ tr->dst_x = (box_w - tr->dst_w) / 2; ++ } else { ++ // Otherwise, fit to width. ++ tr->dst_w = box_w; ++ tr->dst_x = 0; ++ tr->dst_h = src_h * box_w / src_w; ++ tr->dst_y = (box_h - tr->dst_h) / 2; ++ } ++ } ++ ++ // Make sure that the size of the destination image is non-zero. ++ tr->dst_w = MAX(1, tr->dst_w); ++ tr->dst_h = MAX(1, tr->dst_h); ++ ++ // Normally we want the pixmap to be exactly the size of the destination ++ // rectangle. ++ tr->pixmap_w = tr->dst_w; ++ tr->pixmap_h = tr->dst_h; ++ // However, if the pixmap would be larger than the source image, use the ++ // source image size. The upscaling will be done by XRender then. ++ if (tr->pixmap_w * tr->pixmap_h > src_w * src_h) { ++ tr->pixmap_w = MAX(1, src_w); ++ tr->pixmap_h = MAX(1, src_h); ++ } ++ ++ // If the pixmap would be over the limit, scale it down. ++ if (tr->pixmap_w * tr->pixmap_h * 4 > ++ graphics_max_single_image_ram_size) { ++ double scale = sqrt((double)graphics_max_single_image_ram_size / ++ (tr->pixmap_w * tr->pixmap_h * 4)); ++ tr->pixmap_w = MAX(1, (int)(tr->pixmap_w * scale)); ++ tr->pixmap_h = MAX(1, (int)(tr->pixmap_h * scale)); ++ } ++} ++ ++/// Creates and returns a scaled imlib image for the image placement. The ++/// original imlib object must be loaded and the placement size must be inferred ++/// by the caller (by calling `gr_compute_pixmap_transformation`). The caller is ++/// responsible for freeing the returned image. ++Imlib_Image gr_create_scaled_image_object(ImagePlacement *placement, ++ ImageFrame *frame) { ++ // The source rectangle (inside the original image). ++ 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; ++ // The pixmap dimensions. ++ int pixmap_w = placement->pixmap_transformation.pixmap_w; ++ int pixmap_h = placement->pixmap_transformation.pixmap_h; ++ ++ if (pixmap_w * pixmap_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", ++ placement->image->image_id, placement->placement_id, ++ pixmap_w, pixmap_h, graphics_max_single_image_ram_size); ++ return 0; ++ } ++ ++ if (pixmap_w == 0 || pixmap_h == 0) ++ fprintf(stderr, "warning: image of zero size\n"); ++ ++ imlib_context_set_image(frame->imlib_object); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ ++ return imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, ++ pixmap_w, pixmap_h); ++} ++ ++/// Creates a pixmap for the frame of an image placement. The pixmap contains ++/// 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 unconditionally. ++ gr_touch_placement(placement); ++ if (frame) ++ gr_touch_frame(frame); ++ ++ // If cw or ch are different, unload all the pixmaps and recompute the ++ // pixmap geometry and transformation (shared for all pixmaps). ++ if (placement->scaled_cw != cw || placement->scaled_ch != ch) { ++ gr_unload_placement(placement); ++ placement->scaled_cw = cw; ++ placement->scaled_ch = ch; ++ gr_compute_pixmap_transformation(placement); ++ } ++ ++ // 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); ++ ++ if (placement->pixmap_transformation.pixmap_w == 0 || ++ placement->pixmap_transformation.pixmap_h == 0) { ++ GR_LOG("Not loading because the pixmap size is zero\n"); ++ return 0; ++ } ++ ++ // 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; ++ ++ // Create the scaled image. This is temporary, we will upload to the X ++ // server, and then delete immediately. ++ Imlib_Image scaled_image = ++ gr_create_scaled_image_object(placement, frame); ++ if (!scaled_image) ++ return 0; ++ imlib_context_set_image(scaled_image); ++ int pixmap_w = imlib_image_get_width(); ++ int pixmap_h = imlib_image_get_height(); ++ ++ // XRender needs the alpha channel premultiplied. ++ DATA32 *data = imlib_image_get_data(); ++ gr_premultiply_alpha(data, pixmap_w * pixmap_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, pixmap_w, pixmap_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, pixmap_w, pixmap_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, pixmap_w, pixmap_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->original_filename) { ++ fprintf_ind(file, ind, "original filename (sanitized): %s\n", ++ sanitized_filename(frame->original_filename)); ++ fprintf_ind(file, ind, "original file %s\n", ++ gr_is_original_file_still_available(frame) ++ ? "is still available" ++ : "is NOT available anymore"); ++ } ++ 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); ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ fprintf_ind(file, ind, "pixmap size: %ux%u\n", tr->pixmap_w, ++ tr->pixmap_h); ++ fprintf_ind(file, ind, "dst size: %ux%u offset: (%d, %d)\n", tr->dst_w, ++ tr->dst_h, tr->dst_x, tr->dst_y); ++ fprintf_ind(file, ind, "ram per frame: %u KiB\n", ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ unsigned ram_size = gr_placement_current_ram_size(placement); ++ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); ++} ++ ++/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) ++ return; ++ int frameidx = 1; ++ foreach_pixmap(*placement, pixmap, { ++ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, ++ pixmap); ++ ++frameidx; ++ }); ++} ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state() { ++ FILE *file = stderr; ++ int ind = 0; ++ fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); ++ fprintf_ind(file, ind, ++ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " ++ "sizeof(ImagePlacement) = %lu\n", ++ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); ++ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); ++ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); ++ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", ++ images_ram_size / 1024); ++ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", ++ images_disk_size / 1024); ++ ++ Milliseconds now = gr_now_ms(); ++ ++ int64_t images_ram_size_computed = 0; ++ int64_t images_disk_size_computed = 0; ++ ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ fprintf_ind(file, ind, "----------------\n"); ++ gr_dump_image_info(file, img, 0); ++ int64_t total_disk_size_computed = 0; ++ int total_duration_computed = 0; ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ if (frame->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ total_duration_computed += frame->gap; ++ images_disk_size_computed += frame->disk_size; ++ total_disk_size_computed += frame->disk_size; ++ if (frame->imlib_object) ++ images_ram_size_computed += ++ gr_frame_current_ram_size(frame); ++ }); ++ if (img->total_disk_size != total_disk_size_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_disk_size is %u, but " ++ "computed value is %ld\n", ++ img->total_disk_size, total_disk_size_computed); ++ } ++ if (img->total_duration != total_duration_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_duration is %d, but computed " ++ "value is %d\n", ++ img->total_duration, total_duration_computed); ++ } ++ kh_foreach_value(img->placements, placement, { ++ gr_dump_placement_info(file, placement, 4); ++ if (placement->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ fprintf_ind(file, 8, ++ "Pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 12); ++ unsigned ram_size = ++ gr_placement_current_ram_size(placement); ++ images_ram_size_computed += ram_size; ++ }); ++ }); ++ if (images_ram_size != images_ram_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_ram_size is %ld, but computed value " ++ "is %ld\n", ++ images_ram_size, images_ram_size_computed); ++ } ++ if (images_disk_size != images_disk_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_disk_size is %ld, but computed value " ++ "is %ld\n", ++ images_disk_size, images_disk_size_computed); ++ } ++ fprintf_ind(file, ind, "===========================================\n"); ++} ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++// TODO: Currently we do this for the first frame only. Not sure what to do with ++// animations. ++void gr_preview_image(uint32_t image_id, const char *exec) { ++ char command[256]; ++ size_t len; ++ Image *img = gr_find_image(image_id); ++ if (img) { ++ ImageFrame *frame = &img->first_frame; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (frame->disk_size == 0) { ++ len = snprintf(command, 255, ++ "xmessage 'Image with id=%u is not " ++ "fully copied to %s'", ++ image_id, sanitized_filename(filename)); ++ } else { ++ len = snprintf(command, 255, "%s %s &", exec, ++ sanitized_filename(filename)); ++ } ++ } else { ++ len = snprintf(command, 255, ++ "xmessage 'Cannot find image with id=%u'", ++ image_id); ++ } ++ if (len > 255) { ++ fprintf(stderr, "error: command too long: %s\n", command); ++ snprintf(command, 255, "xmessage 'error: command too long'"); ++ } ++ if (system(command) != 0) { ++ fprintf(stderr, "error: could not execute command %s\n", ++ command); ++ } ++} ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable) { ++ char filename[MAX_FILENAME_SIZE]; ++ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); ++ FILE *file = fopen(filename, "w"); ++ if (!file) { ++ perror("fopen"); ++ return; ++ } ++ // Basic information about the cell. ++ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); ++ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); ++ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); ++ fprintf(file, "classic/unicode placeholder = %s\n", ++ is_classic_placeholder ? "classic" : "unicode"); ++ fprintf(file, "original diacritic count = %d\n", diacritic_count); ++ // Information about the image and the placement. ++ Image *img = gr_find_image(image_id); ++ ImagePlacement *placement = gr_find_placement(img, placement_id); ++ gr_dump_image_info(file, img, 0); ++ gr_dump_placement_info(file, placement, 0); ++ // 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; ++ } ++ ++ // The coordinates and size (in pixels) of the src rectangle inside the ++ // box of cells (not the pixmap). ++ int src_x = rect->img_start_col * rect->cw; ++ int src_y = rect->img_start_row * rect->ch; ++ int src_w = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int src_h = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ // The coordinates of the dst rectangle inside the window. The size is ++ // the same as the src size in the box (src_w, src_h). ++ int window_x = rect->screen_x_pix; ++ int window_y = rect->screen_y_pix; ++ ++ 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 = placement->pixmap_transformation.pixmap_w; ++ unsigned pixmap_h = placement->pixmap_transformation.pixmap_h; ++ 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); ++ // We use RepeatPad to avoid bilinear filtering halo. ++ XRenderPictureAttributes attrs = {0}; ++ attrs.repeat = RepeatPad; ++ Picture pixmap_pic = XRenderCreatePicture(disp, pixmap, pic_format, ++ CPRepeat, &attrs); ++ ++ // Since the pixmap may be of different size than the destination box of ++ // cells, we must apply a transformation to it. ++ // The XTransform structure describes a matrix to transform the ++ // destination picture coordinates (i.e. in the box) to the source ++ // coordinates (i.e. in the pixmap). We apply only scaling (translation ++ // will be applied to the src coordinates directly): ++ // ++ // pixmap_x = picture_x * pixmap_w / dst_w ++ // pixmap_y = picture_y * pixmap_h / dst_h ++ // ++ // Where dst_w, dst_h, pixmap_w, pixmap_h are from the placement's ++ // pixmap_transformation structure. ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ double xs = (double)tr->pixmap_w / MAX(tr->dst_w, 1); ++ double ys = (double)tr->pixmap_h / MAX(tr->dst_h, 1); ++ // clang-format off ++ XTransform xform = {{ ++ { XDoubleToFixed(xs), XDoubleToFixed( 0), XDoubleToFixed( 0) }, ++ { XDoubleToFixed( 0), XDoubleToFixed(ys), XDoubleToFixed( 0) }, ++ { XDoubleToFixed( 0), XDoubleToFixed( 0), XDoubleToFixed( 1) } ++ }}; ++ // clang-format on ++ XRenderSetPictureTransform(disp, pixmap_pic, &xform); ++ XRenderSetPictureFilter(disp, pixmap_pic, FilterBilinear, NULL, 0); ++ ++ // Do the translation: modify the src coordinates to make them ++ // coordinates into the picture rather than into the box. ++ src_x -= tr->dst_x; ++ src_y -= tr->dst_y; ++ // At this point src_x, src_y, src_w, src_h are coordinates of the ++ // source rectangle inside the picture (scaled pixmap). ++ ++ // Now do clipping. If src coordinates are negative, adjust the dst ++ // coordinates (into the window) and the width/height. We do clipping ++ // instead of using the values as is to avoid rendering the pad area ++ // outside the pixmap. ++ if (src_x < 0) { ++ window_x += -src_x; ++ src_w -= -src_x; ++ src_x = 0; ++ } ++ if (src_y < 0) { ++ window_y += -src_y; ++ src_h -= -src_y; ++ src_y = 0; ++ } ++ ++ // Adjust width and height if the src rectangle exceeds the picture. ++ src_w = MIN(src_w, tr->dst_w - src_x); ++ src_h = MIN(src_h, tr->dst_h - src_y); ++ ++ // 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; ++ if (src_w > 0 && src_h > 0) ++ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, src_x, ++ src_y, src_x, src_y, window_x, window_y, src_w, ++ src_h); ++ ++ // 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); ++} ++ ++/// Closes the file currently being uploaded. This doesn't necessarily finish ++/// the upload since the file may be reopened. ++static void gr_close_current_upload_file() { ++ Image *img = gr_find_image(current_upload_image_id); ++ ImageFrame *frame = gr_get_frame(img, current_upload_frame_index); ++ gr_close_disk_cache_file(frame); ++} ++ ++/// Sets the current image and frame being uploaded. Closes the previous upload ++/// file if it's changed. If `frame` is NULL, clears the current upload ++/// image/frame. ++static void gr_set_current_upload_frame(ImageFrame *frame) { ++ if (frame) { ++ if (current_upload_image_id != frame->image->image_id || ++ current_upload_frame_index != frame->index) { ++ gr_close_current_upload_file(); ++ } ++ current_upload_image_id = frame->image->image_id; ++ current_upload_frame_index = frame->index; ++ GR_LOG("Set current_upload_image_id = %u, " ++ "current_upload_frame_index = %u\n", ++ current_upload_image_id, current_upload_frame_index); ++ } else { ++ gr_close_current_upload_file(); ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ GR_LOG("Set current_upload_image_id = 0\n"); ++ } ++} ++ ++/// Returns whether direct transmission continuation is allowed for the given ++/// command and frame. ++static int gr_transmission_continuation_is_allowed(GraphicsCommand *cmd, ++ ImageFrame *frame) { ++ if (!frame || frame->status != STATUS_UPLOADING) ++ return 0; ++ ++ // If it's the same image and frame as the current upload, allow it. ++ if (current_upload_image_id == frame->image->image_id && ++ current_upload_frame_index == frame->index) ++ return 1; ++ ++ // Otherwise it's a continuation of an interrupted upload. The kitty ++ // graphics protocol doesn't allow interleaving of direct transmission ++ // with other commands, so interrupted uploads must be aborted. However, ++ // we still allow it as an extension, because it's useful for ++ // protocol-unaware multiplexer. We check that there are no ++ // contradictions, and the time since the last upload activity is small. ++ ++ if (cmd->size && cmd->size != frame->expected_size) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of expected size mismatch\n"); ++ return 0; ++ } ++ if (cmd->format && cmd->format != frame->format) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of format mismatch\n"); ++ return 0; ++ } ++ if (cmd->compression && cmd->compression != frame->compression) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of compression mismatch\n"); ++ return 0; ++ } ++ if ((cmd->frame_pix_width && ++ cmd->frame_pix_width != frame->data_pix_width) || ++ (cmd->frame_pix_height && ++ cmd->frame_pix_height != frame->data_pix_height) || ++ (cmd->background_color && ++ cmd->background_color != frame->background_color) || ++ (cmd->background_frame && ++ cmd->background_frame != frame->background_frame_index) || ++ (cmd->gap && cmd->gap != frame->gap) || ++ (cmd->replace_instead_of_blending && ++ cmd->replace_instead_of_blending != !frame->blend)) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of frame parameters mismatch\n"); ++ return 0; ++ } ++ ++ Milliseconds now = gr_now_ms(); ++ if (now - frame->atime > graphics_direct_transmission_timeout_ms) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of time out\n"); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++/// 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) { ++ gr_set_current_upload_frame(frame); ++ ++ if (frame->status != STATUS_UPLOADING) { ++ if (!more) ++ gr_reportuploaderror(frame); ++ gr_set_current_upload_frame(NULL); ++ 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); ++ gr_set_current_upload_frame(NULL); ++ 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); ++ gr_set_current_upload_frame(NULL); ++ return; ++ } ++ free(data); ++ ++ if (!more) { ++ gr_set_current_upload_frame(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); ++} ++ ++/// Copy the image file `frame->original_filename` to the cache directory. This ++/// is done when the image is transmitted via file transfer, or when we have ++/// evicted the image from the disk cache and need to restore it. ++/// If `cmd` is not NULL, it's used to report errors, otherwise errors are only ++/// printed to stderr. ++static void gr_copy_imagefile(ImageFrame *frame, GraphicsCommand *cmd) { ++ GR_LOG("Copying image %s\n", ++ sanitized_filename(frame->original_filename)); ++ // Stat the file and check that it's a regular file and not too big. ++ struct stat st; ++ int stat_res = stat(frame->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) { ++ fprintf(stderr, "Could not load the file %s: %s\n", ++ sanitized_filename(frame->original_filename), ++ stat_error); ++ if (cmd) ++ gr_reporterror_cmd(cmd, "EBADF: %s", stat_error); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ return; ++ } ++ ++ // Check the expected size if specified. ++ if (frame->expected_size && frame->expected_size != st.st_size) { ++ fprintf(stderr, ++ "Could not load, the size doesn't match: %s expected " ++ "%u vs actual %ld\n", ++ sanitized_filename(frame->original_filename), ++ frame->expected_size, st.st_size); ++ // The file has unexpected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ if (cmd) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // If we know the original modification time, we are trying to restore ++ // the evicted image file. Check that the modification time matches. ++ if (frame->original_file_mtime && ++ frame->original_file_mtime != st.st_mtime) { ++ fprintf(stderr, "Could not load, the mtime doesn't match: %s\n", ++ sanitized_filename(frame->original_filename)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_MTIME_MISMATCH; ++ if (cmd) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ frame->original_file_mtime = st.st_mtime; ++ ++ 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(frame->original_filename, tmp_filename_symlink) || ++ system(command) != 0) { ++ fprintf(stderr, ++ "Could not copy the image " ++ "%s (symlink %s) to %s", ++ sanitized_filename(frame->original_filename), ++ tmp_filename_symlink, cache_filename); ++ if (cmd) ++ gr_reporterror_cmd(cmd, "EBADF: could not copy the " ++ "image to the cache dir"); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ return; ++ } ++ ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ // Set the status and update disk size variables. ++ 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; ++} ++ ++/// Tries to restore the image file for `frame` if the original file is still ++/// available. ++static void gr_try_restore_imagefile(ImageFrame *frame) { ++ if (frame->disk_size != 0) ++ return; ++ if (gr_is_original_file_still_available(frame)) ++ gr_copy_imagefile(frame, NULL); ++} ++ ++/// 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. ++ frame->original_filename = gr_base64dec(cmd->payload, NULL); ++ // Copy the file to the cache directory. ++ gr_copy_imagefile(frame, cmd); ++ if (frame->status == STATUS_UPLOADING_SUCCESS) { ++ // Everything seems fine, try to load and redraw ++ // existing instances. ++ gr_schedule_image_redraw(frame->image); ++ frame = gr_loadimage_and_report(frame); ++ } ++ // Delete the original file if it's temporary. ++ if (cmd->transmission_medium == 't') ++ gr_delete_tmp_file(frame->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 (gr_transmission_continuation_is_allowed(cmd, frame)) { ++ // 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. ++ gr_close_disk_cache_file(frame); ++ // 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; ++ } ++ ++ int was_transmission = 0; ++ ImageFrame *frame = NULL; ++ ++ switch (cmd->action) { ++ case 0: ++ // If no action is specified, it is data transmission. ++ case 't': ++ case 'q': ++ case 'f': ++ was_transmission = 1; ++ // 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': ++ was_transmission = 1; ++ // 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); ++ break; ++ } ++ ++ if (!was_transmission || ++ (cmd->transmission_medium && cmd->transmission_medium != 'd')) { ++ // If it wasn't a transmission command, or if the transmission ++ // wasn't direct, clear the current upload frame and close the ++ // file. (If it was a direct transmission, the current upload ++ // was handled inside `gr_append_data`.) ++ gr_set_current_upload_frame(NULL); ++ } ++} ++ ++/// 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 ++#include ++#include ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm); ++/// Deinitialize the graphics module. ++void gr_deinit(); ++ ++/// Add an image rectangle to a list if rectangles to draw. This function may ++/// actually draw some rectangles, or it may wait till more rectangles are ++/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. ++/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the ++/// part of the image to draw (row/col indices are zero-based, ends are ++/// excluded). ++/// - `x_col` and `y_row` are the coordinates of the top-left corner of the ++/// image in the terminal grid. ++/// - `x_pix` and `y_pix` are the same but in pixels. ++/// - `reverse` indicates whether colors should be inverted. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse); ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch); ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf); ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows); ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters (including '\0'). Returns 1 on success. ++/// Additional informations is returned through `graphics_command_result`. ++int gr_parse_command(char *buf, size_t len); ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++void gr_preview_image(uint32_t image_id, const char *command); ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable); ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state(); ++ ++/// Unloads images to reduce RAM usage. ++void gr_unload_images_to_reduce_ram(); ++ ++/// Executes `callback` for each image cell. 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; +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] ++ ++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;;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 ++# Where is a part of command that specifies the action, it will be ++# repeated for every chunk (if the method is direct), and is the rest ++# of the command that specifies the image parameters. and ++# must not include the transmission method or ';'. ++# Example: ++# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++gr_upload() { ++ arg_action="$1" ++ arg_command="$2" ++ arg_file="$3" ++ if [ "$uploading_method" = "file" ]; then ++ # base64-encode the filename ++ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) ++ # 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 +diff --git a/khash.h b/khash.h +new file mode 100644 +index 0000000..f75f347 +--- /dev/null ++++ b/khash.h +@@ -0,0 +1,627 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, 2009, 2011 by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "khash.h" ++KHASH_MAP_INIT_INT(32, char) ++int main() { ++ int ret, is_missing; ++ khiter_t k; ++ khash_t(32) *h = kh_init(32); ++ k = kh_put(32, h, 5, &ret); ++ kh_value(h, k) = 10; ++ k = kh_get(32, h, 10); ++ is_missing = (k == kh_end(h)); ++ k = kh_get(32, h, 5); ++ kh_del(32, h, k); ++ for (k = kh_begin(h); k != kh_end(h); ++k) ++ if (kh_exist(h, k)) kh_value(h, k) = 1; ++ kh_destroy(32, h); ++ return 0; ++} ++*/ ++ ++/* ++ 2013-05-02 (0.2.8): ++ ++ * Use quadratic probing. When the capacity is power of 2, stepping function ++ i*(i+1)/2 guarantees to traverse each bucket. It is better than double ++ hashing on cache performance and is more robust than linear probing. ++ ++ In theory, double hashing should be more robust than quadratic probing. ++ However, my implementation is probably not for large hash tables, because ++ the second hash function is closely tied to the first hash function, ++ which reduce the effectiveness of double hashing. ++ ++ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php ++ ++ 2011-12-29 (0.2.7): ++ ++ * Minor code clean up; no actual effect. ++ ++ 2011-09-16 (0.2.6): ++ ++ * The capacity is a power of 2. This seems to dramatically improve the ++ speed for simple keys. Thank Zilong Tan for the suggestion. Reference: ++ ++ - http://code.google.com/p/ulib/ ++ - http://nothings.org/computer/judy/ ++ ++ * Allow to optionally use linear probing which usually has better ++ performance for random input. Double hashing is still the default as it ++ is more robust to certain non-random input. ++ ++ * Added Wang's integer hash function (not used by default). This hash ++ function is more robust to certain non-random input. ++ ++ 2011-02-14 (0.2.5): ++ ++ * Allow to declare global functions. ++ ++ 2009-09-26 (0.2.4): ++ ++ * Improve portability ++ ++ 2008-09-19 (0.2.3): ++ ++ * Corrected the example ++ * Improved interfaces ++ ++ 2008-09-11 (0.2.2): ++ ++ * Improved speed a little in kh_put() ++ ++ 2008-09-10 (0.2.1): ++ ++ * Added kh_clear() ++ * Fixed a compiling error ++ ++ 2008-09-02 (0.2.0): ++ ++ * Changed to token concatenation which increases flexibility. ++ ++ 2008-08-31 (0.1.2): ++ ++ * Fixed a bug in kh_get(), which has not been tested previously. ++ ++ 2008-08-31 (0.1.1): ++ ++ * Added destructor ++*/ ++ ++ ++#ifndef __AC_KHASH_H ++#define __AC_KHASH_H ++ ++/*! ++ @header ++ ++ Generic hash table library. ++ */ ++ ++#define AC_VERSION_KHASH_H "0.2.8" ++ ++#include ++#include ++#include ++ ++/* compiler specific configuration */ ++ ++#if UINT_MAX == 0xffffffffu ++typedef unsigned int khint32_t; ++#elif ULONG_MAX == 0xffffffffu ++typedef unsigned long khint32_t; ++#endif ++ ++#if ULONG_MAX == ULLONG_MAX ++typedef unsigned long khint64_t; ++#else ++typedef unsigned long long khint64_t; ++#endif ++ ++#ifndef kh_inline ++#ifdef _MSC_VER ++#define kh_inline __inline ++#else ++#define kh_inline inline ++#endif ++#endif /* kh_inline */ ++ ++#ifndef klib_unused ++#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) ++#define klib_unused __attribute__ ((__unused__)) ++#else ++#define klib_unused ++#endif ++#endif /* klib_unused */ ++ ++typedef khint32_t khint_t; ++typedef khint_t khiter_t; ++ ++#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) ++#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) ++#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) ++#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) ++#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) ++#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) ++#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) ++ ++#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) ++ ++#ifndef kroundup32 ++#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++#endif ++ ++#ifndef kcalloc ++#define kcalloc(N,Z) calloc(N,Z) ++#endif ++#ifndef kmalloc ++#define kmalloc(Z) malloc(Z) ++#endif ++#ifndef krealloc ++#define krealloc(P,Z) realloc(P,Z) ++#endif ++#ifndef kfree ++#define kfree(P) free(P) ++#endif ++ ++static const double __ac_HASH_UPPER = 0.77; ++ ++#define __KHASH_TYPE(name, khkey_t, khval_t) \ ++ typedef struct kh_##name##_s { \ ++ khint_t n_buckets, size, n_occupied, upper_bound; \ ++ khint32_t *flags; \ ++ khkey_t *keys; \ ++ khval_t *vals; \ ++ } kh_##name##_t; ++ ++#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ ++ extern kh_##name##_t *kh_init_##name(void); \ ++ extern void kh_destroy_##name(kh_##name##_t *h); \ ++ extern void kh_clear_##name(kh_##name##_t *h); \ ++ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ ++ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ ++ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ ++ extern void kh_del_##name(kh_##name##_t *h, khint_t x); ++ ++#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ SCOPE kh_##name##_t *kh_init_##name(void) { \ ++ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ ++ } \ ++ SCOPE void kh_destroy_##name(kh_##name##_t *h) \ ++ { \ ++ if (h) { \ ++ kfree((void *)h->keys); kfree(h->flags); \ ++ kfree((void *)h->vals); \ ++ kfree(h); \ ++ } \ ++ } \ ++ SCOPE void kh_clear_##name(kh_##name##_t *h) \ ++ { \ ++ if (h && h->flags) { \ ++ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ ++ h->size = h->n_occupied = 0; \ ++ } \ ++ } \ ++ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ ++ { \ ++ if (h->n_buckets) { \ ++ khint_t k, i, last, mask, step = 0; \ ++ mask = h->n_buckets - 1; \ ++ k = __hash_func(key); i = k & mask; \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) return h->n_buckets; \ ++ } \ ++ return __ac_iseither(h->flags, i)? h->n_buckets : i; \ ++ } else return 0; \ ++ } \ ++ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ ++ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ ++ khint32_t *new_flags = 0; \ ++ khint_t j = 1; \ ++ { \ ++ kroundup32(new_n_buckets); \ ++ if (new_n_buckets < 4) new_n_buckets = 4; \ ++ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ ++ else { /* hash table size to be changed (shrink or expand); rehash */ \ ++ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (!new_flags) return -1; \ ++ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (h->n_buckets < new_n_buckets) { /* expand */ \ ++ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (!new_keys) { kfree(new_flags); return -1; } \ ++ h->keys = new_keys; \ ++ if (kh_is_map) { \ ++ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ if (!new_vals) { kfree(new_flags); return -1; } \ ++ h->vals = new_vals; \ ++ } \ ++ } /* otherwise shrink */ \ ++ } \ ++ } \ ++ if (j) { /* rehashing is needed */ \ ++ for (j = 0; j != h->n_buckets; ++j) { \ ++ if (__ac_iseither(h->flags, j) == 0) { \ ++ khkey_t key = h->keys[j]; \ ++ khval_t val; \ ++ khint_t new_mask; \ ++ new_mask = new_n_buckets - 1; \ ++ if (kh_is_map) val = h->vals[j]; \ ++ __ac_set_isdel_true(h->flags, j); \ ++ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ ++ khint_t k, i, step = 0; \ ++ k = __hash_func(key); \ ++ i = k & new_mask; \ ++ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ ++ __ac_set_isempty_false(new_flags, i); \ ++ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ ++ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ ++ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ ++ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ ++ } else { /* write the element and jump out of the loop */ \ ++ h->keys[i] = key; \ ++ if (kh_is_map) h->vals[i] = val; \ ++ break; \ ++ } \ ++ } \ ++ } \ ++ } \ ++ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ ++ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ } \ ++ kfree(h->flags); /* free the working space */ \ ++ h->flags = new_flags; \ ++ h->n_buckets = new_n_buckets; \ ++ h->n_occupied = h->size; \ ++ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ ++ } \ ++ return 0; \ ++ } \ ++ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ ++ { \ ++ khint_t x; \ ++ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ ++ if (h->n_buckets > (h->size<<1)) { \ ++ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ ++ { \ ++ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ ++ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ ++ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ ++ else { \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ if (__ac_isdel(h->flags, i)) site = i; \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) { x = site; break; } \ ++ } \ ++ if (x == h->n_buckets) { \ ++ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ ++ else x = i; \ ++ } \ ++ } \ ++ } \ ++ if (__ac_isempty(h->flags, x)) { /* not present at all */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; ++h->n_occupied; \ ++ *ret = 1; \ ++ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; \ ++ *ret = 2; \ ++ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ ++ return x; \ ++ } \ ++ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ ++ { \ ++ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ ++ __ac_set_isdel_true(h->flags, x); \ ++ --h->size; \ ++ } \ ++ } ++ ++#define KHASH_DECLARE(name, khkey_t, khval_t) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_PROTOTYPES(name, khkey_t, khval_t) ++ ++#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++/* --- BEGIN OF HASH FUNCTIONS --- */ ++ ++/*! @function ++ @abstract Integer hash function ++ @param key The integer [khint32_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int_hash_func(key) (khint32_t)(key) ++/*! @function ++ @abstract Integer comparison function ++ */ ++#define kh_int_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract 64-bit integer hash function ++ @param key The integer [khint64_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) ++/*! @function ++ @abstract 64-bit integer comparison function ++ */ ++#define kh_int64_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract const char* hash function ++ @param s Pointer to a null terminated string ++ @return The hash value ++ */ ++static kh_inline khint_t __ac_X31_hash_string(const char *s) ++{ ++ khint_t h = (khint_t)*s; ++ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; ++ return h; ++} ++/*! @function ++ @abstract Another interface to const char* hash function ++ @param key Pointer to a null terminated string [const char*] ++ @return The hash value [khint_t] ++ */ ++#define kh_str_hash_func(key) __ac_X31_hash_string(key) ++/*! @function ++ @abstract Const char* comparison function ++ */ ++#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) ++ ++static kh_inline khint_t __ac_Wang_hash(khint_t key) ++{ ++ key += ~(key << 15); ++ key ^= (key >> 10); ++ key += (key << 3); ++ key ^= (key >> 6); ++ key += ~(key << 11); ++ key ^= (key >> 16); ++ return key; ++} ++#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) ++ ++/* --- END OF HASH FUNCTIONS --- */ ++ ++/* Other convenient macros... */ ++ ++/*! ++ @abstract Type of the hash table. ++ @param name Name of the hash table [symbol] ++ */ ++#define khash_t(name) kh_##name##_t ++ ++/*! @function ++ @abstract Initiate a hash table. ++ @param name Name of the hash table [symbol] ++ @return Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_init(name) kh_init_##name() ++ ++/*! @function ++ @abstract Destroy a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_destroy(name, h) kh_destroy_##name(h) ++ ++/*! @function ++ @abstract Reset a hash table without deallocating memory. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_clear(name, h) kh_clear_##name(h) ++ ++/*! @function ++ @abstract Resize a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param s New size [khint_t] ++ */ ++#define kh_resize(name, h, s) kh_resize_##name(h, s) ++ ++/*! @function ++ @abstract Insert a key to the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @param r Extra return code: -1 if the operation failed; ++ 0 if the key is present in the hash table; ++ 1 if the bucket is empty (never used); 2 if the element in ++ the bucket has been deleted [int*] ++ @return Iterator to the inserted element [khint_t] ++ */ ++#define kh_put(name, h, k, r) kh_put_##name(h, k, r) ++ ++/*! @function ++ @abstract Retrieve a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] ++ */ ++#define kh_get(name, h, k) kh_get_##name(h, k) ++ ++/*! @function ++ @abstract Remove a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Iterator to the element to be deleted [khint_t] ++ */ ++#define kh_del(name, h, k) kh_del_##name(h, k) ++ ++/*! @function ++ @abstract Test whether a bucket contains data. ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return 1 if containing data; 0 otherwise [int] ++ */ ++#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) ++ ++/*! @function ++ @abstract Get key given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Key [type of keys] ++ */ ++#define kh_key(h, x) ((h)->keys[x]) ++ ++/*! @function ++ @abstract Get value given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Value [type of values] ++ @discussion For hash sets, calling this results in segfault. ++ */ ++#define kh_val(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Alias of kh_val() ++ */ ++#define kh_value(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Get the start iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The start iterator [khint_t] ++ */ ++#define kh_begin(h) (khint_t)(0) ++ ++/*! @function ++ @abstract Get the end iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The end iterator [khint_t] ++ */ ++#define kh_end(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Get the number of elements in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of elements in the hash table [khint_t] ++ */ ++#define kh_size(h) ((h)->size) ++ ++/*! @function ++ @abstract Get the number of buckets in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of buckets in the hash table [khint_t] ++ */ ++#define kh_n_buckets(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Iterate over the entries in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param kvar Variable to which key will be assigned ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (kvar) = kh_key(h,__i); \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/*! @function ++ @abstract Iterate over the values in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach_value(h, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/* More convenient interfaces */ ++ ++/*! @function ++ @abstract Instantiate a hash set containing integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT(name) \ ++ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT(name, khval_t) \ ++ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash set containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT64(name) \ ++ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT64(name, khval_t) \ ++ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) ++ ++typedef const char *kh_cstr_t; ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_STR(name) \ ++ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_STR(name, khval_t) \ ++ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) ++ ++#endif /* __AC_KHASH_H */ +diff --git a/kvec.h b/kvec.h +new file mode 100644 +index 0000000..10f1c5b +--- /dev/null ++++ b/kvec.h +@@ -0,0 +1,90 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "kvec.h" ++int main() { ++ kvec_t(int) array; ++ kv_init(array); ++ kv_push(int, array, 10); // append ++ kv_a(int, array, 20) = 5; // dynamic ++ kv_A(array, 20) = 4; // static ++ kv_destroy(array); ++ return 0; ++} ++*/ ++ ++/* ++ 2008-09-22 (0.1.0): ++ ++ * The initial version. ++ ++*/ ++ ++#ifndef AC_KVEC_H ++#define AC_KVEC_H ++ ++#include ++ ++#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++ ++#define kvec_t(type) struct { size_t n, m; type *a; } ++#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) ++#define kv_destroy(v) free((v).a) ++#define kv_A(v, i) ((v).a[(i)]) ++#define kv_pop(v) ((v).a[--(v).n]) ++#define kv_size(v) ((v).n) ++#define kv_max(v) ((v).m) ++ ++#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) ++ ++#define kv_copy(type, v1, v0) do { \ ++ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ ++ (v1).n = (v0).n; \ ++ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ ++ } while (0) \ ++ ++#define kv_push(type, v, x) do { \ ++ if ((v).n == (v).m) { \ ++ (v).m = (v).m? (v).m<<1 : 2; \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ ++ } \ ++ (v).a[(v).n++] = (x); \ ++ } while (0) ++ ++#define kv_pushp(type, v) ((((v).n == (v).m)? \ ++ ((v).m = ((v).m? (v).m<<1 : 2), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : 0), ((v).a + ((v).n++))) ++ ++#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ ++ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ ++ : 0), (v).a[(i)]) ++ ++#endif +diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c +new file mode 100644 +index 0000000..829c0fc +--- /dev/null ++++ b/rowcolumn_diacritics_helpers.c +@@ -0,0 +1,391 @@ ++#include ++ ++uint16_t diacritic_to_num(uint32_t code) ++{ ++ switch (code) { ++ case 0x305: ++ return code - 0x305 + 1; ++ case 0x30d: ++ case 0x30e: ++ return code - 0x30d + 2; ++ case 0x310: ++ return code - 0x310 + 4; ++ case 0x312: ++ return code - 0x312 + 5; ++ case 0x33d: ++ case 0x33e: ++ case 0x33f: ++ return code - 0x33d + 6; ++ case 0x346: ++ return code - 0x346 + 9; ++ case 0x34a: ++ case 0x34b: ++ case 0x34c: ++ return code - 0x34a + 10; ++ case 0x350: ++ case 0x351: ++ case 0x352: ++ return code - 0x350 + 13; ++ case 0x357: ++ return code - 0x357 + 16; ++ case 0x35b: ++ return code - 0x35b + 17; ++ case 0x363: ++ case 0x364: ++ case 0x365: ++ case 0x366: ++ case 0x367: ++ case 0x368: ++ case 0x369: ++ case 0x36a: ++ case 0x36b: ++ case 0x36c: ++ case 0x36d: ++ case 0x36e: ++ case 0x36f: ++ return code - 0x363 + 18; ++ case 0x483: ++ case 0x484: ++ case 0x485: ++ case 0x486: ++ case 0x487: ++ return code - 0x483 + 31; ++ case 0x592: ++ case 0x593: ++ case 0x594: ++ case 0x595: ++ return code - 0x592 + 36; ++ case 0x597: ++ case 0x598: ++ case 0x599: ++ return code - 0x597 + 40; ++ case 0x59c: ++ case 0x59d: ++ case 0x59e: ++ case 0x59f: ++ case 0x5a0: ++ case 0x5a1: ++ return code - 0x59c + 43; ++ case 0x5a8: ++ case 0x5a9: ++ return code - 0x5a8 + 49; ++ case 0x5ab: ++ case 0x5ac: ++ return code - 0x5ab + 51; ++ case 0x5af: ++ return code - 0x5af + 53; ++ case 0x5c4: ++ return code - 0x5c4 + 54; ++ case 0x610: ++ case 0x611: ++ case 0x612: ++ case 0x613: ++ case 0x614: ++ case 0x615: ++ case 0x616: ++ case 0x617: ++ return code - 0x610 + 55; ++ case 0x657: ++ case 0x658: ++ case 0x659: ++ case 0x65a: ++ case 0x65b: ++ return code - 0x657 + 63; ++ case 0x65d: ++ case 0x65e: ++ return code - 0x65d + 68; ++ case 0x6d6: ++ case 0x6d7: ++ case 0x6d8: ++ case 0x6d9: ++ case 0x6da: ++ case 0x6db: ++ case 0x6dc: ++ return code - 0x6d6 + 70; ++ case 0x6df: ++ case 0x6e0: ++ case 0x6e1: ++ case 0x6e2: ++ return code - 0x6df + 77; ++ case 0x6e4: ++ return code - 0x6e4 + 81; ++ case 0x6e7: ++ case 0x6e8: ++ return code - 0x6e7 + 82; ++ case 0x6eb: ++ case 0x6ec: ++ return code - 0x6eb + 84; ++ case 0x730: ++ return code - 0x730 + 86; ++ case 0x732: ++ case 0x733: ++ return code - 0x732 + 87; ++ case 0x735: ++ case 0x736: ++ return code - 0x735 + 89; ++ case 0x73a: ++ return code - 0x73a + 91; ++ case 0x73d: ++ return code - 0x73d + 92; ++ case 0x73f: ++ case 0x740: ++ case 0x741: ++ return code - 0x73f + 93; ++ case 0x743: ++ return code - 0x743 + 96; ++ case 0x745: ++ return code - 0x745 + 97; ++ case 0x747: ++ return code - 0x747 + 98; ++ case 0x749: ++ case 0x74a: ++ return code - 0x749 + 99; ++ case 0x7eb: ++ case 0x7ec: ++ case 0x7ed: ++ case 0x7ee: ++ case 0x7ef: ++ case 0x7f0: ++ case 0x7f1: ++ return code - 0x7eb + 101; ++ case 0x7f3: ++ return code - 0x7f3 + 108; ++ case 0x816: ++ case 0x817: ++ case 0x818: ++ case 0x819: ++ return code - 0x816 + 109; ++ case 0x81b: ++ case 0x81c: ++ case 0x81d: ++ case 0x81e: ++ case 0x81f: ++ case 0x820: ++ case 0x821: ++ case 0x822: ++ case 0x823: ++ return code - 0x81b + 113; ++ case 0x825: ++ case 0x826: ++ case 0x827: ++ return code - 0x825 + 122; ++ case 0x829: ++ case 0x82a: ++ case 0x82b: ++ case 0x82c: ++ case 0x82d: ++ return code - 0x829 + 125; ++ case 0x951: ++ return code - 0x951 + 130; ++ case 0x953: ++ case 0x954: ++ return code - 0x953 + 131; ++ case 0xf82: ++ case 0xf83: ++ return code - 0xf82 + 133; ++ case 0xf86: ++ case 0xf87: ++ return code - 0xf86 + 135; ++ case 0x135d: ++ case 0x135e: ++ case 0x135f: ++ return code - 0x135d + 137; ++ case 0x17dd: ++ return code - 0x17dd + 140; ++ case 0x193a: ++ return code - 0x193a + 141; ++ case 0x1a17: ++ return code - 0x1a17 + 142; ++ case 0x1a75: ++ case 0x1a76: ++ case 0x1a77: ++ case 0x1a78: ++ case 0x1a79: ++ case 0x1a7a: ++ case 0x1a7b: ++ case 0x1a7c: ++ return code - 0x1a75 + 143; ++ case 0x1b6b: ++ return code - 0x1b6b + 151; ++ case 0x1b6d: ++ case 0x1b6e: ++ case 0x1b6f: ++ case 0x1b70: ++ case 0x1b71: ++ case 0x1b72: ++ case 0x1b73: ++ return code - 0x1b6d + 152; ++ case 0x1cd0: ++ case 0x1cd1: ++ case 0x1cd2: ++ return code - 0x1cd0 + 159; ++ case 0x1cda: ++ case 0x1cdb: ++ return code - 0x1cda + 162; ++ case 0x1ce0: ++ return code - 0x1ce0 + 164; ++ case 0x1dc0: ++ case 0x1dc1: ++ return code - 0x1dc0 + 165; ++ case 0x1dc3: ++ case 0x1dc4: ++ case 0x1dc5: ++ case 0x1dc6: ++ case 0x1dc7: ++ case 0x1dc8: ++ case 0x1dc9: ++ return code - 0x1dc3 + 167; ++ case 0x1dcb: ++ case 0x1dcc: ++ return code - 0x1dcb + 174; ++ case 0x1dd1: ++ case 0x1dd2: ++ case 0x1dd3: ++ case 0x1dd4: ++ case 0x1dd5: ++ case 0x1dd6: ++ case 0x1dd7: ++ case 0x1dd8: ++ case 0x1dd9: ++ case 0x1dda: ++ case 0x1ddb: ++ case 0x1ddc: ++ case 0x1ddd: ++ case 0x1dde: ++ case 0x1ddf: ++ case 0x1de0: ++ case 0x1de1: ++ case 0x1de2: ++ case 0x1de3: ++ case 0x1de4: ++ case 0x1de5: ++ case 0x1de6: ++ return code - 0x1dd1 + 176; ++ case 0x1dfe: ++ return code - 0x1dfe + 198; ++ case 0x20d0: ++ case 0x20d1: ++ return code - 0x20d0 + 199; ++ case 0x20d4: ++ case 0x20d5: ++ case 0x20d6: ++ case 0x20d7: ++ return code - 0x20d4 + 201; ++ case 0x20db: ++ case 0x20dc: ++ return code - 0x20db + 205; ++ case 0x20e1: ++ return code - 0x20e1 + 207; ++ case 0x20e7: ++ return code - 0x20e7 + 208; ++ case 0x20e9: ++ return code - 0x20e9 + 209; ++ case 0x20f0: ++ return code - 0x20f0 + 210; ++ case 0x2cef: ++ case 0x2cf0: ++ case 0x2cf1: ++ return code - 0x2cef + 211; ++ case 0x2de0: ++ case 0x2de1: ++ case 0x2de2: ++ case 0x2de3: ++ case 0x2de4: ++ case 0x2de5: ++ case 0x2de6: ++ case 0x2de7: ++ case 0x2de8: ++ case 0x2de9: ++ case 0x2dea: ++ case 0x2deb: ++ case 0x2dec: ++ case 0x2ded: ++ case 0x2dee: ++ case 0x2def: ++ case 0x2df0: ++ case 0x2df1: ++ case 0x2df2: ++ case 0x2df3: ++ case 0x2df4: ++ case 0x2df5: ++ case 0x2df6: ++ case 0x2df7: ++ case 0x2df8: ++ case 0x2df9: ++ case 0x2dfa: ++ case 0x2dfb: ++ case 0x2dfc: ++ case 0x2dfd: ++ case 0x2dfe: ++ case 0x2dff: ++ return code - 0x2de0 + 214; ++ case 0xa66f: ++ return code - 0xa66f + 246; ++ case 0xa67c: ++ case 0xa67d: ++ return code - 0xa67c + 247; ++ case 0xa6f0: ++ case 0xa6f1: ++ return code - 0xa6f0 + 249; ++ case 0xa8e0: ++ case 0xa8e1: ++ case 0xa8e2: ++ case 0xa8e3: ++ case 0xa8e4: ++ case 0xa8e5: ++ case 0xa8e6: ++ case 0xa8e7: ++ case 0xa8e8: ++ case 0xa8e9: ++ case 0xa8ea: ++ case 0xa8eb: ++ case 0xa8ec: ++ case 0xa8ed: ++ case 0xa8ee: ++ case 0xa8ef: ++ case 0xa8f0: ++ case 0xa8f1: ++ return code - 0xa8e0 + 251; ++ case 0xaab0: ++ return code - 0xaab0 + 269; ++ case 0xaab2: ++ case 0xaab3: ++ return code - 0xaab2 + 270; ++ case 0xaab7: ++ case 0xaab8: ++ return code - 0xaab7 + 272; ++ case 0xaabe: ++ case 0xaabf: ++ return code - 0xaabe + 274; ++ case 0xaac1: ++ return code - 0xaac1 + 276; ++ case 0xfe20: ++ case 0xfe21: ++ case 0xfe22: ++ case 0xfe23: ++ case 0xfe24: ++ case 0xfe25: ++ case 0xfe26: ++ return code - 0xfe20 + 277; ++ case 0x10a0f: ++ return code - 0x10a0f + 284; ++ case 0x10a38: ++ return code - 0x10a38 + 285; ++ case 0x1d185: ++ case 0x1d186: ++ case 0x1d187: ++ case 0x1d188: ++ case 0x1d189: ++ return code - 0x1d185 + 286; ++ case 0x1d1aa: ++ case 0x1d1ab: ++ case 0x1d1ac: ++ case 0x1d1ad: ++ return code - 0x1d1aa + 291; ++ case 0x1d242: ++ case 0x1d243: ++ case 0x1d244: ++ return code - 0x1d242 + 295; ++ } ++ return 0; ++} +diff --git a/st.c b/st.c +index 8e57991..9824ea6 100644 +--- a/st.c ++++ b/st.c +@@ -19,6 +19,7 @@ + + #include "st.h" + #include "win.h" ++#include "graphics.h" + + #if defined(__linux) + #include +@@ -36,6 +37,10 @@ + #define STR_BUF_SIZ ESC_BUF_SIZ + #define STR_ARG_SIZ ESC_ARG_SIZ + ++/* PUA character used as an image placeholder */ ++#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE ++#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE ++ + /* macros */ + #define IS_SET(flag) ((term.mode & (flag)) != 0) + #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +@@ -113,6 +118,8 @@ typedef struct { + typedef struct { + int row; /* nb row */ + int col; /* nb col */ ++ int pixw; /* width of the text area in pixels */ ++ int pixh; /* height of the text area in pixels */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ +@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); + static char utf8encodebyte(Rune, size_t); + static size_t utf8validate(Rune *, size_t); + +-static char *base64dec(const char *); + static char base64dec_getc(const char **); + + static ssize_t xwrite(int, const char *, size_t); +@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; + static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; + static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + ++/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 ++ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ ++uint16_t diacritic_to_num(uint32_t code); ++ + ssize_t + xwrite(int fd, const char *s, size_t len) + { +@@ -616,6 +626,12 @@ getsel(void) + if (gp->mode & ATTR_WDUMMY) + continue; + ++ if (gp->mode & ATTR_IMAGE) { ++ // TODO: Copy diacritics as well ++ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); ++ continue; ++ } ++ + ptr += utf8encode(gp->u, ptr); + } + +@@ -715,11 +731,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)); +@@ -819,7 +838,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 +854,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 +914,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 +923,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 +967,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 +1064,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 +1088,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 +1267,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 +1311,110 @@ 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, ++ 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 = &term.line[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 = &term.line[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 = &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 +1533,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 +1546,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 +1583,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; +@@ -1431,10 +1612,11 @@ tsetattr(const int *attr, int l) + term.c.attr.bg = defaultbg; + break; + case 58: +- /* This starts a sequence to change the color of +- * "underline" pixels. We don't support that and +- * instead eat up a following "5;n" or "2;r;g;b". */ +- tdefcolor(attr, &i, l); ++ 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)) { +@@ -1823,6 +2005,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; + } + } + +@@ -1985,8 +2200,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; + } +@@ -2492,6 +2726,41 @@ check_control_code: + if (selected(term.c.x, term.c.y)) + selclear(); + ++ // 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 = &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]; ++ 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; +@@ -2658,6 +2927,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; +@@ -2665,6 +2936,8 @@ drawregion(int x1, int y1, int x2, int y2) + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } ++ ++ xfinishimagedraw(); + } + + void +@@ -2699,3 +2972,9 @@ redraw(void) + tfulldirt(); + draw(); + } ++ ++Glyph ++getglyphat(int col, int row) ++{ ++ return term.line[row][col]; ++} +diff --git a/st.h b/st.h +index fd3b0d8..c5dd731 100644 +--- a/st.h ++++ b/st.h +@@ -12,7 +12,7 @@ + #define DEFAULT(a, b) (a) = (a) ? (a) : (b) + #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) + #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ +- (a).bg != (b).bg) ++ (a).bg != (b).bg || (a).decor != (b).decor) + #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) + #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) +@@ -20,6 +20,10 @@ + #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) + #define IS_TRUECOL(x) (1 << 24 & (x)) + ++// This decor color indicates that the fg color should be used. Note that it's ++// not a 24-bit color because the 25-th bit is not set. ++#define DECOR_DEFAULT_COLOR 0x0ffffff ++ + enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, +@@ -34,6 +38,7 @@ enum glyph_attribute { + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, ++ ATTR_IMAGE = 1 << 14, + }; + + enum selection_mode { +@@ -52,6 +57,14 @@ enum selection_snap { + SNAP_LINE = 2 + }; + ++enum underline_style { ++ UNDERLINE_STRAIGHT = 1, ++ UNDERLINE_DOUBLE = 2, ++ UNDERLINE_CURLY = 3, ++ UNDERLINE_DOTTED = 4, ++ UNDERLINE_DASHED = 5, ++}; ++ + typedef unsigned char uchar; + typedef unsigned int uint; + typedef unsigned long ulong; +@@ -65,6 +78,7 @@ typedef struct { + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ ++ uint32_t decor; /* decoration (like underline) */ + } Glyph; + + typedef Glyph *Line; +@@ -105,6 +119,8 @@ void selextend(int, int, int, int); + int selected(int, int); + char *getsel(void); + ++Glyph getglyphat(int, int); ++ + size_t utf8encode(Rune, char *); + + void *xmalloc(size_t); +@@ -124,3 +140,69 @@ extern unsigned int tabspaces; + extern unsigned int defaultfg; + extern unsigned int defaultbg; + extern unsigned int defaultcs; ++ ++// Accessors to decoration properties stored in `decor`. ++// The 25-th bit is used to indicate if it's a 24-bit color. ++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; } ++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; } ++static inline void tsetdecorcolor(Glyph *g, uint32_t color) { ++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff); ++} ++static inline void tsetdecorstyle(Glyph *g, uint32_t style) { ++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25); ++} ++ ++ ++// Some accessors to image placeholder properties stored in `u`: ++// - row (1-base) - 9 bits ++// - column (1-base) - 9 bits ++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified, ++// don't forget to subtract 1). ++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits ++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit ++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; } ++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; } ++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; } ++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; } ++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; } ++static inline void tsetimgrow(Glyph *g, uint32_t row) { ++ g->u = (g->u & ~0x1ff) | (row & 0x1ff); ++} ++static inline void tsetimgcol(Glyph *g, uint32_t col) { ++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9); ++} ++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) { ++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18); ++} ++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) { ++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27); ++} ++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) { ++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29); ++} ++ ++/// Returns the full image id. This is a naive implementation, if the most ++/// significant byte is not specified, it's assumed to be 0 instead of inferring ++/// it from the cells to the left. ++static inline uint32_t tgetimgid(Glyph *g) { ++ uint32_t msb = tgetimgid4thbyteplus1(g); ++ if (msb != 0) ++ --msb; ++ return (msb << 24) | (g->fg & 0xFFFFFF); ++} ++ ++/// Sets the full image id. ++static inline void tsetimgid(Glyph *g, uint32_t id) { ++ g->fg = (id & 0xFFFFFF) | (1 << 24); ++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1); ++} ++ ++static inline uint32_t tgetimgplacementid(Glyph *g) { ++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR) ++ return 0; ++ return g->decor & 0xFFFFFF; ++} ++ ++static inline void tsetimgplacementid(Glyph *g, uint32_t id) { ++ g->decor = (id & 0xFFFFFF) | (1 << 24); ++} +diff --git a/st.info b/st.info +index efab2cf..ded76c1 100644 +--- a/st.info ++++ b/st.info +@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, ++ Smulx=\E[4:%p1%dm, + + st| simpleterm, + use=st-mono, +@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, ++# Underline colors ++ Su, ++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m, ++ Setulc1=\E[58:5:%p1%dm, ++ ol=\E[59m, + + st-meta| simpleterm with meta key, + use=st, +diff --git a/win.h b/win.h +index 6de960d..31b3fff 100644 +--- a/win.h ++++ b/win.h +@@ -39,3 +39,6 @@ void xsetpointermotion(int); + void xsetsel(char *); + int xstartdraw(void); + void xximspot(int, int); ++ ++void xstartimagedraw(int *dirty, int rows); ++void xfinishimagedraw(); +diff --git a/x.c b/x.c +index d73152b..6f1bf8c 100644 +--- a/x.c ++++ b/x.c +@@ -4,6 +4,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -19,6 +21,7 @@ char *argv0; + #include "arg.h" + #include "st.h" + #include "win.h" ++#include "graphics.h" + + /* types used in config.h */ + typedef struct { +@@ -59,6 +62,12 @@ static void zoom(const Arg *); + static void zoomabs(const Arg *); + static void zoomreset(const Arg *); + static void ttysend(const Arg *); ++static void previewimage(const Arg *); ++static void showimageinfo(const Arg *); ++static void togglegrdebug(const Arg *); ++static void dumpgrstate(const Arg *); ++static void unloadimages(const Arg *); ++static void toggleimages(const Arg *); + + /* config.h for applying patches and the configuration. */ + #include "config.h" +@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec; + typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ ++ int hborderpx, vborderpx; + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ +@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int); + static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); + static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); + static void xdrawglyph(Glyph, int, int); ++static void xdrawimages(Glyph, Line, int x1, int y1, int x2); ++static void xdrawoneimagecell(Glyph, int x, int y); + static void xclear(int, int, int, int); + static int xgeommasktogravity(int); + static int ximopen(Display *); +@@ -220,6 +232,7 @@ static DC dc; + static XWindow xw; + static XSelection xsel; + static TermWindow win; ++static unsigned int mouse_col = 0, mouse_row = 0; + + /* Font Ring Cache */ + enum { +@@ -328,10 +341,72 @@ ttysend(const Arg *arg) + ttywrite(arg->s, strlen(arg->s), 1); + } + ++void ++previewimage(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ gr_preview_image(image_id, arg->s); ++ } ++} ++ ++void ++showimageinfo(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ char stcommand[256] = {0}; ++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0); ++ if (len > sizeof(stcommand) - 1) { ++ fprintf(stderr, "Executable name too long: %s\n", ++ argv0); ++ return; ++ } ++ gr_show_image_info(image_id, tgetimgplacementid(&g), ++ tgetimgcol(&g), tgetimgrow(&g), ++ tgetisclassicplaceholder(&g), ++ tgetimgdiacriticcount(&g), argv0); ++ } ++} ++ ++void ++togglegrdebug(const Arg *arg) ++{ ++ graphics_debug_mode = (graphics_debug_mode + 1) % 3; ++ redraw(); ++} ++ ++void ++dumpgrstate(const Arg *arg) ++{ ++ gr_dump_state(); ++} ++ ++void ++unloadimages(const Arg *arg) ++{ ++ gr_unload_images_to_reduce_ram(); ++} ++ ++void ++toggleimages(const Arg *arg) ++{ ++ graphics_display_images = !graphics_display_images; ++ redraw(); ++} ++ + int + evcol(XEvent *e) + { +- int x = e->xbutton.x - borderpx; ++ int x = e->xbutton.x - win.hborderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; + } +@@ -339,7 +414,7 @@ evcol(XEvent *e) + int + evrow(XEvent *e) + { +- int y = e->xbutton.y - borderpx; ++ int y = e->xbutton.y - win.vborderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; + } +@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release) + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + ++ mouse_col = evcol(e); ++ mouse_row = evrow(e); ++ + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && +@@ -739,6 +817,9 @@ cresize(int width, int height) + col = MAX(1, col); + row = MAX(1, row); + ++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100; ++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100; ++ + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +@@ -869,8 +950,8 @@ xhints(void) + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; +- sizeh->height_inc = win.ch; +- sizeh->width_inc = win.cw; ++ sizeh->height_inc = 1; ++ sizeh->width_inc = 1; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; +@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } +- defaultfontsize = usedfontsize; ++ if (defaultfontsize <= 0) ++ defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) +@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; +- if (fontsize == 0) ++ if (defaultfontsize <= 0 && fontsize == 0) + defaultfontsize = fontval; + } + +@@ -1152,8 +1234,8 @@ xinit(int cols, int rows) + xloadcols(); + + /* adjust fixed window geometry */ +- win.w = 2 * borderpx + cols * win.cw; +- win.h = 2 * borderpx + rows * win.ch; ++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw; ++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) +@@ -1240,12 +1322,15 @@ xinit(int cols, int rows) + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; ++ ++ // Initialize the graphics (image display) module. ++ gr_init(xw.dpy, xw.vis, xw.cmap); + } + + int + xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) + { +- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; ++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; +@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + if (mode == ATTR_WDUMMY) + continue; + ++ /* Draw spaces for image placeholders (images will be drawn ++ * separately). */ ++ if (mode & ATTR_IMAGE) ++ rune = ' '; ++ + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; +@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + return numspecs; + } + ++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen` ++ * is the length of the dash plus the length of the gap. `fraction` is the ++ * fraction of the dash length compared to `wavelen`. */ ++static void ++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w, ++ int wavelen, float fraction, int thick) ++{ ++ int dashw = MAX(1, fraction * wavelen); ++ for (int i = x - x % wavelen; i < x + w; i += wavelen) { ++ int startx = MAX(i, x); ++ int endx = MIN(i + dashw, x + w); ++ if (startx < endx) ++ XftDrawRect(xw.draw, color, startx, y, endx - startx, ++ thick); ++ } ++} ++ ++/* Draws an undercurl. `h` is the total height, including line thickness. */ ++static void ++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) ++{ ++ XGCValues gcvals = {.foreground = color->pixel, ++ .line_width = thick, ++ .line_style = LineSolid, ++ .cap_style = CapRound}; ++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw), ++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle, ++ &gcvals); ++ ++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h}; ++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted); ++ ++ int yoffset = thick / 2; ++ int segh = MAX(1, h - thick); ++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't ++ * look good without antialiasing. */ ++ int segw = segh; ++ int wavelen = MAX(1, segw * 2); ++ ++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) { ++ XPoint points[3] = {{.x = i, .y = y + yoffset}, ++ {.x = i + segw, .y = y + yoffset + segh}, ++ {.x = i + wavelen, .y = y + yoffset}}; ++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3, ++ CoordModeOrigin); ++ } ++ ++ XFreeGC(xw.dpy, gc); ++} ++ + void + xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) + { + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); +- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, ++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; +@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { +- xclear(0, (y == 0)? 0 : winy, borderpx, ++ xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + +- ((winy + win.ch >= borderpx + win.th)? win.h : 0)); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } +- if (winx + width >= borderpx + win.tw) { ++ if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, +- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) +- xclear(winx, 0, winx + width, borderpx); +- if (winy + win.ch >= borderpx + win.th) ++ xclear(winx, 0, winx + width, win.vborderpx); ++ if (winy + win.ch >= win.vborderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ +@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + +- /* Render the glyphs. */ +- XftDrawGlyphFontSpec(xw.draw, fg, specs, len); +- +- /* Render underline and strikethrough. */ ++ /* Decoration color. */ ++ Color decor; ++ uint32_t decorcolor = tgetdecorcolor(&base); ++ if (decorcolor == DECOR_DEFAULT_COLOR) { ++ decor = *fg; ++ } else if (IS_TRUECOL(decorcolor)) { ++ colfg.alpha = 0xffff; ++ colfg.red = TRUERED(decorcolor); ++ colfg.green = TRUEGREEN(decorcolor); ++ colfg.blue = TRUEBLUE(decorcolor); ++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor); ++ } else { ++ decor = dc.col[decorcolor]; ++ } ++ decor.color.alpha = 0xffff; ++ decor.pixel |= 0xff << 24; ++ ++ /* Float thickness, used as a base to compute other values. */ ++ float fthick = dc.font.height / 18.0; ++ /* Integer thickness in pixels. Must not be 0. */ ++ int thick = MAX(1, roundf(fthick)); ++ /* The default gap between the baseline and a single underline. */ ++ int gap = roundf(fthick * 2); ++ /* The total thickness of a double underline. */ ++ int doubleh = thick * 2 + ceilf(fthick * 0.5); ++ /* The total thickness of an undercurl. */ ++ int curlh = thick * 2 + roundf(fthick * 0.75); ++ ++ /* Render the underline before the glyphs. */ + if (base.mode & ATTR_UNDERLINE) { +- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, +- width, 1); ++ uint32_t style = tgetdecorstyle(&base); ++ int liney = winy + dc.font.ascent + gap; ++ /* Adjust liney to guarantee that a single underline fits. */ ++ liney -= MAX(0, liney + thick - (winy + win.ch)); ++ if (style == UNDERLINE_DOUBLE) { ++ liney -= MAX(0, liney + doubleh - (winy + win.ch)); ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ XftDrawRect(xw.draw, &decor, winx, ++ liney + doubleh - thick, width, thick); ++ } else if (style == UNDERLINE_DOTTED) { ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ thick * 2, 0.5, thick); ++ } else if (style == UNDERLINE_DASHED) { ++ int wavelen = MAX(2, win.cw * 0.9); ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ wavelen, 0.65, thick); ++ } else if (style == UNDERLINE_CURLY) { ++ liney -= MAX(0, liney + curlh - (winy + win.ch)); ++ xdrawundercurl(xw.draw, &decor, winx, liney, width, ++ curlh, thick); ++ } else { ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ } + } + ++ /* Render the glyphs. */ ++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len); ++ ++ /* Render strikethrough. Alway use the fg color. */ + if (base.mode & ATTR_STRUCK) { +- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, +- width, 1); ++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, ++ width, thick); + } + + /* Reset clip to none. */ +@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y) + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); ++ if (g.mode & ATTR_IMAGE) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ xdrawoneimagecell(g, x, y); ++ gr_finish_drawing(xw.buf); ++ } + } + + void +@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + if (IS_SET(MODE_HIDE)) + return; + ++ // If it's an image, just draw a ballot box for simplicity. ++ if (g.mode & ATTR_IMAGE) ++ g.u = 0x2610; ++ + /* + * Select the right color for the right mode. + */ +@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - \ ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + (cx + 1) * win.cw - 1, +- borderpx + cy * win.ch, ++ win.hborderpx + (cx + 1) * win.cw - 1, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - 1, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } + } + ++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming ++ * that they have the same attributes (and thus the same lower 24 bits of the ++ * image ID and the same placement ID). */ ++void ++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) { ++ int y_pix = win.vborderpx + y1 * win.ch; ++ uint32_t image_id_24bits = base.fg & 0xFFFFFF; ++ uint32_t placement_id = tgetimgplacementid(&base); ++ // Columns and rows are 1-based, 0 means unspecified. ++ int last_col = 0; ++ int last_row = 0; ++ int last_start_col = 0; ++ int last_start_x = x1; ++ // The most significant byte is also 1-base, subtract 1 before use. ++ uint32_t last_id_4thbyteplus1 = 0; ++ // We may need to inherit row/column/4th byte from the previous cell. ++ Glyph *prev = &line[x1 - 1]; ++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) && ++ (prev->fg & 0xFFFFFF) == image_id_24bits && ++ prev->decor == base.decor) { ++ last_row = tgetimgrow(prev); ++ last_col = tgetimgcol(prev); ++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev); ++ last_start_col = last_col + 1; ++ } ++ for (int x = x1; x < x2; ++x) { ++ Glyph *g = &line[x]; ++ uint32_t cur_row = tgetimgrow(g); ++ uint32_t cur_col = tgetimgcol(g); ++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g); ++ uint32_t num_diacritics = tgetimgdiacriticcount(g); ++ // If the row is not specified, assume it's the same as the row ++ // of the previous cell. Note that `cur_row` may contain a ++ // value imputed earlier, which will be preserved if `last_row` ++ // is zero (i.e. we don't know the row of the previous cell). ++ if (last_row && (num_diacritics == 0 || !cur_row)) ++ cur_row = last_row; ++ // If the column is not specified and the row is the same as the ++ // row of the previous cell, then assume that the column is the ++ // next one. ++ if (last_col && (num_diacritics <= 1 || !cur_col) && ++ cur_row == last_row) ++ cur_col = last_col + 1; ++ // If the additional id byte is not specified and the ++ // coordinates are consecutive, assume the byte is also the ++ // same. ++ if (last_id_4thbyteplus1 && ++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) && ++ cur_row == last_row && cur_col == last_col + 1) ++ cur_id_4thbyteplus1 = last_id_4thbyteplus1; ++ // If we couldn't infer row and column, start from the top left ++ // corner. ++ if (cur_row == 0) ++ cur_row = 1; ++ if (cur_col == 0) ++ cur_col = 1; ++ // If this cell breaks a contiguous stripe of image cells, draw ++ // that line and start a new one. ++ if (cur_col != last_col + 1 || cur_row != last_row || ++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) { ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ if (last_row != 0) { ++ int x_pix = ++ win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect( ++ xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, ++ last_row - 1, last_row, last_start_x, ++ y1, x_pix, y_pix, win.cw, win.ch, ++ base.mode & ATTR_REVERSE); ++ } ++ last_start_col = cur_col; ++ last_start_x = x; ++ } ++ last_row = cur_row; ++ last_col = cur_col; ++ last_id_4thbyteplus1 = cur_id_4thbyteplus1; ++ // Populate the missing glyph data to enable inheritance between ++ // runs and support the naive implementation of tgetimgid. ++ if (!tgetimgrow(g)) ++ tsetimgrow(g, cur_row); ++ // We cannot save this information if there are > 511 cols. ++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0) ++ tsetimgcol(g, cur_col); ++ if (!tgetimgid4thbyteplus1(g)) ++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1); ++ } ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ // Draw the last contiguous stripe. ++ if (last_row != 0) { ++ int x_pix = win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect(xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, last_row - 1, ++ last_row, last_start_x, y1, x_pix, y_pix, ++ win.cw, win.ch, base.mode & ATTR_REVERSE); ++ } ++} ++ ++/* Draw just one image cell without inheriting attributes from the left. */ ++void xdrawoneimagecell(Glyph g, int x, int y) { ++ if (!(g.mode & ATTR_IMAGE)) ++ return; ++ int x_pix = win.hborderpx + x * win.cw; ++ int y_pix = win.vborderpx + y * win.ch; ++ uint32_t row = tgetimgrow(&g) - 1; ++ uint32_t col = tgetimgcol(&g) - 1; ++ uint32_t placement_id = tgetimgplacementid(&g); ++ uint32_t image_id = tgetimgid(&g); ++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row, ++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch, ++ g.mode & ATTR_REVERSE); ++} ++ ++/* Prepare for image drawing. */ ++void xstartimagedraw(int *dirty, int rows) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ gr_mark_dirty_animations(dirty, rows); ++} ++ ++/* Draw all queued image cells. */ ++void xfinishimagedraw() { ++ gr_finish_drawing(xw.buf); ++} ++ + void + xsetenv(void) + { +@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + specs += i; + numspecs -= i; + i = 0; +@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2) + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (i > 0 && base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + } + + void +@@ -1907,6 +2238,7 @@ cmessage(XEvent *e) + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); ++ gr_deinit(); + exit(0); + } + } +@@ -1957,6 +2289,13 @@ run(void) + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + ++ /* Decrease the timeout if there are active animations. */ ++ if (graphics_next_redraw_delay != INT_MAX && ++ IS_SET(MODE_VISIBLE)) ++ timeout = timeout < 0 ? graphics_next_redraw_delay ++ : MIN(timeout, ++ graphics_next_redraw_delay); ++ + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; +-- +2.43.0 + diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c new file mode 100644 index 0000000..829c0fc --- /dev/null +++ b/rowcolumn_diacritics_helpers.c @@ -0,0 +1,391 @@ +#include + +uint16_t diacritic_to_num(uint32_t code) +{ + switch (code) { + case 0x305: + return code - 0x305 + 1; + case 0x30d: + case 0x30e: + return code - 0x30d + 2; + case 0x310: + return code - 0x310 + 4; + case 0x312: + return code - 0x312 + 5; + case 0x33d: + case 0x33e: + case 0x33f: + return code - 0x33d + 6; + case 0x346: + return code - 0x346 + 9; + case 0x34a: + case 0x34b: + case 0x34c: + return code - 0x34a + 10; + case 0x350: + case 0x351: + case 0x352: + return code - 0x350 + 13; + case 0x357: + return code - 0x357 + 16; + case 0x35b: + return code - 0x35b + 17; + case 0x363: + case 0x364: + case 0x365: + case 0x366: + case 0x367: + case 0x368: + case 0x369: + case 0x36a: + case 0x36b: + case 0x36c: + case 0x36d: + case 0x36e: + case 0x36f: + return code - 0x363 + 18; + case 0x483: + case 0x484: + case 0x485: + case 0x486: + case 0x487: + return code - 0x483 + 31; + case 0x592: + case 0x593: + case 0x594: + case 0x595: + return code - 0x592 + 36; + case 0x597: + case 0x598: + case 0x599: + return code - 0x597 + 40; + case 0x59c: + case 0x59d: + case 0x59e: + case 0x59f: + case 0x5a0: + case 0x5a1: + return code - 0x59c + 43; + case 0x5a8: + case 0x5a9: + return code - 0x5a8 + 49; + case 0x5ab: + case 0x5ac: + return code - 0x5ab + 51; + case 0x5af: + return code - 0x5af + 53; + case 0x5c4: + return code - 0x5c4 + 54; + case 0x610: + case 0x611: + case 0x612: + case 0x613: + case 0x614: + case 0x615: + case 0x616: + case 0x617: + return code - 0x610 + 55; + case 0x657: + case 0x658: + case 0x659: + case 0x65a: + case 0x65b: + return code - 0x657 + 63; + case 0x65d: + case 0x65e: + return code - 0x65d + 68; + case 0x6d6: + case 0x6d7: + case 0x6d8: + case 0x6d9: + case 0x6da: + case 0x6db: + case 0x6dc: + return code - 0x6d6 + 70; + case 0x6df: + case 0x6e0: + case 0x6e1: + case 0x6e2: + return code - 0x6df + 77; + case 0x6e4: + return code - 0x6e4 + 81; + case 0x6e7: + case 0x6e8: + return code - 0x6e7 + 82; + case 0x6eb: + case 0x6ec: + return code - 0x6eb + 84; + case 0x730: + return code - 0x730 + 86; + case 0x732: + case 0x733: + return code - 0x732 + 87; + case 0x735: + case 0x736: + return code - 0x735 + 89; + case 0x73a: + return code - 0x73a + 91; + case 0x73d: + return code - 0x73d + 92; + case 0x73f: + case 0x740: + case 0x741: + return code - 0x73f + 93; + case 0x743: + return code - 0x743 + 96; + case 0x745: + return code - 0x745 + 97; + case 0x747: + return code - 0x747 + 98; + case 0x749: + case 0x74a: + return code - 0x749 + 99; + case 0x7eb: + case 0x7ec: + case 0x7ed: + case 0x7ee: + case 0x7ef: + case 0x7f0: + case 0x7f1: + return code - 0x7eb + 101; + case 0x7f3: + return code - 0x7f3 + 108; + case 0x816: + case 0x817: + case 0x818: + case 0x819: + return code - 0x816 + 109; + case 0x81b: + case 0x81c: + case 0x81d: + case 0x81e: + case 0x81f: + case 0x820: + case 0x821: + case 0x822: + case 0x823: + return code - 0x81b + 113; + case 0x825: + case 0x826: + case 0x827: + return code - 0x825 + 122; + case 0x829: + case 0x82a: + case 0x82b: + case 0x82c: + case 0x82d: + return code - 0x829 + 125; + case 0x951: + return code - 0x951 + 130; + case 0x953: + case 0x954: + return code - 0x953 + 131; + case 0xf82: + case 0xf83: + return code - 0xf82 + 133; + case 0xf86: + case 0xf87: + return code - 0xf86 + 135; + case 0x135d: + case 0x135e: + case 0x135f: + return code - 0x135d + 137; + case 0x17dd: + return code - 0x17dd + 140; + case 0x193a: + return code - 0x193a + 141; + case 0x1a17: + return code - 0x1a17 + 142; + case 0x1a75: + case 0x1a76: + case 0x1a77: + case 0x1a78: + case 0x1a79: + case 0x1a7a: + case 0x1a7b: + case 0x1a7c: + return code - 0x1a75 + 143; + case 0x1b6b: + return code - 0x1b6b + 151; + case 0x1b6d: + case 0x1b6e: + case 0x1b6f: + case 0x1b70: + case 0x1b71: + case 0x1b72: + case 0x1b73: + return code - 0x1b6d + 152; + case 0x1cd0: + case 0x1cd1: + case 0x1cd2: + return code - 0x1cd0 + 159; + case 0x1cda: + case 0x1cdb: + return code - 0x1cda + 162; + case 0x1ce0: + return code - 0x1ce0 + 164; + case 0x1dc0: + case 0x1dc1: + return code - 0x1dc0 + 165; + case 0x1dc3: + case 0x1dc4: + case 0x1dc5: + case 0x1dc6: + case 0x1dc7: + case 0x1dc8: + case 0x1dc9: + return code - 0x1dc3 + 167; + case 0x1dcb: + case 0x1dcc: + return code - 0x1dcb + 174; + case 0x1dd1: + case 0x1dd2: + case 0x1dd3: + case 0x1dd4: + case 0x1dd5: + case 0x1dd6: + case 0x1dd7: + case 0x1dd8: + case 0x1dd9: + case 0x1dda: + case 0x1ddb: + case 0x1ddc: + case 0x1ddd: + case 0x1dde: + case 0x1ddf: + case 0x1de0: + case 0x1de1: + case 0x1de2: + case 0x1de3: + case 0x1de4: + case 0x1de5: + case 0x1de6: + return code - 0x1dd1 + 176; + case 0x1dfe: + return code - 0x1dfe + 198; + case 0x20d0: + case 0x20d1: + return code - 0x20d0 + 199; + case 0x20d4: + case 0x20d5: + case 0x20d6: + case 0x20d7: + return code - 0x20d4 + 201; + case 0x20db: + case 0x20dc: + return code - 0x20db + 205; + case 0x20e1: + return code - 0x20e1 + 207; + case 0x20e7: + return code - 0x20e7 + 208; + case 0x20e9: + return code - 0x20e9 + 209; + case 0x20f0: + return code - 0x20f0 + 210; + case 0x2cef: + case 0x2cf0: + case 0x2cf1: + return code - 0x2cef + 211; + case 0x2de0: + case 0x2de1: + case 0x2de2: + case 0x2de3: + case 0x2de4: + case 0x2de5: + case 0x2de6: + case 0x2de7: + case 0x2de8: + case 0x2de9: + case 0x2dea: + case 0x2deb: + case 0x2dec: + case 0x2ded: + case 0x2dee: + case 0x2def: + case 0x2df0: + case 0x2df1: + case 0x2df2: + case 0x2df3: + case 0x2df4: + case 0x2df5: + case 0x2df6: + case 0x2df7: + case 0x2df8: + case 0x2df9: + case 0x2dfa: + case 0x2dfb: + case 0x2dfc: + case 0x2dfd: + case 0x2dfe: + case 0x2dff: + return code - 0x2de0 + 214; + case 0xa66f: + return code - 0xa66f + 246; + case 0xa67c: + case 0xa67d: + return code - 0xa67c + 247; + case 0xa6f0: + case 0xa6f1: + return code - 0xa6f0 + 249; + case 0xa8e0: + case 0xa8e1: + case 0xa8e2: + case 0xa8e3: + case 0xa8e4: + case 0xa8e5: + case 0xa8e6: + case 0xa8e7: + case 0xa8e8: + case 0xa8e9: + case 0xa8ea: + case 0xa8eb: + case 0xa8ec: + case 0xa8ed: + case 0xa8ee: + case 0xa8ef: + case 0xa8f0: + case 0xa8f1: + return code - 0xa8e0 + 251; + case 0xaab0: + return code - 0xaab0 + 269; + case 0xaab2: + case 0xaab3: + return code - 0xaab2 + 270; + case 0xaab7: + case 0xaab8: + return code - 0xaab7 + 272; + case 0xaabe: + case 0xaabf: + return code - 0xaabe + 274; + case 0xaac1: + return code - 0xaac1 + 276; + case 0xfe20: + case 0xfe21: + case 0xfe22: + case 0xfe23: + case 0xfe24: + case 0xfe25: + case 0xfe26: + return code - 0xfe20 + 277; + case 0x10a0f: + return code - 0x10a0f + 284; + case 0x10a38: + return code - 0x10a38 + 285; + case 0x1d185: + case 0x1d186: + case 0x1d187: + case 0x1d188: + case 0x1d189: + return code - 0x1d185 + 286; + case 0x1d1aa: + case 0x1d1ab: + case 0x1d1ac: + case 0x1d1ad: + return code - 0x1d1aa + 291; + case 0x1d242: + case 0x1d243: + case 0x1d244: + return code - 0x1d242 + 295; + } + return 0; +} diff --git a/rowcolumn_diacritics_helpers.o b/rowcolumn_diacritics_helpers.o new file mode 100644 index 0000000000000000000000000000000000000000..70727e73c775f725db6deb7dd46940bf89dae421 GIT binary patch literal 16176 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 F{|lL53ZDP~ literal 0 HcmV?d00001 diff --git a/st b/st new file mode 100755 index 0000000000000000000000000000000000000000..58d9cddcaf2a83326f85cd00b3ae4b4987caf8b4 GIT binary patch literal 191880 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
I#E8+YN$flAXFe*o5bb&LiK&_~dd#rQc?)Ht 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%0AB{a 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 literal 0 HcmV?d00001 diff --git a/st.c b/st.c index 6f40e35..7331a90 100644 --- a/st.c +++ b/st.c @@ -19,6 +19,7 @@ #include "st.h" #include "win.h" +#include "graphics.h" #if defined(__linux) #include @@ -36,6 +37,10 @@ #define STR_BUF_SIZ ESC_BUF_SIZ #define STR_ARG_SIZ ESC_ARG_SIZ +/* PUA character used as an image placeholder */ +#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE +#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE + /* macros */ #define IS_SET(flag) ((term.mode & (flag)) != 0) #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) @@ -113,6 +118,8 @@ typedef struct { typedef struct { int row; /* nb row */ int col; /* nb col */ + int pixw; /* width of the text area in pixels */ + int pixh; /* height of the text area in pixels */ Line *line; /* screen */ Line *alt; /* alternate screen */ int *dirty; /* dirtyness of lines */ @@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); static char utf8encodebyte(Rune, size_t); static size_t utf8validate(Rune *, size_t); -static char *base64dec(const char *); static char base64dec_getc(const char **); static ssize_t xwrite(int, const char *, size_t); @@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; +/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 + * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ +uint16_t diacritic_to_num(uint32_t code); + ssize_t xwrite(int fd, const char *s, size_t len) { @@ -616,6 +626,12 @@ getsel(void) if (gp->mode & ATTR_WDUMMY) continue; + if (gp->mode & ATTR_IMAGE) { + // TODO: Copy diacritics as well + ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); + continue; + } + ptr += utf8encode(gp->u, ptr); } @@ -715,11 +731,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)); @@ -819,7 +838,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 +854,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 +914,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 +923,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 +967,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; @@ -1018,7 +1067,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)); @@ -1041,7 +1091,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(); } @@ -1218,9 +1270,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 @@ -1247,12 +1314,110 @@ 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, + 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 = &term.line[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 = &term.line[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 = &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) { @@ -1371,6 +1536,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; @@ -1383,6 +1549,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 */ @@ -1406,6 +1586,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; @@ -1434,10 +1615,11 @@ tsetattr(const int *attr, int l) term.c.attr.bg = defaultbg; break; case 58: - /* This starts a sequence to change the color of - * "underline" pixels. We don't support that and - * instead eat up a following "5;n" or "2;r;g;b". */ - tdefcolor(attr, &i, l); + 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)) { @@ -1826,6 +2008,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; } } @@ -1988,8 +2203,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; } @@ -2496,6 +2730,41 @@ check_control_code: if (selected(term.c.x, term.c.y)) selclear(); + // 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 = &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]; + 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; @@ -2662,6 +2931,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; @@ -2669,6 +2940,8 @@ drawregion(int x1, int y1, int x2, int y2) term.dirty[y] = 0; xdrawline(term.line[y], x1, y, x2); } + + xfinishimagedraw(); } void @@ -2703,3 +2976,9 @@ redraw(void) tfulldirt(); draw(); } + +Glyph +getglyphat(int col, int row) +{ + return term.line[row][col]; +} diff --git a/st.c.orig b/st.c.orig new file mode 100644 index 0000000..6f40e35 --- /dev/null +++ b/st.c.orig @@ -0,0 +1,2705 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "st.h" +#include "win.h" + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#endif + +/* Arbitrary sizes */ +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 +#define ESC_BUF_SIZ (128*UTF_SIZ) +#define ESC_ARG_SIZ 16 +#define STR_BUF_SIZ ESC_BUF_SIZ +#define STR_ARG_SIZ ESC_ARG_SIZ + +/* macros */ +#define IS_SET(flag) ((term.mode & (flag)) != 0) +#define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +#define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f)) +#define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c)) +#define ISDELIM(u) (u && wcschr(worddelimiters, u)) + +enum term_mode { + MODE_WRAP = 1 << 0, + MODE_INSERT = 1 << 1, + MODE_ALTSCREEN = 1 << 2, + MODE_CRLF = 1 << 3, + MODE_ECHO = 1 << 4, + MODE_PRINT = 1 << 5, + MODE_UTF8 = 1 << 6, +}; + +enum cursor_movement { + CURSOR_SAVE, + CURSOR_LOAD +}; + +enum cursor_state { + CURSOR_DEFAULT = 0, + CURSOR_WRAPNEXT = 1, + CURSOR_ORIGIN = 2 +}; + +enum charset { + CS_GRAPHIC0, + CS_GRAPHIC1, + CS_UK, + CS_USA, + CS_MULTI, + CS_GER, + CS_FIN +}; + +enum escape_state { + ESC_START = 1, + ESC_CSI = 2, + ESC_STR = 4, /* DCS, OSC, PM, APC */ + ESC_ALTCHARSET = 8, + ESC_STR_END = 16, /* a final string was encountered */ + ESC_TEST = 32, /* Enter in test mode */ + ESC_UTF8 = 64, +}; + +typedef struct { + Glyph attr; /* current char attributes */ + int x; + int y; + char state; +} TCursor; + +typedef struct { + int mode; + int type; + int snap; + /* + * Selection variables: + * nb – normalized coordinates of the beginning of the selection + * ne – normalized coordinates of the end of the selection + * ob – original coordinates of the beginning of the selection + * oe – original coordinates of the end of the selection + */ + struct { + int x, y; + } nb, ne, ob, oe; + + int alt; +} Selection; + +/* Internal representation of the screen */ +typedef struct { + int row; /* nb row */ + int col; /* nb col */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ + TCursor c; /* cursor */ + int ocx; /* old cursor col */ + int ocy; /* old cursor row */ + int top; /* top scroll limit */ + int bot; /* bottom scroll limit */ + int mode; /* terminal mode flags */ + int esc; /* escape state flags */ + char trantbl[4]; /* charset table translation */ + int charset; /* current charset */ + int icharset; /* selected charset for sequence */ + int *tabs; + Rune lastc; /* last printed char outside of sequence, 0 if control */ +} Term; + +/* CSI Escape sequence structs */ +/* ESC '[' [[ [] [;]] []] */ +typedef struct { + char buf[ESC_BUF_SIZ]; /* raw string */ + size_t len; /* raw string length */ + char priv; + int arg[ESC_ARG_SIZ]; + int narg; /* nb of args */ + char mode[2]; +} CSIEscape; + +/* STR Escape sequence structs */ +/* ESC type [[ [] [;]] ] ESC '\' */ +typedef struct { + char type; /* ESC type ... */ + char *buf; /* allocated raw string */ + size_t siz; /* allocation size */ + size_t len; /* raw string length */ + char *args[STR_ARG_SIZ]; + int narg; /* nb of args */ +} STREscape; + +static void execsh(char *, char **); +static void stty(char **); +static void sigchld(int); +static void ttywriteraw(const char *, size_t); + +static void csidump(void); +static void csihandle(void); +static void csiparse(void); +static void csireset(void); +static void osc_color_response(int, int, int); +static int eschandle(uchar); +static void strdump(void); +static void strhandle(void); +static void strparse(void); +static void strreset(void); + +static void tprinter(char *, size_t); +static void tdumpsel(void); +static void tdumpline(int); +static void tdump(void); +static void tclearregion(int, int, int, int); +static void tcursor(int); +static void tdeletechar(int); +static void tdeleteline(int); +static void tinsertblank(int); +static void tinsertblankline(int); +static int tlinelen(int); +static void tmoveto(int, int); +static void tmoveato(int, int); +static void tnewline(int); +static void tputtab(int); +static void tputc(Rune); +static void treset(void); +static void tscrollup(int, int); +static void tscrolldown(int, int); +static void tsetattr(const int *, int); +static void tsetchar(Rune, const Glyph *, int, int); +static void tsetdirt(int, int); +static void tsetscroll(int, int); +static void tswapscreen(void); +static void tsetmode(int, int, const int *, int); +static int twrite(const char *, int, int); +static void tfulldirt(void); +static void tcontrolcode(uchar ); +static void tdectest(char ); +static void tdefutf8(char); +static int32_t tdefcolor(const int *, int *, int); +static void tdeftran(char); +static void tstrsequence(uchar); + +static void drawregion(int, int, int, int); + +static void selnormalize(void); +static void selscroll(int, int); +static void selsnap(int *, int *, int); + +static size_t utf8decode(const char *, Rune *, size_t); +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); + +/* Globals */ +static Term term; +static Selection sel; +static CSIEscape csiescseq; +static STREscape strescseq; +static int iofd = 1; +static int cmdfd; +static pid_t pid; + +static const uchar utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +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}; + +ssize_t +xwrite(int fd, const char *s, size_t len) +{ + size_t aux = len; + ssize_t r; + + while (len > 0) { + r = write(fd, s, len); + if (r < 0) + return r; + len -= r; + s += r; + } + + return aux; +} + +void * +xmalloc(size_t len) +{ + void *p; + + if (!(p = malloc(len))) + die("malloc: %s\n", strerror(errno)); + + return p; +} + +void * +xrealloc(void *p, size_t len) +{ + if ((p = realloc(p, len)) == NULL) + die("realloc: %s\n", strerror(errno)); + + return p; +} + +char * +xstrdup(const char *s) +{ + char *p; + + if ((p = strdup(s)) == NULL) + die("strdup: %s\n", strerror(errno)); + + return p; +} + +size_t +utf8decode(const char *c, Rune *u, size_t clen) +{ + size_t i, j, len, type; + Rune udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type != 0) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Rune +utf8decodebyte(char c, size_t *i) +{ + for (*i = 0; *i < LEN(utfmask); ++(*i)) + if (((uchar)c & utfmask[*i]) == utfbyte[*i]) + return (uchar)c & ~utfmask[*i]; + + return 0; +} + +size_t +utf8encode(Rune u, char *c) +{ + size_t len, i; + + len = utf8validate(&u, 0); + if (len > UTF_SIZ) + return 0; + + for (i = len - 1; i != 0; --i) { + c[i] = utf8encodebyte(u, 0); + u >>= 6; + } + c[0] = utf8encodebyte(u, len); + + return len; +} + +char +utf8encodebyte(Rune u, size_t i) +{ + return utfbyte[i] | (u & ~utfmask[i]); +} + +size_t +utf8validate(Rune *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + + return i; +} + +char +base64dec_getc(const char **src) +{ + while (**src && !isprint((unsigned char)**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char * +base64dec(const char *src) +{ + size_t in_len = strlen(src); + char *result, *dst; + static const char base64_digits[256] = { + [43] = 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 + }; + + if (in_len % 4) + in_len += 4 - (in_len % 4); + result = dst = xmalloc(in_len / 4 * 3 + 1); + while (*src) { + int a = base64_digits[(unsigned char) base64dec_getc(&src)]; + int b = base64_digits[(unsigned char) base64dec_getc(&src)]; + int c = base64_digits[(unsigned char) base64dec_getc(&src)]; + int d = base64_digits[(unsigned char) base64dec_getc(&src)]; + + /* invalid input. 'a' can be -1, e.g. if src is "\n" (c-str) */ + 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'; + return result; +} + +void +selinit(void) +{ + sel.mode = SEL_IDLE; + sel.snap = 0; + sel.ob.x = -1; +} + +int +tlinelen(int y) +{ + int i = term.col; + + if (term.line[y][i - 1].mode & ATTR_WRAP) + return i; + + while (i > 0 && term.line[y][i - 1].u == ' ') + --i; + + return i; +} + +void +selstart(int col, int row, int snap) +{ + selclear(); + sel.mode = SEL_EMPTY; + sel.type = SEL_REGULAR; + sel.alt = IS_SET(MODE_ALTSCREEN); + sel.snap = snap; + sel.oe.x = sel.ob.x = col; + sel.oe.y = sel.ob.y = row; + selnormalize(); + + if (sel.snap != 0) + sel.mode = SEL_READY; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +selextend(int col, int row, int type, int done) +{ + int oldey, oldex, oldsby, oldsey, oldtype; + + if (sel.mode == SEL_IDLE) + return; + if (done && sel.mode == SEL_EMPTY) { + selclear(); + return; + } + + oldey = sel.oe.y; + oldex = sel.oe.x; + oldsby = sel.nb.y; + oldsey = sel.ne.y; + oldtype = sel.type; + + sel.oe.x = col; + sel.oe.y = row; + selnormalize(); + sel.type = type; + + if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY) + tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); + + sel.mode = done ? SEL_IDLE : SEL_READY; +} + +void +selnormalize(void) +{ + int i; + + if (sel.type == SEL_REGULAR && sel.ob.y != sel.oe.y) { + sel.nb.x = sel.ob.y < sel.oe.y ? sel.ob.x : sel.oe.x; + sel.ne.x = sel.ob.y < sel.oe.y ? sel.oe.x : sel.ob.x; + } else { + sel.nb.x = MIN(sel.ob.x, sel.oe.x); + sel.ne.x = MAX(sel.ob.x, sel.oe.x); + } + sel.nb.y = MIN(sel.ob.y, sel.oe.y); + sel.ne.y = MAX(sel.ob.y, sel.oe.y); + + selsnap(&sel.nb.x, &sel.nb.y, -1); + selsnap(&sel.ne.x, &sel.ne.y, +1); + + /* expand selection over line breaks */ + if (sel.type == SEL_RECTANGULAR) + return; + i = tlinelen(sel.nb.y); + if (i < sel.nb.x) + sel.nb.x = i; + if (tlinelen(sel.ne.y) <= sel.ne.x) + sel.ne.x = term.col - 1; +} + +int +selected(int x, int y) +{ + if (sel.mode == SEL_EMPTY || sel.ob.x == -1 || + sel.alt != IS_SET(MODE_ALTSCREEN)) + return 0; + + if (sel.type == SEL_RECTANGULAR) + return BETWEEN(y, sel.nb.y, sel.ne.y) + && BETWEEN(x, sel.nb.x, sel.ne.x); + + return BETWEEN(y, sel.nb.y, sel.ne.y) + && (y != sel.nb.y || x >= sel.nb.x) + && (y != sel.ne.y || x <= sel.ne.x); +} + +void +selsnap(int *x, int *y, int direction) +{ + int newx, newy, xt, yt; + int delim, prevdelim; + const Glyph *gp, *prevgp; + + switch (sel.snap) { + case SNAP_WORD: + /* + * Snap around if the word wraps around at the end or + * beginning of a line. + */ + prevgp = &term.line[*y][*x]; + prevdelim = ISDELIM(prevgp->u); + for (;;) { + newx = *x + direction; + newy = *y; + if (!BETWEEN(newx, 0, term.col - 1)) { + newy += direction; + newx = (newx + term.col) % term.col; + if (!BETWEEN(newy, 0, term.row - 1)) + break; + + if (direction > 0) + yt = *y, xt = *x; + else + yt = newy, xt = newx; + if (!(term.line[yt][xt].mode & ATTR_WRAP)) + break; + } + + if (newx >= tlinelen(newy)) + break; + + gp = &term.line[newy][newx]; + delim = ISDELIM(gp->u); + if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim + || (delim && gp->u != prevgp->u))) + break; + + *x = newx; + *y = newy; + prevgp = gp; + prevdelim = delim; + } + break; + case SNAP_LINE: + /* + * Snap around if the the previous line or the current one + * has set ATTR_WRAP at its end. Then the whole next or + * previous line will be selected. + */ + *x = (direction < 0) ? 0 : term.col - 1; + if (direction < 0) { + for (; *y > 0; *y += direction) { + if (!(term.line[*y-1][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } else if (direction > 0) { + for (; *y < term.row-1; *y += direction) { + if (!(term.line[*y][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } + break; + } +} + +char * +getsel(void) +{ + char *str, *ptr; + int y, bufsize, lastx, linelen; + const Glyph *gp, *last; + + if (sel.ob.x == -1) + return NULL; + + bufsize = (term.col+1) * (sel.ne.y-sel.nb.y+1) * UTF_SIZ; + ptr = str = xmalloc(bufsize); + + /* append every set & selected glyph to the selection */ + for (y = sel.nb.y; y <= sel.ne.y; y++) { + if ((linelen = tlinelen(y)) == 0) { + *ptr++ = '\n'; + continue; + } + + if (sel.type == SEL_RECTANGULAR) { + gp = &term.line[y][sel.nb.x]; + lastx = sel.ne.x; + } else { + gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0]; + lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; + } + last = &term.line[y][MIN(lastx, linelen-1)]; + while (last >= gp && last->u == ' ') + --last; + + for ( ; gp <= last; ++gp) { + if (gp->mode & ATTR_WDUMMY) + continue; + + ptr += utf8encode(gp->u, ptr); + } + + /* + * Copy and pasting of line endings is inconsistent + * in the inconsistent terminal and GUI world. + * The best solution seems like to produce '\n' when + * something is copied from st and convert '\n' to + * '\r', when something to be pasted is received by + * st. + * FIXME: Fix the computer world. + */ + if ((y < sel.ne.y || lastx >= linelen) && + (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR)) + *ptr++ = '\n'; + } + *ptr = 0; + return str; +} + +void +selclear(void) +{ + if (sel.ob.x == -1) + return; + sel.mode = SEL_IDLE; + sel.ob.x = -1; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +void +execsh(char *cmd, char **args) +{ + char *sh, *prog, *arg; + const struct passwd *pw; + + errno = 0; + if ((pw = getpwuid(getuid())) == NULL) { + if (errno) + die("getpwuid: %s\n", strerror(errno)); + else + die("who are you?\n"); + } + + if ((sh = getenv("SHELL")) == NULL) + sh = (pw->pw_shell[0]) ? pw->pw_shell : cmd; + + if (args) { + prog = args[0]; + arg = NULL; + } else if (scroll) { + prog = scroll; + arg = utmp ? utmp : sh; + } else if (utmp) { + prog = utmp; + arg = NULL; + } else { + prog = sh; + arg = NULL; + } + DEFAULT(args, ((char *[]) {prog, arg, NULL})); + + unsetenv("COLUMNS"); + unsetenv("LINES"); + unsetenv("TERMCAP"); + setenv("LOGNAME", pw->pw_name, 1); + setenv("USER", pw->pw_name, 1); + setenv("SHELL", sh, 1); + setenv("HOME", pw->pw_dir, 1); + setenv("TERM", termname, 1); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + execvp(prog, args); + _exit(1); +} + +void +sigchld(int a) +{ + int stat; + pid_t p; + + if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); + + if (pid != p) + return; + + if (WIFEXITED(stat) && WEXITSTATUS(stat)) + die("child exited with status %d\n", WEXITSTATUS(stat)); + else if (WIFSIGNALED(stat)) + die("child terminated due to signal %d\n", WTERMSIG(stat)); + _exit(0); +} + +void +stty(char **args) +{ + char cmd[_POSIX_ARG_MAX], **p, *q, *s; + size_t n, siz; + + if ((n = strlen(stty_args)) > sizeof(cmd)-1) + die("incorrect stty parameters\n"); + memcpy(cmd, stty_args, n); + q = cmd + n; + siz = sizeof(cmd) - n; + for (p = args; p && (s = *p); ++p) { + if ((n = strlen(s)) > siz-1) + die("stty parameter length too long\n"); + *q++ = ' '; + memcpy(q, s, n); + q += n; + siz -= n + 1; + } + *q = '\0'; + if (system(cmd) != 0) + perror("Couldn't call stty"); +} + +int +ttynew(const char *line, char *cmd, const char *out, char **args) +{ + int m, s; + + if (out) { + term.mode |= MODE_PRINT; + iofd = (!strcmp(out, "-")) ? + 1 : open(out, O_WRONLY | O_CREAT, 0666); + if (iofd < 0) { + fprintf(stderr, "Error opening %s:%s\n", + out, strerror(errno)); + } + } + + if (line) { + if ((cmdfd = open(line, O_RDWR)) < 0) + die("open line '%s' failed: %s\n", + line, strerror(errno)); + dup2(cmdfd, 0); + stty(args); + return cmdfd; + } + + /* seems to work fine on linux, openbsd and freebsd */ + if (openpty(&m, &s, NULL, NULL, NULL) < 0) + die("openpty failed: %s\n", strerror(errno)); + + switch (pid = fork()) { + case -1: + die("fork failed: %s\n", strerror(errno)); + break; + case 0: + close(iofd); + close(m); + setsid(); /* create a new process group */ + dup2(s, 0); + dup2(s, 1); + dup2(s, 2); + if (ioctl(s, TIOCSCTTY, NULL) < 0) + die("ioctl TIOCSCTTY failed: %s\n", strerror(errno)); + if (s > 2) + close(s); +#ifdef __OpenBSD__ + if (pledge("stdio getpw proc exec", NULL) == -1) + die("pledge\n"); +#endif + execsh(cmd, args); + break; + default: +#ifdef __OpenBSD__ + if (pledge("stdio rpath tty proc", NULL) == -1) + die("pledge\n"); +#endif + close(s); + cmdfd = m; + signal(SIGCHLD, sigchld); + break; + } + return cmdfd; +} + +size_t +ttyread(void) +{ + static char buf[BUFSIZ]; + static int buflen = 0; + int ret, written; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); + + switch (ret) { + case 0: + exit(0); + case -1: + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; + written = twrite(buf, buflen, 0); + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) + memmove(buf, buf + written, buflen); + return ret; + } +} + +void +ttywrite(const char *s, size_t n, int may_echo) +{ + const char *next; + + if (may_echo && IS_SET(MODE_ECHO)) + twrite(s, n, 1); + + if (!IS_SET(MODE_CRLF)) { + ttywriteraw(s, n); + return; + } + + /* This is similar to how the kernel handles ONLCR for ttys */ + while (n > 0) { + if (*s == '\r') { + next = s + 1; + ttywriteraw("\r\n", 2); + } else { + next = memchr(s, '\r', n); + DEFAULT(next, s + n); + ttywriteraw(s, next - s); + } + n -= next - s; + s = next; + } +} + +void +ttywriteraw(const char *s, size_t n) +{ + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; + + /* + * Remember that we are using a pty, which might be a modem line. + * Writing too much will clog the line. That's why we are doing this + * dance. + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); + FD_SET(cmdfd, &rfd); + + /* Check if we can write. */ + if (pselect(cmdfd+1, &rfd, &wfd, NULL, NULL, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + if (FD_ISSET(cmdfd, &wfd)) { + /* + * Only write the bytes written by ttywrite() or the + * default of 256. This seems to be a reasonable value + * for a serial line. Bigger values might clog the I/O. + */ + if ((r = write(cmdfd, s, (n < lim)? n : lim)) < 0) + goto write_error; + if (r < n) { + /* + * We weren't able to write out everything. + * This means the buffer is getting full + * again. Empty it. + */ + if (n < lim) + lim = ttyread(); + n -= r; + s += r; + } else { + /* All bytes have been written. */ + break; + } + } + if (FD_ISSET(cmdfd, &rfd)) + lim = ttyread(); + } + return; + +write_error: + die("write error on tty: %s\n", strerror(errno)); +} + +void +ttyresize(int tw, int th) +{ + struct winsize w; + + w.ws_row = term.row; + w.ws_col = term.col; + w.ws_xpixel = tw; + w.ws_ypixel = th; + if (ioctl(cmdfd, TIOCSWINSZ, &w) < 0) + fprintf(stderr, "Couldn't set window size: %s\n", strerror(errno)); +} + +void +ttyhangup(void) +{ + /* Send SIGHUP to shell */ + kill(pid, SIGHUP); +} + +int +tattrset(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) + return 1; + } + } + + return 0; +} + +void +tsetdirt(int top, int bot) +{ + int i; + + if (term.row <= 0) + return; + + LIMIT(top, 0, term.row-1); + LIMIT(bot, 0, term.row-1); + + for (i = top; i <= bot; i++) + term.dirty[i] = 1; +} + +void +tsetdirtattr(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) { + tsetdirt(i, i); + break; + } + } + } +} + +void +tfulldirt(void) +{ + tsetdirt(0, term.row-1); +} + +void +tcursor(int mode) +{ + static TCursor c[2]; + int alt = IS_SET(MODE_ALTSCREEN); + + if (mode == CURSOR_SAVE) { + c[alt] = term.c; + } else if (mode == CURSOR_LOAD) { + term.c = c[alt]; + tmoveto(c[alt].x, c[alt].y); + } +} + +void +treset(void) +{ + uint i; + + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, + .bg = defaultbg + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + for (i = tabspaces; i < term.col; i += tabspaces) + term.tabs[i] = 1; + term.top = 0; + term.bot = term.row - 1; + term.mode = MODE_WRAP|MODE_UTF8; + memset(term.trantbl, CS_USA, sizeof(term.trantbl)); + 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(); + } +} + +void +tnew(int col, int row) +{ + term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + tresize(col, row); + treset(); +} + +void +tswapscreen(void) +{ + Line *tmp = term.line; + + term.line = term.alt; + term.alt = tmp; + term.mode ^= MODE_ALTSCREEN; + tfulldirt(); +} + +void +tscrolldown(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + 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; + } + + selscroll(orig, n); +} + +void +tscrollup(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+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; + } + + selscroll(orig, -n); +} + +void +selscroll(int orig, int n) +{ + if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) + return; + + if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) { + selclear(); + } else if (BETWEEN(sel.nb.y, orig, term.bot)) { + sel.ob.y += n; + sel.oe.y += n; + if (sel.ob.y < term.top || sel.ob.y > term.bot || + sel.oe.y < term.top || sel.oe.y > term.bot) { + selclear(); + } else { + selnormalize(); + } + } +} + +void +tnewline(int first_col) +{ + int y = term.c.y; + + if (y == term.bot) { + tscrollup(term.top, 1); + } else { + y++; + } + tmoveto(first_col ? 0 : term.c.x, y); +} + +void +csiparse(void) +{ + char *p = csiescseq.buf, *np; + long int v; + int sep = ';'; /* colon or semi-colon, but not both */ + + csiescseq.narg = 0; + if (*p == '?') { + csiescseq.priv = 1; + p++; + } + + csiescseq.buf[csiescseq.len] = '\0'; + while (p < csiescseq.buf+csiescseq.len) { + np = NULL; + v = strtol(p, &np, 10); + if (np == p) + v = 0; + if (v == LONG_MAX || v == LONG_MIN) + v = -1; + csiescseq.arg[csiescseq.narg++] = v; + p = np; + if (sep == ';' && *p == ':') + sep = ':'; /* allow override to colon once */ + if (*p != sep || csiescseq.narg == ESC_ARG_SIZ) + break; + p++; + } + csiescseq.mode[0] = *p++; + csiescseq.mode[1] = (p < csiescseq.buf+csiescseq.len) ? *p : '\0'; +} + +/* for absolute user moves, when decom is set */ +void +tmoveato(int x, int y) +{ + tmoveto(x, y + ((term.c.state & CURSOR_ORIGIN) ? term.top: 0)); +} + +void +tmoveto(int x, int y) +{ + int miny, maxy; + + if (term.c.state & CURSOR_ORIGIN) { + miny = term.top; + maxy = term.bot; + } else { + miny = 0; + maxy = term.row - 1; + } + term.c.state &= ~CURSOR_WRAPNEXT; + term.c.x = LIMIT(x, 0, term.col-1); + term.c.y = LIMIT(y, miny, maxy); +} + +void +tsetchar(Rune u, const Glyph *attr, int x, int y) +{ + static const char *vt100_0[62] = { /* 0x41 - 0x7e */ + "↑", "↓", "→", "←", "█", "▚", "☃", /* A - G */ + 0, 0, 0, 0, 0, 0, 0, 0, /* H - O */ + 0, 0, 0, 0, 0, 0, 0, 0, /* P - W */ + 0, 0, 0, 0, 0, 0, 0, " ", /* X - _ */ + "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", /* ` - g */ + "␤", "␋", "┘", "┐", "┌", "└", "┼", "⎺", /* h - o */ + "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */ + "│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */ + }; + + /* + * The table is proudly stolen from rxvt. + */ + if (term.trantbl[term.charset] == CS_GRAPHIC0 && + 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 (x+1 < term.col) { + term.line[y][x+1].u = ' '; + term.line[y][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; + } + + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; +} + +void +tclearregion(int x1, int y1, int x2, int y2) +{ + int x, y, temp; + Glyph *gp; + + if (x1 > x2) + temp = x1, x1 = x2, x2 = temp; + if (y1 > y2) + temp = y1, y1 = y2, y2 = temp; + + LIMIT(x1, 0, term.col-1); + LIMIT(x2, 0, term.col-1); + LIMIT(y1, 0, term.row-1); + LIMIT(y2, 0, term.row-1); + + for (y = y1; y <= y2; y++) { + term.dirty[y] = 1; + for (x = x1; x <= x2; x++) { + gp = &term.line[y][x]; + if (selected(x, y)) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; + gp->mode = 0; + gp->u = ' '; + } + } +} + +void +tdeletechar(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x; + src = term.c.x + n; + size = term.col - src; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(term.col-n, term.c.y, term.col-1, term.c.y); +} + +void +tinsertblank(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x + n; + src = term.c.x; + size = term.col - dst; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(src, term.c.y, dst - 1, term.c.y); +} + +void +tinsertblankline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrolldown(term.c.y, n); +} + +void +tdeleteline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrollup(term.c.y, n); +} + +int32_t +tdefcolor(const int *attr, int *npar, int l) +{ + int32_t idx = -1; + uint r, g, b; + + switch (attr[*npar + 1]) { + case 2: /* direct color in RGB space */ + if (*npar + 4 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + if (!BETWEEN(r, 0, 255) || !BETWEEN(g, 0, 255) || !BETWEEN(b, 0, 255)) + fprintf(stderr, "erresc: bad rgb color (%u,%u,%u)\n", + r, g, b); + else + idx = TRUECOLOR(r, g, b); + break; + case 5: /* indexed color */ + if (*npar + 2 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + *npar += 2; + if (!BETWEEN(attr[*npar], 0, 255)) + fprintf(stderr, "erresc: bad fgcolor %d\n", attr[*npar]); + else + idx = attr[*npar]; + break; + case 0: /* implemented defined (only foreground) */ + case 1: /* transparent */ + case 3: /* direct color in CMY space */ + case 4: /* direct color in CMYK space */ + default: + fprintf(stderr, + "erresc(38): gfx attr %d unknown\n", attr[*npar]); + break; + } + + return idx; +} + +void +tsetattr(const int *attr, int l) +{ + int i; + int32_t idx; + + for (i = 0; i < l; i++) { + switch (attr[i]) { + case 0: + term.c.attr.mode &= ~( + ATTR_BOLD | + ATTR_FAINT | + ATTR_ITALIC | + ATTR_UNDERLINE | + ATTR_BLINK | + ATTR_REVERSE | + ATTR_INVISIBLE | + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; + break; + case 2: + term.c.attr.mode |= ATTR_FAINT; + break; + case 3: + term.c.attr.mode |= ATTR_ITALIC; + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ + case 6: /* rapid blink */ + term.c.attr.mode |= ATTR_BLINK; + break; + case 7: + term.c.attr.mode |= ATTR_REVERSE; + break; + case 8: + term.c.attr.mode |= ATTR_INVISIBLE; + break; + case 9: + term.c.attr.mode |= ATTR_STRUCK; + break; + case 22: + term.c.attr.mode &= ~(ATTR_BOLD | ATTR_FAINT); + break; + case 23: + term.c.attr.mode &= ~ATTR_ITALIC; + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; + break; + case 27: + term.c.attr.mode &= ~ATTR_REVERSE; + break; + case 28: + term.c.attr.mode &= ~ATTR_INVISIBLE; + break; + case 29: + term.c.attr.mode &= ~ATTR_STRUCK; + break; + case 38: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.fg = idx; + break; + case 39: /* set foreground color to default */ + term.c.attr.fg = defaultfg; + break; + case 48: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.bg = idx; + break; + case 49: /* set background color to default */ + term.c.attr.bg = defaultbg; + break; + case 58: + /* This starts a sequence to change the color of + * "underline" pixels. We don't support that and + * instead eat up a following "5;n" or "2;r;g;b". */ + tdefcolor(attr, &i, l); + break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; + } else if (BETWEEN(attr[i], 40, 47)) { + term.c.attr.bg = attr[i] - 40; + } else if (BETWEEN(attr[i], 90, 97)) { + term.c.attr.fg = attr[i] - 90 + 8; + } else if (BETWEEN(attr[i], 100, 107)) { + term.c.attr.bg = attr[i] - 100 + 8; + } else { + fprintf(stderr, + "erresc(default): gfx attr %d unknown\n", + attr[i]); + csidump(); + } + break; + } + } +} + +void +tsetscroll(int t, int b) +{ + int temp; + + LIMIT(t, 0, term.row-1); + LIMIT(b, 0, term.row-1); + if (t > b) { + temp = t; + t = b; + b = temp; + } + term.top = t; + term.bot = b; +} + +void +tsetmode(int priv, int set, const int *args, int narg) +{ + int alt; const int *lim; + + for (lim = args + narg; args < lim; ++args) { + if (priv) { + switch (*args) { + case 1: /* DECCKM -- Cursor key */ + xsetmode(set, MODE_APPCURSOR); + break; + case 5: /* DECSCNM -- Reverse video */ + xsetmode(set, MODE_REVERSE); + break; + case 6: /* DECOM -- Origin */ + MODBIT(term.c.state, set, CURSOR_ORIGIN); + tmoveato(0, 0); + break; + case 7: /* DECAWM -- Auto wrap */ + MODBIT(term.mode, set, MODE_WRAP); + break; + case 0: /* Error (IGNORED) */ + case 2: /* DECANM -- ANSI/VT52 (IGNORED) */ + case 3: /* DECCOLM -- Column (IGNORED) */ + case 4: /* DECSCLM -- Scroll (IGNORED) */ + case 8: /* DECARM -- Auto repeat (IGNORED) */ + case 18: /* DECPFF -- Printer feed (IGNORED) */ + case 19: /* DECPEX -- Printer extent (IGNORED) */ + case 42: /* DECNRCM -- National characters (IGNORED) */ + case 12: /* att610 -- Start blinking cursor (IGNORED) */ + break; + case 25: /* DECTCEM -- Text Cursor Enable Mode */ + xsetmode(!set, MODE_HIDE); + break; + case 9: /* X10 mouse compatibility mode */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEX10); + break; + case 1000: /* 1000: report button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEBTN); + break; + case 1002: /* 1002: report motion on button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMOTION); + break; + case 1003: /* 1003: enable all mouse motions */ + xsetpointermotion(set); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMANY); + break; + case 1004: /* 1004: send focus events to tty */ + xsetmode(set, MODE_FOCUS); + break; + case 1006: /* 1006: extended reporting mode */ + xsetmode(set, MODE_MOUSESGR); + break; + case 1034: /* 1034: enable 8-bit mode for keyboard input */ + xsetmode(set, MODE_8BIT); + break; + case 1049: /* swap screen & set/restore cursor as xterm */ + if (!allowaltscreen) + break; + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + /* FALLTHROUGH */ + case 47: /* swap screen buffer */ + case 1047: /* swap screen buffer */ + if (!allowaltscreen) + break; + alt = IS_SET(MODE_ALTSCREEN); + if (alt) { + tclearregion(0, 0, term.col-1, + term.row-1); + } + if (set ^ alt) /* set is always 1 or 0 */ + tswapscreen(); + if (*args != 1049) + break; + /* FALLTHROUGH */ + case 1048: /* save/restore cursor (like DECSC/DECRC) */ + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + break; + case 2004: /* 2004: bracketed paste mode */ + xsetmode(set, MODE_BRCKTPASTE); + break; + /* Not implemented mouse modes. See comments there. */ + case 1001: /* mouse highlight mode; can hang the + terminal by design when implemented. */ + case 1005: /* UTF-8 mouse mode; will confuse + applications not supporting UTF-8 + and luit. */ + case 1015: /* urxvt mangled mouse mode; incompatible + and can be mistaken for other control + codes. */ + break; + default: + fprintf(stderr, + "erresc: unknown private set/reset mode %d\n", + *args); + break; + } + } else { + switch (*args) { + case 0: /* Error (IGNORED) */ + break; + case 2: + xsetmode(set, MODE_KBDLOCK); + break; + case 4: /* IRM -- Insertion-replacement */ + MODBIT(term.mode, set, MODE_INSERT); + break; + case 12: /* SRM -- Send/Receive */ + MODBIT(term.mode, !set, MODE_ECHO); + break; + case 20: /* LNM -- Linefeed/new line */ + MODBIT(term.mode, set, MODE_CRLF); + break; + default: + fprintf(stderr, + "erresc: unknown set/reset mode %d\n", + *args); + break; + } + } + } +} + +void +csihandle(void) +{ + char buf[40]; + int len; + + switch (csiescseq.mode[0]) { + default: + unknown: + fprintf(stderr, "erresc: unknown csi "); + csidump(); + /* die(""); */ + break; + case '@': /* ICH -- Insert blank char */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblank(csiescseq.arg[0]); + break; + case 'A': /* CUU -- Cursor Up */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y-csiescseq.arg[0]); + break; + case 'B': /* CUD -- Cursor Down */ + case 'e': /* VPR --Cursor Down */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y+csiescseq.arg[0]); + break; + case 'i': /* MC -- Media Copy */ + switch (csiescseq.arg[0]) { + case 0: + tdump(); + break; + case 1: + tdumpline(term.c.y); + break; + case 2: + tdumpsel(); + break; + case 4: + term.mode &= ~MODE_PRINT; + break; + case 5: + term.mode |= MODE_PRINT; + break; + } + break; + case 'c': /* DA -- Device Attributes */ + if (csiescseq.arg[0] == 0) + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'b': /* REP -- if last char is printable print it more times */ + LIMIT(csiescseq.arg[0], 1, 65535); + if (term.lastc) + while (csiescseq.arg[0]-- > 0) + tputc(term.lastc); + break; + case 'C': /* CUF -- Cursor Forward */ + case 'a': /* HPR -- Cursor Forward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x+csiescseq.arg[0], term.c.y); + break; + case 'D': /* CUB -- Cursor Backward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x-csiescseq.arg[0], term.c.y); + break; + case 'E': /* CNL -- Cursor Down and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y+csiescseq.arg[0]); + break; + case 'F': /* CPL -- Cursor Up and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y-csiescseq.arg[0]); + break; + case 'g': /* TBC -- Tabulation clear */ + switch (csiescseq.arg[0]) { + case 0: /* clear current tab stop */ + term.tabs[term.c.x] = 0; + break; + case 3: /* clear all the tabs */ + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + break; + default: + goto unknown; + } + break; + case 'G': /* CHA -- Move to */ + case '`': /* HPA */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(csiescseq.arg[0]-1, term.c.y); + break; + case 'H': /* CUP -- Move to */ + case 'f': /* HVP */ + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], 1); + tmoveato(csiescseq.arg[1]-1, csiescseq.arg[0]-1); + break; + case 'I': /* CHT -- Cursor Forward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(csiescseq.arg[0]); + break; + case 'J': /* ED -- Clear screen */ + switch (csiescseq.arg[0]) { + case 0: /* below */ + tclearregion(term.c.x, term.c.y, term.col-1, term.c.y); + if (term.c.y < term.row-1) { + tclearregion(0, term.c.y+1, term.col-1, + term.row-1); + } + break; + case 1: /* above */ + 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; + case 2: /* all */ + tclearregion(0, 0, term.col-1, term.row-1); + break; + default: + goto unknown; + } + break; + case 'K': /* EL -- Clear line */ + switch (csiescseq.arg[0]) { + case 0: /* right */ + tclearregion(term.c.x, term.c.y, term.col-1, + term.c.y); + break; + case 1: /* left */ + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, term.c.y, term.col-1, term.c.y); + break; + } + break; + case 'S': /* SU -- Scroll line up */ + if (csiescseq.priv) break; + DEFAULT(csiescseq.arg[0], 1); + tscrollup(term.top, csiescseq.arg[0]); + break; + case 'T': /* SD -- Scroll line down */ + DEFAULT(csiescseq.arg[0], 1); + tscrolldown(term.top, csiescseq.arg[0]); + break; + case 'L': /* IL -- Insert blank lines */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblankline(csiescseq.arg[0]); + break; + case 'l': /* RM -- Reset Mode */ + tsetmode(csiescseq.priv, 0, csiescseq.arg, csiescseq.narg); + break; + case 'M': /* DL -- Delete lines */ + DEFAULT(csiescseq.arg[0], 1); + tdeleteline(csiescseq.arg[0]); + break; + case 'X': /* ECH -- Erase char */ + DEFAULT(csiescseq.arg[0], 1); + tclearregion(term.c.x, term.c.y, + term.c.x + csiescseq.arg[0] - 1, term.c.y); + break; + case 'P': /* DCH -- Delete char */ + DEFAULT(csiescseq.arg[0], 1); + tdeletechar(csiescseq.arg[0]); + break; + case 'Z': /* CBT -- Cursor Backward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(-csiescseq.arg[0]); + break; + case 'd': /* VPA -- Move to */ + DEFAULT(csiescseq.arg[0], 1); + tmoveato(term.c.x, csiescseq.arg[0]-1); + break; + case 'h': /* SM -- Set terminal mode */ + tsetmode(csiescseq.priv, 1, csiescseq.arg, csiescseq.narg); + break; + case 'm': /* SGR -- Terminal attribute (color) */ + tsetattr(csiescseq.arg, csiescseq.narg); + break; + case 'n': /* DSR -- Device Status Report */ + switch (csiescseq.arg[0]) { + case 5: /* Status Report "OK" `0n` */ + ttywrite("\033[0n", sizeof("\033[0n") - 1, 0); + break; + case 6: /* Report Cursor Position (CPR) ";R" */ + len = snprintf(buf, sizeof(buf), "\033[%i;%iR", + term.c.y+1, term.c.x+1); + ttywrite(buf, len, 0); + break; + default: + goto unknown; + } + break; + case 'r': /* DECSTBM -- Set Scrolling Region */ + if (csiescseq.priv) { + goto unknown; + } else { + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], term.row); + tsetscroll(csiescseq.arg[0]-1, csiescseq.arg[1]-1); + tmoveato(0, 0); + } + break; + case 's': /* DECSC -- Save cursor position (ANSI.SYS) */ + tcursor(CURSOR_SAVE); + break; + case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ + if (csiescseq.priv) { + goto unknown; + } else { + tcursor(CURSOR_LOAD); + } + break; + case ' ': + switch (csiescseq.mode[1]) { + case 'q': /* DECSCUSR -- Set Cursor Style */ + if (xsetcursor(csiescseq.arg[0])) + goto unknown; + break; + default: + goto unknown; + } + break; + } +} + +void +csidump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC["); + for (i = 0; i < csiescseq.len; i++) { + c = csiescseq.buf[i] & 0xff; + if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + putc('\n', stderr); +} + +void +csireset(void) +{ + memset(&csiescseq, 0, sizeof(csiescseq)); +} + +void +osc_color_response(int num, int index, int is_osc4) +{ + int n; + char buf[32]; + unsigned char r, g, b; + + if (xgetcolor(is_osc4 ? num : index, &r, &g, &b)) { + fprintf(stderr, "erresc: failed to fetch %s color %d\n", + is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index); + return; + } + + n = snprintf(buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", + is_osc4 ? "4;" : "", num, r, r, g, g, b, b); + if (n < 0 || n >= sizeof(buf)) { + fprintf(stderr, "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc"); + } else { + ttywrite(buf, n, 1); + } +} + +void +strhandle(void) +{ + char *p = NULL, *dec; + int j, narg, par; + const struct { int idx; char *str; } osc_table[] = { + { defaultfg, "foreground" }, + { defaultbg, "background" }, + { defaultcs, "cursor" } + }; + + term.esc &= ~(ESC_STR_END|ESC_STR); + strparse(); + par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0; + + switch (strescseq.type) { + case ']': /* OSC -- Operating System Command */ + switch (par) { + case 0: + if (narg > 1) { + xsettitle(strescseq.args[1]); + xseticontitle(strescseq.args[1]); + } + return; + case 1: + if (narg > 1) + xseticontitle(strescseq.args[1]); + return; + case 2: + if (narg > 1) + xsettitle(strescseq.args[1]); + return; + case 52: /* manipulate selection data */ + if (narg > 2 && allowwindowops) { + dec = base64dec(strescseq.args[2]); + if (dec) { + xsetsel(dec); + xclipcopy(); + } else { + fprintf(stderr, "erresc: invalid base64\n"); + } + } + return; + case 10: /* set dynamic VT100 text foreground color */ + case 11: /* set dynamic VT100 text background color */ + case 12: /* set dynamic text cursor color */ + if (narg < 2) + break; + p = strescseq.args[1]; + if ((j = par - 10) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + + if (!strcmp(p, "?")) { + osc_color_response(par, osc_table[j].idx, 0); + } else if (xsetcolorname(osc_table[j].idx, p)) { + fprintf(stderr, "erresc: invalid %s color: %s\n", + osc_table[j].str, p); + } else { + tfulldirt(); + } + return; + case 4: /* color set */ + if (narg < 3) + break; + p = strescseq.args[2]; + /* FALLTHROUGH */ + case 104: /* color reset */ + j = (narg > 1) ? atoi(strescseq.args[1]) : -1; + + if (p && !strcmp(p, "?")) { + osc_color_response(j, 0, 1); + } else if (xsetcolorname(j, p)) { + if (par == 104 && narg <= 1) { + xloadcols(); + return; /* color reset without parameter */ + } + fprintf(stderr, "erresc: invalid color j=%d, p=%s\n", + j, p ? p : "(null)"); + } else { + /* + * TODO if defaultbg color is changed, borders + * are dirty + */ + tfulldirt(); + } + return; + case 110: /* reset dynamic VT100 text foreground color */ + case 111: /* reset dynamic VT100 text background color */ + case 112: /* reset dynamic text cursor color */ + if (narg != 1) + break; + if ((j = par - 110) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + if (xsetcolorname(osc_table[j].idx, NULL)) { + fprintf(stderr, "erresc: %s color not found\n", osc_table[j].str); + } else { + tfulldirt(); + } + return; + } + break; + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + return; + } + + fprintf(stderr, "erresc: unknown str "); + strdump(); +} + +void +strparse(void) +{ + int c; + char *p = strescseq.buf; + + strescseq.narg = 0; + strescseq.buf[strescseq.len] = '\0'; + + if (*p == '\0') + return; + + while (strescseq.narg < STR_ARG_SIZ) { + strescseq.args[strescseq.narg++] = p; + while ((c = *p) != ';' && c != '\0') + ++p; + if (c == '\0') + return; + *p++ = '\0'; + } +} + +void +strdump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC%c", strescseq.type); + for (i = 0; i < strescseq.len; i++) { + c = strescseq.buf[i] & 0xff; + if (c == '\0') { + putc('\n', stderr); + return; + } else if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + fprintf(stderr, "ESC\\\n"); +} + +void +strreset(void) +{ + strescseq = (STREscape){ + .buf = xrealloc(strescseq.buf, STR_BUF_SIZ), + .siz = STR_BUF_SIZ, + }; +} + +void +sendbreak(const Arg *arg) +{ + if (tcsendbreak(cmdfd, 0)) + perror("Error sending break"); +} + +void +tprinter(char *s, size_t len) +{ + if (iofd != -1 && xwrite(iofd, s, len) < 0) { + perror("Error writing to output file"); + close(iofd); + iofd = -1; + } +} + +void +toggleprinter(const Arg *arg) +{ + term.mode ^= MODE_PRINT; +} + +void +printscreen(const Arg *arg) +{ + tdump(); +} + +void +printsel(const Arg *arg) +{ + tdumpsel(); +} + +void +tdumpsel(void) +{ + char *ptr; + + if ((ptr = getsel())) { + tprinter(ptr, strlen(ptr)); + free(ptr); + } +} + +void +tdumpline(int n) +{ + char buf[UTF_SIZ]; + const Glyph *bp, *end; + + bp = &term.line[n][0]; + end = &bp[MIN(tlinelen(n), term.col) - 1]; + if (bp != end || bp->u != ' ') { + for ( ; bp <= end; ++bp) + tprinter(buf, utf8encode(bp->u, buf)); + } + tprinter("\n", 1); +} + +void +tdump(void) +{ + int i; + + for (i = 0; i < term.row; ++i) + tdumpline(i); +} + +void +tputtab(int n) +{ + uint x = term.c.x; + + if (n > 0) { + while (x < term.col && n--) + for (++x; x < term.col && !term.tabs[x]; ++x) + /* nothing */ ; + } else if (n < 0) { + while (x > 0 && n++) + for (--x; x > 0 && !term.tabs[x]; --x) + /* nothing */ ; + } + term.c.x = LIMIT(x, 0, term.col-1); +} + +void +tdefutf8(char ascii) +{ + if (ascii == 'G') + term.mode |= MODE_UTF8; + else if (ascii == '@') + term.mode &= ~MODE_UTF8; +} + +void +tdeftran(char ascii) +{ + static char cs[] = "0B"; + static int vcs[] = {CS_GRAPHIC0, CS_USA}; + char *p; + + if ((p = strchr(cs, ascii)) == NULL) { + fprintf(stderr, "esc unhandled charset: ESC ( %c\n", ascii); + } else { + term.trantbl[term.icharset] = vcs[p - cs]; + } +} + +void +tdectest(char c) +{ + int x, y; + + if (c == '8') { /* DEC screen alignment test. */ + for (x = 0; x < term.col; ++x) { + for (y = 0; y < term.row; ++y) + tsetchar('E', &term.c.attr, x, y); + } + } +} + +void +tstrsequence(uchar c) +{ + switch (c) { + case 0x90: /* DCS -- Device Control String */ + c = 'P'; + break; + case 0x9f: /* APC -- Application Program Command */ + c = '_'; + break; + case 0x9e: /* PM -- Privacy Message */ + c = '^'; + break; + case 0x9d: /* OSC -- Operating System Command */ + c = ']'; + break; + } + strreset(); + strescseq.type = c; + term.esc |= ESC_STR; +} + +void +tcontrolcode(uchar ascii) +{ + switch (ascii) { + case '\t': /* HT */ + tputtab(1); + return; + case '\b': /* BS */ + tmoveto(term.c.x-1, term.c.y); + return; + case '\r': /* CR */ + tmoveto(0, term.c.y); + return; + case '\f': /* LF */ + case '\v': /* VT */ + case '\n': /* LF */ + /* go to first col if the mode is set */ + tnewline(IS_SET(MODE_CRLF)); + return; + case '\a': /* BEL */ + if (term.esc & ESC_STR_END) { + /* backwards compatibility to xterm */ + strhandle(); + } else { + xbell(); + } + break; + case '\033': /* ESC */ + csireset(); + term.esc &= ~(ESC_CSI|ESC_ALTCHARSET|ESC_TEST); + term.esc |= ESC_START; + return; + case '\016': /* SO (LS1 -- Locking shift 1) */ + case '\017': /* SI (LS0 -- Locking shift 0) */ + term.charset = 1 - (ascii - '\016'); + return; + case '\032': /* SUB */ + tsetchar('?', &term.c.attr, term.c.x, term.c.y); + /* FALLTHROUGH */ + case '\030': /* CAN */ + csireset(); + break; + case '\005': /* ENQ (IGNORED) */ + case '\000': /* NUL (IGNORED) */ + case '\021': /* XON (IGNORED) */ + case '\023': /* XOFF (IGNORED) */ + case 0177: /* DEL (IGNORED) */ + return; + case 0x80: /* TODO: PAD */ + case 0x81: /* TODO: HOP */ + case 0x82: /* TODO: BPH */ + case 0x83: /* TODO: NBH */ + case 0x84: /* TODO: IND */ + break; + case 0x85: /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 0x86: /* TODO: SSA */ + case 0x87: /* TODO: ESA */ + break; + case 0x88: /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 0x89: /* TODO: HTJ */ + case 0x8a: /* TODO: VTS */ + case 0x8b: /* TODO: PLD */ + case 0x8c: /* TODO: PLU */ + case 0x8d: /* TODO: RI */ + case 0x8e: /* TODO: SS2 */ + case 0x8f: /* TODO: SS3 */ + case 0x91: /* TODO: PU1 */ + case 0x92: /* TODO: PU2 */ + case 0x93: /* TODO: STS */ + case 0x94: /* TODO: CCH */ + case 0x95: /* TODO: MW */ + case 0x96: /* TODO: SPA */ + case 0x97: /* TODO: EPA */ + case 0x98: /* TODO: SOS */ + case 0x99: /* TODO: SGCI */ + break; + case 0x9a: /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 0x9b: /* TODO: CSI */ + case 0x9c: /* TODO: ST */ + break; + case 0x90: /* DCS -- Device Control String */ + case 0x9d: /* OSC -- Operating System Command */ + case 0x9e: /* PM -- Privacy Message */ + case 0x9f: /* APC -- Application Program Command */ + tstrsequence(ascii); + return; + } + /* only CAN, SUB, \a and C1 chars interrupt a sequence */ + term.esc &= ~(ESC_STR_END|ESC_STR); +} + +/* + * returns 1 when the sequence is finished and it hasn't to read + * more characters for this sequence, otherwise 0 + */ +int +eschandle(uchar ascii) +{ + switch (ascii) { + case '[': + term.esc |= ESC_CSI; + return 0; + case '#': + term.esc |= ESC_TEST; + return 0; + case '%': + term.esc |= ESC_UTF8; + return 0; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + case ']': /* OSC -- Operating System Command */ + case 'k': /* old title set compatibility */ + tstrsequence(ascii); + return 0; + case 'n': /* LS2 -- Locking shift 2 */ + case 'o': /* LS3 -- Locking shift 3 */ + term.charset = 2 + (ascii - 'n'); + break; + case '(': /* GZD4 -- set primary charset G0 */ + case ')': /* G1D4 -- set secondary charset G1 */ + case '*': /* G2D4 -- set tertiary charset G2 */ + case '+': /* G3D4 -- set quaternary charset G3 */ + term.icharset = ascii - '('; + term.esc |= ESC_ALTCHARSET; + return 0; + case 'D': /* IND -- Linefeed */ + if (term.c.y == term.bot) { + tscrollup(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y+1); + } + break; + case 'E': /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 'H': /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 'M': /* RI -- Reverse index */ + if (term.c.y == term.top) { + tscrolldown(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y-1); + } + break; + case 'Z': /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'c': /* RIS -- Reset to initial state */ + treset(); + resettitle(); + xloadcols(); + xsetmode(0, MODE_HIDE); + xsetmode(0, MODE_BRCKTPASTE); + break; + case '=': /* DECPAM -- Application keypad */ + xsetmode(1, MODE_APPKEYPAD); + break; + case '>': /* DECPNM -- Normal keypad */ + xsetmode(0, MODE_APPKEYPAD); + break; + case '7': /* DECSC -- Save Cursor */ + tcursor(CURSOR_SAVE); + break; + case '8': /* DECRC -- Restore Cursor */ + tcursor(CURSOR_LOAD); + break; + case '\\': /* ST -- String Terminator */ + if (term.esc & ESC_STR_END) + strhandle(); + break; + default: + fprintf(stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", + (uchar) ascii, isprint(ascii)? ascii:'.'); + break; + } + return 1; +} + +void +tputc(Rune u) +{ + char c[UTF_SIZ]; + int control; + int width, len; + Glyph *gp; + + control = ISCONTROL(u); + if (u < 127 || !IS_SET(MODE_UTF8)) { + c[0] = u; + width = len = 1; + } else { + len = utf8encode(u, c); + if (!control && (width = wcwidth(u)) == -1) + width = 1; + } + + if (IS_SET(MODE_PRINT)) + tprinter(c, len); + + /* + * STR sequence must be checked before anything else + * because it uses all following characters until it + * receives a ESC, a SUB, a ST or any other C1 control + * character. + */ + if (term.esc & ESC_STR) { + if (u == '\a' || u == 030 || u == 032 || u == 033 || + ISCONTROLC1(u)) { + term.esc &= ~(ESC_START|ESC_STR); + term.esc |= ESC_STR_END; + goto check_control_code; + } + + if (strescseq.len+len >= strescseq.siz) { + /* + * Here is a bug in terminals. If the user never sends + * some code to stop the str or esc command, then st + * will stop responding. But this is better than + * silently failing with unknown characters. At least + * then users will report back. + * + * In the case users ever get fixed, here is the code: + */ + /* + * term.esc = 0; + * strhandle(); + */ + if (strescseq.siz > (SIZE_MAX - UTF_SIZ) / 2) + return; + strescseq.siz *= 2; + strescseq.buf = xrealloc(strescseq.buf, strescseq.siz); + } + + memmove(&strescseq.buf[strescseq.len], c, len); + strescseq.len += len; + return; + } + +check_control_code: + /* + * Actions of control codes must be performed as soon they arrive + * because they can be embedded inside a control sequence, and + * they must not cause conflicts with sequences. + */ + if (control) { + /* in UTF-8 mode ignore handling C1 control characters */ + if (IS_SET(MODE_UTF8) && ISCONTROLC1(u)) + return; + tcontrolcode(u); + /* + * control codes are not shown ever + */ + if (!term.esc) + term.lastc = 0; + return; + } else if (term.esc & ESC_START) { + if (term.esc & ESC_CSI) { + csiescseq.buf[csiescseq.len++] = u; + if (BETWEEN(u, 0x40, 0x7E) + || csiescseq.len >= \ + sizeof(csiescseq.buf)-1) { + term.esc = 0; + csiparse(); + csihandle(); + } + return; + } else if (term.esc & ESC_UTF8) { + tdefutf8(u); + } else if (term.esc & ESC_ALTCHARSET) { + tdeftran(u); + } else if (term.esc & ESC_TEST) { + tdectest(u); + } else { + if (!eschandle(u)) + return; + /* sequence already finished */ + } + term.esc = 0; + /* + * All characters which form part of a sequence are not + * printed + */ + return; + } + if (selected(term.c.x, term.c.y)) + selclear(); + + gp = &term.line[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]; + } + + if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) { + memmove(gp+width, gp, (term.col - term.c.x - width) * sizeof(Glyph)); + gp->mode &= ~ATTR_WIDE; + } + + if (term.c.x+width > term.col) { + if (IS_SET(MODE_WRAP)) + tnewline(1); + else + tmoveto(term.col - width, term.c.y); + gp = &term.line[term.c.y][term.c.x]; + } + + tsetchar(u, &term.c.attr, term.c.x, term.c.y); + term.lastc = u; + + if (width == 2) { + gp->mode |= ATTR_WIDE; + if (term.c.x+1 < term.col) { + if (gp[1].mode == ATTR_WIDE && term.c.x+2 < term.col) { + gp[2].u = ' '; + gp[2].mode &= ~ATTR_WDUMMY; + } + gp[1].u = '\0'; + gp[1].mode = ATTR_WDUMMY; + } + } + if (term.c.x+width < term.col) { + tmoveto(term.c.x+width, term.c.y); + } else { + term.c.state |= CURSOR_WRAPNEXT; + } +} + +int +twrite(const char *buf, int buflen, int show_ctrl) +{ + int charsize; + Rune u; + int n; + + for (n = 0; n < buflen; n += charsize) { + if (IS_SET(MODE_UTF8)) { + /* process a complete utf8 char */ + charsize = utf8decode(buf + n, &u, buflen - n); + if (charsize == 0) + break; + } else { + u = buf[n] & 0xFF; + charsize = 1; + } + if (show_ctrl && ISCONTROL(u)) { + if (u & 0x80) { + u &= 0x7f; + tputc('^'); + tputc('['); + } else if (u != '\n' && u != '\r' && u != '\t') { + u ^= 0x40; + tputc('^'); + } + } + tputc(u); + } + return n; +} + +void +tresize(int col, int row) +{ + int i; + int minrow = MIN(row, term.row); + int mincol = MIN(col, term.col); + int *bp; + TCursor c; + + if (col < 1 || row < 1) { + 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]); + } + /* 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)); + } + for (i += row; i < term.row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + + /* 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)); + + /* 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)); + } + if (col > term.col) { + bp = term.tabs + term.col; + + memset(bp, 0, sizeof(*term.tabs) * (col - term.col)); + while (--bp > term.tabs && !*bp) + /* nothing */ ; + for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) + *bp = 1; + } + /* update terminal size */ + term.col = col; + term.row = row; + /* 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; +} + +void +resettitle(void) +{ + xsettitle(NULL); +} + +void +drawregion(int x1, int y1, int x2, int y2) +{ + int y; + + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; + + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } +} + +void +draw(void) +{ + int cx = term.c.x, ocx = term.ocx, ocy = term.ocy; + + if (!xstartdraw()) + return; + + /* 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) + term.ocx--; + if (term.line[term.c.y][cx].mode & ATTR_WDUMMY) + cx--; + + drawregion(0, 0, term.col, term.row); + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; + term.ocy = term.c.y; + xfinishdraw(); + if (ocx != term.ocx || ocy != term.ocy) + xximspot(term.ocx, term.ocy); +} + +void +redraw(void) +{ + tfulldirt(); + draw(); +} 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/st.o b/st.o new file mode 100644 index 0000000000000000000000000000000000000000..b71205cbbe9f1ae43c23578fbd8f3266c1e6e3d3 GIT binary patch literal 88920 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�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 Ah5!Hn literal 0 HcmV?d00001 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..4223885 100644 --- a/x.c +++ b/x.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ char *argv0; #include "arg.h" #include "st.h" #include "win.h" +#include "graphics.h" /* types used in config.h */ typedef struct { @@ -59,6 +62,12 @@ static void zoom(const Arg *); static void zoomabs(const Arg *); static void zoomreset(const Arg *); static void ttysend(const Arg *); +static void previewimage(const Arg *); +static void showimageinfo(const Arg *); +static void togglegrdebug(const Arg *); +static void dumpgrstate(const Arg *); +static void unloadimages(const Arg *); +static void toggleimages(const Arg *); /* config.h for applying patches and the configuration. */ #include "config.h" @@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec; typedef struct { int tw, th; /* tty width and height */ int w, h; /* window width and height */ + int hborderpx, vborderpx; int ch; /* char height */ int cw; /* char width */ int mode; /* window state/mode flags */ @@ -105,6 +115,7 @@ typedef struct { XSetWindowAttributes attrs; int scr; int isfixed; /* is fixed geometry? */ + int depth; /* bit depth */ int l, t; /* left and top offset */ int gm; /* geometry mask */ } XWindow; @@ -144,6 +155,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 +233,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 +342,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 +415,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 +528,9 @@ mouseaction(XEvent *e, uint release) /* ignore Buttonmask for Button - it's set on release */ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + mouse_col = evcol(e); + mouse_row = evrow(e); + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { if (ms->release == release && ms->button == e->xbutton.button && @@ -739,6 +818,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); @@ -752,7 +834,7 @@ xresize(int col, int row) XFreePixmap(xw.dpy, xw.buf); xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, - DefaultDepth(xw.dpy, xw.scr)); + xw.depth); XftDrawChange(xw.draw, xw.buf); xclear(0, 0, win.w, win.h); @@ -812,6 +894,10 @@ xloadcols(void) else die("could not allocate color %d\n", i); } + + dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha); + dc.col[defaultbg].pixel &= 0x00FFFFFF; + dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24; loaded = 1; } @@ -842,6 +928,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; } @@ -869,8 +961,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 +1106,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 +1117,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; } @@ -1134,11 +1227,25 @@ xinit(int cols, int rows) Window parent, root; pid_t thispid = getpid(); XColor xmousefg, xmousebg; + XWindowAttributes attr; + XVisualInfo vis; if (!(xw.dpy = XOpenDisplay(NULL))) die("can't open display\n"); xw.scr = XDefaultScreen(xw.dpy); - xw.vis = XDefaultVisual(xw.dpy, xw.scr); + + root = XRootWindow(xw.dpy, xw.scr); + if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) + parent = root; + + if (XMatchVisualInfo(xw.dpy, xw.scr, 32, TrueColor, &vis) != 0) { + xw.vis = vis.visual; + xw.depth = vis.depth; + } else { + XGetWindowAttributes(xw.dpy, parent, &attr); + xw.vis = attr.visual; + xw.depth = attr.depth; + } /* font */ if (!FcInit()) @@ -1148,12 +1255,12 @@ xinit(int cols, int rows) xloadfonts(usedfont, 0); /* colors */ - xw.cmap = XDefaultColormap(xw.dpy, xw.scr); + xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None); xloadcols(); /* adjust fixed window geometry */ - 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) @@ -1168,11 +1275,8 @@ xinit(int cols, int rows) | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; xw.attrs.colormap = xw.cmap; - root = XRootWindow(xw.dpy, xw.scr); - if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) - parent = root; - xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, - win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, + xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t, + win.w, win.h, 0, xw.depth, InputOutput, xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity | CWEventMask | CWColormap, &xw.attrs); if (parent != root) @@ -1183,7 +1287,7 @@ xinit(int cols, int rows) dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, &gcvalues); xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, - DefaultDepth(xw.dpy, xw.scr)); + xw.depth); XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); @@ -1240,12 +1344,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 +1374,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 +1486,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 +1630,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 +1653,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i r.width = width; XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 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]; + } + 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); + } + } + /* 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); - } - + /* 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 +1729,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 +1749,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 +1793,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 +2020,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 +2034,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 +2260,7 @@ cmessage(XEvent *e) } } else if (e->xclient.data.l[0] == xw.wmdeletewin) { ttyhangup(); + gr_deinit(); exit(0); } } @@ -1957,6 +2311,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; @@ -2047,6 +2408,10 @@ main(int argc, char *argv[]) case 'a': allowaltscreen = 0; break; + case 'A': + alpha = strtof(EARGF(usage()), NULL); + LIMIT(alpha, 0.0, 1.0); + break; case 'c': opt_class = EARGF(usage()); break; diff --git a/x.c.orig b/x.c.orig new file mode 100644 index 0000000..6f1bf8c --- /dev/null +++ b/x.c.orig @@ -0,0 +1,2447 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +char *argv0; +#include "arg.h" +#include "st.h" +#include "win.h" +#include "graphics.h" + +/* types used in config.h */ +typedef struct { + uint mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Shortcut; + +typedef struct { + uint mod; + uint button; + void (*func)(const Arg *); + const Arg arg; + uint release; +} MouseShortcut; + +typedef struct { + KeySym k; + uint mask; + char *s; + /* three-valued logic variables: 0 indifferent, 1 on, -1 off */ + signed char appkey; /* application keypad */ + signed char appcursor; /* application cursor */ +} Key; + +/* X modifiers */ +#define XK_ANY_MOD UINT_MAX +#define XK_NO_MOD 0 +#define XK_SWITCH_MOD (1<<13|1<<14) + +/* function definitions used in config.h */ +static void clipcopy(const Arg *); +static void clippaste(const Arg *); +static void numlock(const Arg *); +static void selpaste(const Arg *); +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" + +/* XEMBED messages */ +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 + +/* macros */ +#define IS_SET(flag) ((win.mode & (flag)) != 0) +#define TRUERED(x) (((x) & 0xff0000) >> 8) +#define TRUEGREEN(x) (((x) & 0xff00)) +#define TRUEBLUE(x) (((x) & 0xff) << 8) + +typedef XftDraw *Draw; +typedef XftColor Color; +typedef XftGlyphFontSpec GlyphFontSpec; + +/* Purely graphic info */ +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 */ + int cursor; /* cursor style */ +} TermWindow; + +typedef struct { + Display *dpy; + Colormap cmap; + Window win; + Drawable buf; + GlyphFontSpec *specbuf; /* font spec buffer used for rendering */ + Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid; + struct { + XIM xim; + XIC xic; + XPoint spot; + XVaNestedList spotlist; + } ime; + Draw draw; + Visual *vis; + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ +} XWindow; + +typedef struct { + Atom xtarget; + char *primary, *clipboard; + struct timespec tclick1; + struct timespec tclick2; +} XSelection; + +/* Font structure */ +#define Font Font_ +typedef struct { + int height; + int width; + int ascent; + int descent; + int badslant; + int badweight; + short lbearing; + short rbearing; + XftFont *match; + FcFontSet *set; + FcPattern *pattern; +} Font; + +/* Drawing Context */ +typedef struct { + Color *col; + size_t collen; + Font font, bfont, ifont, ibfont; + GC gc; +} DC; + +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 *); +static void ximinstantiate(Display *, XPointer, XPointer); +static void ximdestroy(XIM, XPointer, XPointer); +static int xicdestroy(XIC, XPointer, XPointer); +static void xinit(int, int); +static void cresize(int, int); +static void xresize(int, int); +static void xhints(void); +static int xloadcolor(int, const char *, Color *); +static int xloadfont(Font *, FcPattern *); +static void xloadfonts(const char *, double); +static void xunloadfont(Font *); +static void xunloadfonts(void); +static void xsetenv(void); +static void xseturgency(int); +static int evcol(XEvent *); +static int evrow(XEvent *); + +static void expose(XEvent *); +static void visibility(XEvent *); +static void unmap(XEvent *); +static void kpress(XEvent *); +static void cmessage(XEvent *); +static void resize(XEvent *); +static void focus(XEvent *); +static uint buttonmask(uint); +static int mouseaction(XEvent *, uint); +static void brelease(XEvent *); +static void bpress(XEvent *); +static void bmotion(XEvent *); +static void propnotify(XEvent *); +static void selnotify(XEvent *); +static void selclear_(XEvent *); +static void selrequest(XEvent *); +static void setsel(char *, Time); +static void mousesel(XEvent *, int); +static void mousereport(XEvent *); +static char *kmap(KeySym, uint); +static int match(uint, uint); + +static void run(void); +static void usage(void); + +static void (*handler[LASTEvent])(XEvent *) = { + [KeyPress] = kpress, + [ClientMessage] = cmessage, + [ConfigureNotify] = resize, + [VisibilityNotify] = visibility, + [UnmapNotify] = unmap, + [Expose] = expose, + [FocusIn] = focus, + [FocusOut] = focus, + [MotionNotify] = bmotion, + [ButtonPress] = bpress, + [ButtonRelease] = brelease, +/* + * Uncomment if you want the selection to disappear when you select something + * different in another window. + */ +/* [SelectionClear] = selclear_, */ + [SelectionNotify] = selnotify, +/* + * PropertyNotify is only turned on when there is some INCR transfer happening + * for the selection retrieval. + */ + [PropertyNotify] = propnotify, + [SelectionRequest] = selrequest, +}; + +/* Globals */ +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 { + FRC_NORMAL, + FRC_ITALIC, + FRC_BOLD, + FRC_ITALICBOLD +}; + +typedef struct { + XftFont *font; + int flags; + Rune unicodep; +} Fontcache; + +/* Fontcache is an array now. A new font will be appended to the array. */ +static Fontcache *frc = NULL; +static int frclen = 0; +static int frccap = 0; +static char *usedfont = NULL; +static double usedfontsize = 0; +static double defaultfontsize = 0; + +static char *opt_class = NULL; +static char **opt_cmd = NULL; +static char *opt_embed = NULL; +static char *opt_font = NULL; +static char *opt_io = NULL; +static char *opt_line = NULL; +static char *opt_name = NULL; +static char *opt_title = NULL; + +static uint buttons; /* bit field of pressed buttons */ + +void +clipcopy(const Arg *dummy) +{ + Atom clipboard; + + free(xsel.clipboard); + xsel.clipboard = NULL; + + if (xsel.primary != NULL) { + xsel.clipboard = xstrdup(xsel.primary); + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XSetSelectionOwner(xw.dpy, clipboard, xw.win, CurrentTime); + } +} + +void +clippaste(const Arg *dummy) +{ + Atom clipboard; + + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard, + xw.win, CurrentTime); +} + +void +selpaste(const Arg *dummy) +{ + XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY, + xw.win, CurrentTime); +} + +void +numlock(const Arg *dummy) +{ + win.mode ^= MODE_NUMLOCK; +} + +void +zoom(const Arg *arg) +{ + Arg larg; + + larg.f = usedfontsize + arg->f; + zoomabs(&larg); +} + +void +zoomabs(const Arg *arg) +{ + xunloadfonts(); + xloadfonts(usedfont, arg->f); + cresize(0, 0); + redraw(); + xhints(); +} + +void +zoomreset(const Arg *arg) +{ + Arg larg; + + if (defaultfontsize > 0) { + larg.f = defaultfontsize; + zoomabs(&larg); + } +} + +void +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 - win.hborderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; +} + +int +evrow(XEvent *e) +{ + int y = e->xbutton.y - win.vborderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; +} + +void +mousesel(XEvent *e, int done) +{ + int type, seltype = SEL_REGULAR; + uint state = e->xbutton.state & ~(Button1Mask | forcemousemod); + + for (type = 1; type < LEN(selmasks); ++type) { + if (match(selmasks[type], state)) { + seltype = type; + break; + } + } + selextend(evcol(e), evrow(e), seltype, done); + if (done) + setsel(getsel(), e->xbutton.time); +} + +void +mousereport(XEvent *e) +{ + int len, btn, code; + int x = evcol(e), y = evrow(e); + int state = e->xbutton.state; + char buf[40]; + static int ox, oy; + + if (e->type == MotionNotify) { + if (x == ox && y == oy) + return; + if (!IS_SET(MODE_MOUSEMOTION) && !IS_SET(MODE_MOUSEMANY)) + return; + /* MODE_MOUSEMOTION: no reporting if no button is pressed */ + if (IS_SET(MODE_MOUSEMOTION) && buttons == 0) + return; + /* Set btn to lowest-numbered pressed button, or 12 if no + * buttons are pressed. */ + for (btn = 1; btn <= 11 && !(buttons & (1<<(btn-1))); btn++) + ; + code = 32; + } else { + btn = e->xbutton.button; + /* Only buttons 1 through 11 can be encoded */ + if (btn < 1 || btn > 11) + return; + if (e->type == ButtonRelease) { + /* MODE_MOUSEX10: no button release reporting */ + if (IS_SET(MODE_MOUSEX10)) + return; + /* Don't send release events for the scroll wheel */ + if (btn == 4 || btn == 5) + return; + } + code = 0; + } + + ox = x; + oy = y; + + /* Encode btn into code. If no button is pressed for a motion event in + * MODE_MOUSEMANY, then encode it as a release. */ + if ((!IS_SET(MODE_MOUSESGR) && e->type == ButtonRelease) || btn == 12) + code += 3; + else if (btn >= 8) + code += 128 + btn - 8; + else if (btn >= 4) + code += 64 + btn - 4; + else + code += btn - 1; + + if (!IS_SET(MODE_MOUSEX10)) { + code += ((state & ShiftMask ) ? 4 : 0) + + ((state & Mod1Mask ) ? 8 : 0) /* meta key: alt */ + + ((state & ControlMask) ? 16 : 0); + } + + if (IS_SET(MODE_MOUSESGR)) { + len = snprintf(buf, sizeof(buf), "\033[<%d;%d;%d%c", + code, x+1, y+1, + e->type == ButtonRelease ? 'm' : 'M'); + } else if (x < 223 && y < 223) { + len = snprintf(buf, sizeof(buf), "\033[M%c%c%c", + 32+code, 32+x+1, 32+y+1); + } else { + return; + } + + ttywrite(buf, len, 0); +} + +uint +buttonmask(uint button) +{ + return button == Button1 ? Button1Mask + : button == Button2 ? Button2Mask + : button == Button3 ? Button3Mask + : button == Button4 ? Button4Mask + : button == Button5 ? Button5Mask + : 0; +} + +int +mouseaction(XEvent *e, uint release) +{ + MouseShortcut *ms; + + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + + mouse_col = evcol(e); + mouse_row = evrow(e); + + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && + (match(ms->mod, state) || /* exact or forced */ + match(ms->mod, state & ~forcemousemod))) { + ms->func(&(ms->arg)); + return 1; + } + } + + return 0; +} + +void +bpress(XEvent *e) +{ + int btn = e->xbutton.button; + struct timespec now; + int snap; + + if (1 <= btn && btn <= 11) + buttons |= 1 << (btn-1); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 0)) + return; + + if (btn == Button1) { + /* + * If the user clicks below predefined timeouts specific + * snapping behaviour is exposed. + */ + clock_gettime(CLOCK_MONOTONIC, &now); + if (TIMEDIFF(now, xsel.tclick2) <= tripleclicktimeout) { + snap = SNAP_LINE; + } else if (TIMEDIFF(now, xsel.tclick1) <= doubleclicktimeout) { + snap = SNAP_WORD; + } else { + snap = 0; + } + xsel.tclick2 = xsel.tclick1; + xsel.tclick1 = now; + + selstart(evcol(e), evrow(e), snap); + } +} + +void +propnotify(XEvent *e) +{ + XPropertyEvent *xpev; + Atom clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + + xpev = &e->xproperty; + if (xpev->state == PropertyNewValue && + (xpev->atom == XA_PRIMARY || + xpev->atom == clipboard)) { + selnotify(e); + } +} + +void +selnotify(XEvent *e) +{ + ulong nitems, ofs, rem; + int format; + uchar *data, *last, *repl; + Atom type, incratom, property = None; + + incratom = XInternAtom(xw.dpy, "INCR", 0); + + ofs = 0; + if (e->type == SelectionNotify) + property = e->xselection.property; + else if (e->type == PropertyNotify) + property = e->xproperty.atom; + + if (property == None) + return; + + do { + if (XGetWindowProperty(xw.dpy, xw.win, property, ofs, + BUFSIZ/4, False, AnyPropertyType, + &type, &format, &nitems, &rem, + &data)) { + fprintf(stderr, "Clipboard allocation failed\n"); + return; + } + + if (e->type == PropertyNotify && nitems == 0 && rem == 0) { + /* + * If there is some PropertyNotify with no data, then + * this is the signal of the selection owner that all + * data has been transferred. We won't need to receive + * PropertyNotify events anymore. + */ + MODBIT(xw.attrs.event_mask, 0, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + } + + if (type == incratom) { + /* + * Activate the PropertyNotify events so we receive + * when the selection owner does send us the next + * chunk of data. + */ + MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + + /* + * Deleting the property is the transfer start signal. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); + continue; + } + + /* + * As seen in getsel: + * Line endings are inconsistent in the terminal and GUI world + * copy and pasting. When receiving some selection data, + * replace all '\n' with '\r'. + * FIXME: Fix the computer world. + */ + repl = data; + last = data + nitems * format / 8; + while ((repl = memchr(repl, '\n', last - repl))) { + *repl++ = '\r'; + } + + if (IS_SET(MODE_BRCKTPASTE) && ofs == 0) + ttywrite("\033[200~", 6, 0); + ttywrite((char *)data, nitems * format / 8, 1); + if (IS_SET(MODE_BRCKTPASTE) && rem == 0) + ttywrite("\033[201~", 6, 0); + XFree(data); + /* number of 32-bit chunks returned */ + ofs += nitems * format / 32; + } while (rem > 0); + + /* + * Deleting the property again tells the selection owner to send the + * next data chunk in the property. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); +} + +void +xclipcopy(void) +{ + clipcopy(NULL); +} + +void +selclear_(XEvent *e) +{ + selclear(); +} + +void +selrequest(XEvent *e) +{ + XSelectionRequestEvent *xsre; + XSelectionEvent xev; + Atom xa_targets, string, clipboard; + char *seltext; + + xsre = (XSelectionRequestEvent *) e; + xev.type = SelectionNotify; + xev.requestor = xsre->requestor; + xev.selection = xsre->selection; + xev.target = xsre->target; + xev.time = xsre->time; + if (xsre->property == None) + xsre->property = xsre->target; + + /* reject */ + xev.property = None; + + xa_targets = XInternAtom(xw.dpy, "TARGETS", 0); + if (xsre->target == xa_targets) { + /* respond with the supported type */ + string = xsel.xtarget; + XChangeProperty(xsre->display, xsre->requestor, xsre->property, + XA_ATOM, 32, PropModeReplace, + (uchar *) &string, 1); + xev.property = xsre->property; + } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) { + /* + * xith XA_STRING non ascii characters may be incorrect in the + * requestor. It is not our problem, use utf8. + */ + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + if (xsre->selection == XA_PRIMARY) { + seltext = xsel.primary; + } else if (xsre->selection == clipboard) { + seltext = xsel.clipboard; + } else { + fprintf(stderr, + "Unhandled clipboard selection 0x%lx\n", + xsre->selection); + return; + } + if (seltext != NULL) { + XChangeProperty(xsre->display, xsre->requestor, + xsre->property, xsre->target, + 8, PropModeReplace, + (uchar *)seltext, strlen(seltext)); + xev.property = xsre->property; + } + } + + /* all done, send a notification to the listener */ + if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *) &xev)) + fprintf(stderr, "Error sending SelectionNotify event\n"); +} + +void +setsel(char *str, Time t) +{ + if (!str) + return; + + free(xsel.primary); + xsel.primary = str; + + XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t); + if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win) + selclear(); +} + +void +xsetsel(char *str) +{ + setsel(str, CurrentTime); +} + +void +brelease(XEvent *e) +{ + int btn = e->xbutton.button; + + if (1 <= btn && btn <= 11) + buttons &= ~(1 << (btn-1)); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 1)) + return; + if (btn == Button1) + mousesel(e, 1); +} + +void +bmotion(XEvent *e) +{ + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + mousesel(e, 0); +} + +void +cresize(int width, int height) +{ + int col, row; + + if (width != 0) + win.w = width; + if (height != 0) + win.h = height; + + col = (win.w - 2 * borderpx) / win.cw; + row = (win.h - 2 * borderpx) / win.ch; + 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); +} + +void +xresize(int col, int row) +{ + win.tw = col * win.cw; + win.th = row * win.ch; + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ + xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec)); +} + +ushort +sixd_to_16bit(int x) +{ + return x == 0 ? 0 : 0x3737 + 0x2828 * x; +} + +int +xloadcolor(int i, const char *name, Color *ncolor) +{ + XRenderColor color = { .alpha = 0xffff }; + + if (!name) { + if (BETWEEN(i, 16, 255)) { /* 256 color */ + if (i < 6*6*6+16) { /* same colors as xterm */ + color.red = sixd_to_16bit( ((i-16)/36)%6 ); + color.green = sixd_to_16bit( ((i-16)/6) %6 ); + color.blue = sixd_to_16bit( ((i-16)/1) %6 ); + } else { /* greyscale */ + color.red = 0x0808 + 0x0a0a * (i - (6*6*6+16)); + color.green = color.blue = color.red; + } + return XftColorAllocValue(xw.dpy, xw.vis, + xw.cmap, &color, ncolor); + } else + name = colorname[i]; + } + + return XftColorAllocName(xw.dpy, xw.vis, xw.cmap, name, ncolor); +} + +void +xloadcols(void) +{ + int i; + static int loaded; + Color *cp; + + if (loaded) { + for (cp = dc.col; cp < &dc.col[dc.collen]; ++cp) + XftColorFree(xw.dpy, xw.vis, xw.cmap, cp); + } else { + dc.collen = MAX(LEN(colorname), 256); + dc.col = xmalloc(dc.collen * sizeof(Color)); + } + + for (i = 0; i < dc.collen; i++) + if (!xloadcolor(i, NULL, &dc.col[i])) { + if (colorname[i]) + die("could not allocate color '%s'\n", colorname[i]); + else + die("could not allocate color %d\n", i); + } + loaded = 1; +} + +int +xgetcolor(int x, unsigned char *r, unsigned char *g, unsigned char *b) +{ + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + *r = dc.col[x].color.red >> 8; + *g = dc.col[x].color.green >> 8; + *b = dc.col[x].color.blue >> 8; + + return 0; +} + +int +xsetcolorname(int x, const char *name) +{ + Color ncolor; + + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + if (!xloadcolor(x, name, &ncolor)) + return 1; + + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + + return 0; +} + +/* + * Absolute coordinates. + */ +void +xclear(int x1, int y1, int x2, int y2) +{ + XftDrawRect(xw.draw, + &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg], + x1, y1, x2-x1, y2-y1); +} + +void +xhints(void) +{ + XClassHint class = {opt_name ? opt_name : termname, + opt_class ? opt_class : termname}; + XWMHints wm = {.flags = InputHint, .input = 1}; + XSizeHints *sizeh; + + sizeh = XAllocSizeHints(); + + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; + 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; + sizeh->min_width = win.cw + 2 * borderpx; + if (xw.isfixed) { + sizeh->flags |= PMaxSize; + sizeh->min_width = sizeh->max_width = win.w; + sizeh->min_height = sizeh->max_height = win.h; + } + if (xw.gm & (XValue|YValue)) { + sizeh->flags |= USPosition | PWinGravity; + sizeh->x = xw.l; + sizeh->y = xw.t; + sizeh->win_gravity = xgeommasktogravity(xw.gm); + } + + XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, + &class); + XFree(sizeh); +} + +int +xgeommasktogravity(int mask) +{ + switch (mask & (XNegative|YNegative)) { + case 0: + return NorthWestGravity; + case XNegative: + return NorthEastGravity; + case YNegative: + return SouthWestGravity; + } + + return SouthEastGravity; +} + +int +xloadfont(Font *f, FcPattern *pattern) +{ + FcPattern *configured; + FcPattern *match; + FcResult result; + XGlyphInfo extents; + int wantattr, haveattr; + + /* + * Manually configure instead of calling XftMatchFont + * so that we can use the configured pattern for + * "missing glyph" lookups. + */ + configured = FcPatternDuplicate(pattern); + if (!configured) + return 1; + + FcConfigSubstitute(NULL, configured, FcMatchPattern); + XftDefaultSubstitute(xw.dpy, xw.scr, configured); + + match = FcFontMatch(NULL, configured, &result); + if (!match) { + FcPatternDestroy(configured); + return 1; + } + + if (!(f->match = XftFontOpenPattern(xw.dpy, match))) { + FcPatternDestroy(configured); + FcPatternDestroy(match); + return 1; + } + + if ((XftPatternGetInteger(pattern, "slant", 0, &wantattr) == + XftResultMatch)) { + /* + * Check if xft was unable to find a font with the appropriate + * slant but gave us one anyway. Try to mitigate. + */ + if ((XftPatternGetInteger(f->match->pattern, "slant", 0, + &haveattr) != XftResultMatch) || haveattr < wantattr) { + f->badslant = 1; + fputs("font slant does not match\n", stderr); + } + } + + if ((XftPatternGetInteger(pattern, "weight", 0, &wantattr) == + XftResultMatch)) { + if ((XftPatternGetInteger(f->match->pattern, "weight", 0, + &haveattr) != XftResultMatch) || haveattr != wantattr) { + f->badweight = 1; + fputs("font weight does not match\n", stderr); + } + } + + XftTextExtentsUtf8(xw.dpy, f->match, + (const FcChar8 *) ascii_printable, + strlen(ascii_printable), &extents); + + f->set = NULL; + f->pattern = configured; + + f->ascent = f->match->ascent; + f->descent = f->match->descent; + f->lbearing = 0; + f->rbearing = f->match->max_advance_width; + + f->height = f->ascent + f->descent; + f->width = DIVCEIL(extents.xOff, strlen(ascii_printable)); + + return 0; +} + +void +xloadfonts(const char *fontstr, double fontsize) +{ + FcPattern *pattern; + double fontval; + + if (fontstr[0] == '-') + pattern = XftXlfdParse(fontstr, False, False); + else + pattern = FcNameParse((const FcChar8 *)fontstr); + + if (!pattern) + die("can't open font %s\n", fontstr); + + if (fontsize > 1) { + FcPatternDel(pattern, FC_PIXEL_SIZE); + FcPatternDel(pattern, FC_SIZE); + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, (double)fontsize); + usedfontsize = fontsize; + } else { + if (FcPatternGetDouble(pattern, FC_PIXEL_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = fontval; + } else if (FcPatternGetDouble(pattern, FC_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = -1; + } else { + /* + * Default font size is 12, if none given. This is to + * have a known usedfontsize value. + */ + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } + if (defaultfontsize <= 0) + defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) + die("can't open font %s\n", fontstr); + + if (usedfontsize < 0) { + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; + if (defaultfontsize <= 0 && fontsize == 0) + defaultfontsize = fontval; + } + + /* Setting character width and height. */ + win.cw = ceilf(dc.font.width * cwscale); + win.ch = ceilf(dc.font.height * chscale); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadfont(&dc.ifont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_WEIGHT); + FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadfont(&dc.ibfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadfont(&dc.bfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDestroy(pattern); +} + +void +xunloadfont(Font *f) +{ + XftFontClose(xw.dpy, f->match); + FcPatternDestroy(f->pattern); + if (f->set) + FcFontSetDestroy(f->set); +} + +void +xunloadfonts(void) +{ + /* Free the loaded fonts in the font cache. */ + while (frclen > 0) + XftFontClose(xw.dpy, frc[--frclen].font); + + xunloadfont(&dc.font); + xunloadfont(&dc.bfont); + xunloadfont(&dc.ifont); + xunloadfont(&dc.ibfont); +} + +int +ximopen(Display *dpy) +{ + XIMCallback imdestroy = { .client_data = NULL, .callback = ximdestroy }; + XICCallback icdestroy = { .client_data = NULL, .callback = xicdestroy }; + + xw.ime.xim = XOpenIM(xw.dpy, NULL, NULL, NULL); + if (xw.ime.xim == NULL) + return 0; + + if (XSetIMValues(xw.ime.xim, XNDestroyCallback, &imdestroy, NULL)) + fprintf(stderr, "XSetIMValues: " + "Could not set XNDestroyCallback.\n"); + + xw.ime.spotlist = XVaCreateNestedList(0, XNSpotLocation, &xw.ime.spot, + NULL); + + if (xw.ime.xic == NULL) { + xw.ime.xic = XCreateIC(xw.ime.xim, XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, xw.win, + XNDestroyCallback, &icdestroy, + NULL); + } + if (xw.ime.xic == NULL) + fprintf(stderr, "XCreateIC: Could not create input context.\n"); + + return 1; +} + +void +ximinstantiate(Display *dpy, XPointer client, XPointer call) +{ + if (ximopen(dpy)) + XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); +} + +void +ximdestroy(XIM xim, XPointer client, XPointer call) +{ + xw.ime.xim = NULL; + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + XFree(xw.ime.spotlist); +} + +int +xicdestroy(XIC xim, XPointer client, XPointer call) +{ + xw.ime.xic = NULL; + return 1; +} + +void +xinit(int cols, int rows) +{ + XGCValues gcvalues; + Cursor cursor; + Window parent, root; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; + + if (!(xw.dpy = XOpenDisplay(NULL))) + die("can't open display\n"); + xw.scr = XDefaultScreen(xw.dpy); + xw.vis = XDefaultVisual(xw.dpy, xw.scr); + + /* font */ + if (!FcInit()) + die("could not init fontconfig.\n"); + + usedfont = (opt_font == NULL)? font : opt_font; + xloadfonts(usedfont, 0); + + /* colors */ + xw.cmap = XDefaultColormap(xw.dpy, xw.scr); + xloadcols(); + + /* adjust fixed window geometry */ + 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) + xw.t += DisplayHeight(xw.dpy, xw.scr) - win.h - 2; + + /* Events */ + xw.attrs.background_pixel = dc.col[defaultbg].pixel; + xw.attrs.border_pixel = dc.col[defaultbg].pixel; + xw.attrs.bit_gravity = NorthWestGravity; + xw.attrs.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask + | ExposureMask | VisibilityChangeMask | StructureNotifyMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + + root = XRootWindow(xw.dpy, xw.scr); + if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) + parent = root; + xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, + win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, + xw.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; + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + + /* font spec buffer */ + xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec)); + + /* Xft rendering context */ + xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap); + + /* input methods */ + if (!ximopen(xw.dpy)) { + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + } + + /* white cursor, black outline */ + cursor = XCreateFontCursor(xw.dpy, mouseshape); + XDefineCursor(xw.dpy, xw.win, cursor); + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousefg], &xmousefg) == 0) { + xmousefg.red = 0xffff; + xmousefg.green = 0xffff; + xmousefg.blue = 0xffff; + } + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousebg], &xmousebg) == 0) { + xmousebg.red = 0x0000; + xmousebg.green = 0x0000; + xmousebg.blue = 0x0000; + } + + XRecolorCursor(xw.dpy, cursor, &xmousefg, &xmousebg); + + xw.xembed = XInternAtom(xw.dpy, "_XEMBED", False); + xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); + xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); + xw.netwmiconname = XInternAtom(xw.dpy, "_NET_WM_ICON_NAME", False); + XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); + + xw.netwmpid = XInternAtom(xw.dpy, "_NET_WM_PID", False); + XChangeProperty(xw.dpy, xw.win, xw.netwmpid, XA_CARDINAL, 32, + PropModeReplace, (uchar *)&thispid, 1); + + win.mode = MODE_NUMLOCK; + resettitle(); + xhints(); + XMapWindow(xw.dpy, xw.win); + XSync(xw.dpy, False); + + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick1); + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick2); + xsel.primary = NULL; + xsel.clipboard = NULL; + 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 = 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; + Rune rune; + FT_UInt glyphidx; + FcResult fcres; + FcPattern *fcpattern, *fontpattern; + FcFontSet *fcsets[] = { NULL }; + FcCharSet *fccharset; + int i, f, numspecs = 0; + + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + /* Fetch rune and mode for current glyph. */ + rune = glyphs[i].u; + mode = glyphs[i].mode; + + /* Skip dummy wide-character spacing. */ + 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; + font = &dc.font; + frcflags = FRC_NORMAL; + runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f); + if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) { + font = &dc.ibfont; + frcflags = FRC_ITALICBOLD; + } else if (mode & ATTR_ITALIC) { + font = &dc.ifont; + frcflags = FRC_ITALIC; + } else if (mode & ATTR_BOLD) { + font = &dc.bfont; + frcflags = FRC_BOLD; + } + yp = winy + font->ascent; + } + + /* Lookup character index with default font. */ + glyphidx = XftCharIndex(xw.dpy, font->match, rune); + if (glyphidx) { + specs[numspecs].font = font->match; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + continue; + } + + /* Fallback on font cache, search the font cache for match. */ + 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; + xp += runewidth; + numspecs++; + } + + 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 = 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; + XRectangle r; + + /* Fallback on color display for attributes not supported by the font */ + if (base.mode & ATTR_ITALIC && base.mode & ATTR_BOLD) { + if (dc.ibfont.badslant || dc.ibfont.badweight) + base.fg = defaultattr; + } else if ((base.mode & ATTR_ITALIC && dc.ifont.badslant) || + (base.mode & ATTR_BOLD && dc.bfont.badweight)) { + base.fg = defaultattr; + } + + if (IS_TRUECOL(base.fg)) { + colfg.alpha = 0xffff; + colfg.red = TRUERED(base.fg); + colfg.green = TRUEGREEN(base.fg); + colfg.blue = TRUEBLUE(base.fg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &truefg); + fg = &truefg; + } else { + fg = &dc.col[base.fg]; + } + + if (IS_TRUECOL(base.bg)) { + colbg.alpha = 0xffff; + colbg.green = TRUEGREEN(base.bg); + colbg.red = TRUERED(base.bg); + colbg.blue = TRUEBLUE(base.bg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, &truebg); + bg = &truebg; + } else { + bg = &dc.col[base.bg]; + } + + /* Change basic system colors [0-7] to bright system colors [8-15] */ + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_BOLD && BETWEEN(base.fg, 0, 7)) + fg = &dc.col[base.fg + 8]; + + if (IS_SET(MODE_REVERSE)) { + if (fg == &dc.col[defaultfg]) { + fg = &dc.col[defaultbg]; + } else { + colfg.red = ~fg->color.red; + colfg.green = ~fg->color.green; + colfg.blue = ~fg->color.blue; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, + &revfg); + fg = &revfg; + } + + if (bg == &dc.col[defaultbg]) { + bg = &dc.col[defaultfg]; + } else { + colbg.red = ~bg->color.red; + colbg.green = ~bg->color.green; + colbg.blue = ~bg->color.blue; + colbg.alpha = bg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, + &revbg); + bg = &revbg; + } + } + + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_FAINT) { + colfg.red = fg->color.red / 2; + colfg.green = fg->color.green / 2; + colfg.blue = fg->color.blue / 2; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &revfg); + fg = &revfg; + } + + if (base.mode & ATTR_REVERSE) { + temp = fg; + fg = bg; + bg = temp; + } + + if (base.mode & ATTR_BLINK && win.mode & MODE_BLINK) + fg = bg; + + if (base.mode & ATTR_INVISIBLE) + fg = bg; + + /* Intelligent cleaning up of the borders. */ + 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); + + /* Set the clip region because Xft is sometimes dirty. */ + r.x = 0; + r.y = 0; + r.height = win.ch; + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 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]; + } + 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); + } + } + + /* 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); + } + + /* Reset clip to none. */ + XftDrawSetClip(xw.draw, 0); +} + +void +xdrawglyph(Glyph g, int x, int y) +{ + int numspecs; + XftGlyphFontSpec spec; + + 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 +xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) +{ + Color drawcol; + + /* remove the old cursor */ + if (selected(ox, oy)) + og.mode ^= ATTR_REVERSE; + xdrawglyph(og, ox, oy); + + 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; + + if (IS_SET(MODE_REVERSE)) { + g.mode |= ATTR_REVERSE; + g.bg = defaultfg; + if (selected(cx, cy)) { + drawcol = dc.col[defaultcs]; + g.fg = defaultrcs; + } else { + drawcol = dc.col[defaultrcs]; + g.fg = defaultcs; + } + } else { + if (selected(cx, cy)) { + g.fg = defaultfg; + g.bg = defaultrcs; + } else { + g.fg = defaultbg; + g.bg = defaultcs; + } + drawcol = dc.col[g.bg]; + } + + /* draw the new one */ + if (IS_SET(MODE_FOCUSED)) { + switch (win.cursor) { + case 7: /* st extension */ + g.u = 0x2603; /* snowman (U+2603) */ + /* FALLTHROUGH */ + case 0: /* Blinking Block */ + case 1: /* Blinking Block (Default) */ + case 2: /* Steady Block */ + xdrawglyph(g, cx, cy); + break; + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, + 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, + win.hborderpx + cx * win.cw, + win.vborderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, + win.hborderpx + cx * win.cw, + win.vborderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, + win.hborderpx + cx * win.cw, + win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + win.hborderpx + (cx + 1) * win.cw - 1, + win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + 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) +{ + char buf[sizeof(long) * 8 + 1]; + + snprintf(buf, sizeof(buf), "%lu", xw.win); + setenv("WINDOWID", buf, 1); +} + +void +xseticontitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMIconName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmiconname); + XFree(prop.value); +} + +void +xsettitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); + XFree(prop.value); +} + +int +xstartdraw(void) +{ + return IS_SET(MODE_VISIBLE); +} + +void +xdrawline(Line line, int x1, int y1, int x2) +{ + int i, x, ox, numspecs; + Glyph base, new; + XftGlyphFontSpec *specs = xw.specbuf; + + numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1); + i = ox = 0; + for (x = x1; x < x2 && i < numspecs; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + 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; + } + if (i == 0) { + ox = x; + base = new; + } + i++; + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); + if (i > 0 && base.mode & ATTR_IMAGE) + xdrawimages(base, line, ox, y1, x); +} + +void +xfinishdraw(void) +{ + XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, + win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); +} + +void +xximspot(int x, int y) +{ + if (xw.ime.xic == NULL) + return; + + xw.ime.spot.x = borderpx + x * win.cw; + xw.ime.spot.y = borderpx + (y + 1) * win.ch; + + XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL); +} + +void +expose(XEvent *ev) +{ + redraw(); +} + +void +visibility(XEvent *ev) +{ + XVisibilityEvent *e = &ev->xvisibility; + + MODBIT(win.mode, e->state != VisibilityFullyObscured, MODE_VISIBLE); +} + +void +unmap(XEvent *ev) +{ + win.mode &= ~MODE_VISIBLE; +} + +void +xsetpointermotion(int set) +{ + MODBIT(xw.attrs.event_mask, set, PointerMotionMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs); +} + +void +xsetmode(int set, unsigned int flags) +{ + int mode = win.mode; + MODBIT(win.mode, set, flags); + if ((win.mode & MODE_REVERSE) != (mode & MODE_REVERSE)) + redraw(); +} + +int +xsetcursor(int cursor) +{ + if (!BETWEEN(cursor, 0, 7)) /* 7: st extension */ + return 1; + win.cursor = cursor; + return 0; +} + +void +xseturgency(int add) +{ + XWMHints *h = XGetWMHints(xw.dpy, xw.win); + + MODBIT(h->flags, add, XUrgencyHint); + XSetWMHints(xw.dpy, xw.win, h); + XFree(h); +} + +void +xbell(void) +{ + if (!(IS_SET(MODE_FOCUSED))) + xseturgency(1); + if (bellvolume) + XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL); +} + +void +focus(XEvent *ev) +{ + XFocusChangeEvent *e = &ev->xfocus; + + if (e->mode == NotifyGrab) + return; + + if (ev->type == FocusIn) { + if (xw.ime.xic) + XSetICFocus(xw.ime.xic); + win.mode |= MODE_FOCUSED; + xseturgency(0); + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[I", 3, 0); + } else { + if (xw.ime.xic) + XUnsetICFocus(xw.ime.xic); + win.mode &= ~MODE_FOCUSED; + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[O", 3, 0); + } +} + +int +match(uint mask, uint state) +{ + return mask == XK_ANY_MOD || mask == (state & ~ignoremod); +} + +char* +kmap(KeySym k, uint state) +{ + Key *kp; + int i; + + /* Check for mapped keys out of X11 function keys. */ + for (i = 0; i < LEN(mappedkeys); i++) { + if (mappedkeys[i] == k) + break; + } + if (i == LEN(mappedkeys)) { + if ((k & 0xFFFF) < 0xFD00) + return NULL; + } + + for (kp = key; kp < key + LEN(key); kp++) { + if (kp->k != k) + continue; + + if (!match(kp->mask, state)) + continue; + + if (IS_SET(MODE_APPKEYPAD) ? kp->appkey < 0 : kp->appkey > 0) + continue; + if (IS_SET(MODE_NUMLOCK) && kp->appkey == 2) + continue; + + if (IS_SET(MODE_APPCURSOR) ? kp->appcursor < 0 : kp->appcursor > 0) + continue; + + return kp->s; + } + + return NULL; +} + +void +kpress(XEvent *ev) +{ + XKeyEvent *e = &ev->xkey; + KeySym ksym = NoSymbol; + char buf[64], *customkey; + int len; + Rune c; + Status status; + Shortcut *bp; + + if (IS_SET(MODE_KBDLOCK)) + return; + + if (xw.ime.xic) { + len = XmbLookupString(xw.ime.xic, e, buf, sizeof buf, &ksym, &status); + if (status == XBufferOverflow) + return; + } else { + len = XLookupString(e, buf, sizeof buf, &ksym, NULL); + } + /* 1. shortcuts */ + for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { + if (ksym == bp->keysym && match(bp->mod, e->state)) { + bp->func(&(bp->arg)); + return; + } + } + + /* 2. custom keys from config.h */ + if ((customkey = kmap(ksym, e->state))) { + ttywrite(customkey, strlen(customkey), 1); + return; + } + + /* 3. composed string from input method */ + if (len == 0) + return; + if (len == 1 && e->state & Mod1Mask) { + if (IS_SET(MODE_8BIT)) { + if (*buf < 0177) { + c = *buf | 0x80; + len = utf8encode(c, buf); + } + } else { + buf[1] = buf[0]; + buf[0] = '\033'; + len = 2; + } + } + ttywrite(buf, len, 1); +} + +void +cmessage(XEvent *e) +{ + /* + * See xembed specs + * http://standards.freedesktop.org/xembed-spec/xembed-spec-latest.html + */ + if (e->xclient.message_type == xw.xembed && e->xclient.format == 32) { + if (e->xclient.data.l[1] == XEMBED_FOCUS_IN) { + win.mode |= MODE_FOCUSED; + xseturgency(0); + } else if (e->xclient.data.l[1] == XEMBED_FOCUS_OUT) { + win.mode &= ~MODE_FOCUSED; + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); + gr_deinit(); + exit(0); + } +} + +void +resize(XEvent *e) +{ + if (e->xconfigure.width == win.w && e->xconfigure.height == win.h) + return; + + cresize(e->xconfigure.width, e->xconfigure.height); +} + +void +run(void) +{ + XEvent ev; + int w = win.w, h = win.h; + fd_set rfd; + int xfd = XConnectionNumber(xw.dpy), ttyfd, xev, drawing; + struct timespec seltv, *tv, now, lastblink, trigger; + double timeout; + + /* Waiting for window mapping */ + do { + XNextEvent(xw.dpy, &ev); + /* + * This XFilterEvent call is required because of XOpenIM. It + * does filter out the key event and some client message for + * the input method too. + */ + if (XFilterEvent(&ev, None)) + continue; + if (ev.type == ConfigureNotify) { + w = ev.xconfigure.width; + h = ev.xconfigure.height; + } + } while (ev.type != MapNotify); + + ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd); + cresize(w, h); + + for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) { + FD_ZERO(&rfd); + FD_SET(ttyfd, &rfd); + FD_SET(xfd, &rfd); + + 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; + + if (pselect(MAX(xfd, ttyfd)+1, &rfd, NULL, NULL, tv, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + clock_gettime(CLOCK_MONOTONIC, &now); + + if (FD_ISSET(ttyfd, &rfd)) + ttyread(); + + xev = 0; + while (XPending(xw.dpy)) { + xev = 1; + XNextEvent(xw.dpy, &ev); + if (XFilterEvent(&ev, None)) + continue; + if (handler[ev.type]) + (handler[ev.type])(&ev); + } + + /* + * To reduce flicker and tearing, when new content or event + * triggers drawing, we first wait a bit to ensure we got + * everything, and if nothing new arrives - we draw. + * We start with trying to wait minlatency ms. If more content + * arrives sooner, we retry with shorter and shorter periods, + * and eventually draw even without idle after maxlatency ms. + * Typically this results in low latency while interacting, + * maximum latency intervals during `cat huge.txt`, and perfect + * sync with periodic updates from animations/key-repeats/etc. + */ + if (FD_ISSET(ttyfd, &rfd) || xev) { + if (!drawing) { + trigger = now; + drawing = 1; + } + timeout = (maxlatency - TIMEDIFF(now, trigger)) \ + / maxlatency * minlatency; + if (timeout > 0) + continue; /* we have time, try to find idle */ + } + + /* idle detected or maxlatency exhausted -> draw */ + timeout = -1; + if (blinktimeout && tattrset(ATTR_BLINK)) { + timeout = blinktimeout - TIMEDIFF(now, lastblink); + if (timeout <= 0) { + if (-timeout > blinktimeout) /* start visible */ + win.mode |= MODE_BLINK; + win.mode ^= MODE_BLINK; + tsetdirtattr(ATTR_BLINK); + lastblink = now; + timeout = blinktimeout; + } + } + + draw(); + XFlush(xw.dpy); + drawing = 0; + } +} + +void +usage(void) +{ + die("usage: %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid]" + " [[-e] command [args ...]]\n" + " %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid] -l line" + " [stty_args ...]\n", argv0, argv0); +} + +int +main(int argc, char *argv[]) +{ + xw.l = xw.t = 0; + xw.isfixed = False; + xsetcursor(cursorshape); + + ARGBEGIN { + case 'a': + allowaltscreen = 0; + break; + case 'c': + opt_class = EARGF(usage()); + break; + case 'e': + if (argc > 0) + --argc, ++argv; + goto run; + case 'f': + opt_font = EARGF(usage()); + break; + case 'g': + xw.gm = XParseGeometry(EARGF(usage()), + &xw.l, &xw.t, &cols, &rows); + break; + case 'i': + xw.isfixed = 1; + break; + case 'o': + opt_io = EARGF(usage()); + break; + case 'l': + opt_line = EARGF(usage()); + break; + case 'n': + opt_name = EARGF(usage()); + break; + case 't': + case 'T': + opt_title = EARGF(usage()); + break; + case 'w': + opt_embed = EARGF(usage()); + break; + case 'v': + die("%s " VERSION "\n", argv0); + break; + default: + usage(); + } ARGEND; + +run: + if (argc > 0) /* eat all remaining arguments */ + opt_cmd = argv; + + if (!opt_title) + opt_title = (opt_line || !opt_cmd) ? "st" : opt_cmd[0]; + + setlocale(LC_CTYPE, ""); + XSetLocaleModifiers(""); + cols = MAX(cols, 1); + rows = MAX(rows, 1); + tnew(cols, rows); + xinit(cols, rows); + xsetenv(); + selinit(); + run(); + + return 0; +} diff --git a/x.o b/x.o new file mode 100644 index 0000000000000000000000000000000000000000..7f725e982ef9af7d33c60d7d4a2e363e232cc4fc GIT binary patch literal 89208 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 z