summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorshipwreckt <me@shipwreckt.co.uk>2025-10-16 22:13:02 +0100
committershipwreckt <me@shipwreckt.co.uk>2025-10-16 22:13:02 +0100
commite63a16b509b05993fc7900b6296ba8601e343976 (patch)
treeb9e1d57e09a111981427bafe3b3223384e358830
parente8b5675eb77aa20027f369ca278457b6a7c2e142 (diff)
More small changes, added thunderbird items.
-rwxr-xr-xautoinstall.sh19
-rw-r--r--files/config/nvim/init.vim26
m---------files/config/nvim/plug/goyo.vim0
-rw-r--r--files/config/nvim/test.md2
-rw-r--r--files/config/suckless/dwm/config.h5
-rw-r--r--files/config/suckless/dwm/drw.obin10872 -> 10864 bytes
-rwxr-xr-xfiles/config/suckless/dwm/dwmbin67792 -> 67936 bytes
-rw-r--r--files/config/suckless/dwm/dwm.obin59760 -> 60176 bytes
-rw-r--r--files/config/suckless/dwm/util.obin2216 -> 2216 bytes
m---------files/config/suckless/slock0
-rw-r--r--files/config/suckless/slstatus/config.h6
-rwxr-xr-xfiles/config/suckless/slstatus/slstatusbin31832 -> 31832 bytes
-rw-r--r--files/config/suckless/slstatus/slstatus.obin6536 -> 6568 bytes
-rw-r--r--files/config/suckless/st/Makefile.orig3
-rw-r--r--files/config/suckless/st/Makefile.rej20
-rw-r--r--files/config/suckless/st/boxdraw.obin6184 -> 0 bytes
-rw-r--r--files/config/suckless/st/config.def.h42
-rw-r--r--files/config/suckless/st/config.def.h.orig488
-rw-r--r--files/config/suckless/st/config.def.h.rej13
-rw-r--r--files/config/suckless/st/config.mk5
-rw-r--r--files/config/suckless/st/graphics.c3812
-rw-r--r--files/config/suckless/st/graphics.h107
-rwxr-xr-xfiles/config/suckless/st/icat-mini.sh801
-rw-r--r--files/config/suckless/st/khash.h627
-rw-r--r--files/config/suckless/st/kvec.h90
-rw-r--r--files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff7324
-rw-r--r--files/config/suckless/st/rowcolumn_diacritics_helpers.c391
-rwxr-xr-xfiles/config/suckless/st/stbin110240 -> 0 bytes
-rw-r--r--files/config/suckless/st/st.c268
-rw-r--r--files/config/suckless/st/st.c.orig3
-rw-r--r--files/config/suckless/st/st.c.rej27
-rw-r--r--files/config/suckless/st/st.h23
-rw-r--r--files/config/suckless/st/st.h.orig12
-rw-r--r--files/config/suckless/st/st.h.rej72
-rw-r--r--files/config/suckless/st/st.info6
-rw-r--r--files/config/suckless/st/st.obin81648 -> 0 bytes
-rw-r--r--files/config/suckless/st/win.h3
-rw-r--r--files/config/suckless/st/x.c3872
-rw-r--r--files/config/suckless/st/x.c.bk2405
-rw-r--r--files/config/suckless/st/x.c.orig21
-rw-r--r--files/config/suckless/st/x.c.rej96
-rw-r--r--files/config/suckless/st/x.obin76224 -> 0 bytes
42 files changed, 18789 insertions, 1800 deletions
diff --git a/autoinstall.sh b/autoinstall.sh
index 21af092..22d0fd9 100755
--- a/autoinstall.sh
+++ b/autoinstall.sh
@@ -7,7 +7,7 @@ UPDATE='sudo pacman -Syu --noconfirm'
install_packages() {
$UPDATE
- $INSTALL mpv feh redshift linux-firmware-qlogic pavucontrol picom nitrogen thunar gvfs lxappearance alsa-utils neovim yubico-pam starship fish man-db qt5ct breeze breeze-gtk redshift htop lsb-release libreoffice-fresh ly ufw scrot keepassxc ranger unzip gcr webkit2gtk gd dosfstools xorg-xkill openresolv wireguard-tools libdvdcss libdvdread dunst cryptsetup wget ncmpcpp xclip xdotool
+ $INSTALL mpv feh redshift linux-firmware-qlogic pavucontrol picom nitrogen thunar gvfs lxappearance alsa-utils neovim yubico-pam starship fish man-db qt5ct breeze breeze-gtk redshift htop lsb-release libreoffice-fresh ly ufw scrot keepassxc ranger unzip gcr webkit2gtk gd dosfstools xorg-xkill openresolv wireguard-tools libdvdcss libdvdread dunst cryptsetup wget ncmpcpp xclip xdotool xterm xorg-xclock xorg-twm okular thunderbird
echo "=============================="
echo "Programs are done installing"
echo "============================="
@@ -45,6 +45,7 @@ setup_home_directory() {
mkdir -p ~/Videos/Personal
mkdir -p ~/Music/
mkdir -p ~/Games/
+ mkdir -p ~/.config/
touch ~/.bookmarks
echo "=================="
echo "Directories setup"
@@ -53,14 +54,14 @@ setup_home_directory() {
copy_config_files() {
sudo mkdir -p /usr/share/xsessions
- sudo cp ~/dotfiles/files/dwm.desktop /usr/share/xsessions/
+ sudo cp ~/Dotfiles/files/dwm.desktop /usr/share/xsessions/
- sudo cp -r ~/dotfiles/files/pacman.conf /etc/pacman.conf
+ sudo cp -r ~/Dotfiles/files/pacman.conf /etc/pacman.conf
- sudo cp -r ~/dotfiles/files/config/* ~/.config/
- sudo cp ~/dotfiles/files/Ly/config.ini /etc/ly/config.ini
+ sudo cp -r ~/Dotfiles/files/config/* ~/.config/
+ sudo cp ~/Dotfiles/files/Ly/config.ini /etc/ly/config.ini
- cd ~/dotfiles/files/config/suckless/dwm/
+ cd ~/Dotfiles/files/config/suckless/dwm/
sudo make clean install
cd ../slstatus
sudo make clean install
@@ -76,7 +77,7 @@ copy_config_files() {
# Ranger config
ranger --copy-config=all
rm -rf ~/.config/ranger/*
- sudo cp -r ~/dotfiles/files/ranger/* ~/.config/ranger/
+ sudo cp -r ~/Dotfiles/files/ranger/* ~/.config/ranger/
# Install files for plug manager for NVIM
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
@@ -88,7 +89,7 @@ copy_config_files() {
}
fonts(){
- cp -rf ~/dotfiles/files/fonts ~/.fonts
+ cp -rf ~/Dotfiles/files/fonts ~/.fonts
}
bashrc_additions(){
@@ -137,5 +138,5 @@ main() {
#This runs the script and gives the current user perms
main
sudo chown -R $(whoami):$(whoami) /home/$(whoami)
-sh ~/dotfiles/yay.sh
+sh ~/Dotfiles/yay.sh
diff --git a/files/config/nvim/init.vim b/files/config/nvim/init.vim
index 478595e..bd66c00 100644
--- a/files/config/nvim/init.vim
+++ b/files/config/nvim/init.vim
@@ -51,3 +51,29 @@ Plug 'junegunn/goyo.vim', { 'for': 'markdown' }
Plug 'jceb/vim-orgmode'
call plug#end()
+
+function! s:goyo_enter()
+ set noshowmode
+ set noshowcmd
+ set scrolloff=999
+ set linebreak
+ " ...
+endfunction
+
+function! s:goyo_leave()
+ if executable('tmux') && strlen($TMUX)
+ silent !tmux set status on
+ silent !tmux list-panes -F '\#F' | grep -q Z && tmux resize-pane -Z
+ endif
+ set showmode
+ set showcmd
+ set scrolloff=5
+ set nolinebreak
+ " ...
+endfunction
+
+let g:goyo_width='80%'
+let g:goyo_height='80%'
+
+autocmd! User GoyoEnter nested call <SID>goyo_enter()
+autocmd! User GoyoLeave nested call <SID>goyo_leave()
diff --git a/files/config/nvim/plug/goyo.vim b/files/config/nvim/plug/goyo.vim
new file mode 160000
+Subproject fa0263d456dd43f5926484d1c4c7022dfcb21ba
diff --git a/files/config/nvim/test.md b/files/config/nvim/test.md
new file mode 100644
index 0000000..d0df633
--- /dev/null
+++ b/files/config/nvim/test.md
@@ -0,0 +1,2 @@
+test
+:>>>>>!!!
diff --git a/files/config/suckless/dwm/config.h b/files/config/suckless/dwm/config.h
index e040b41..f2f179e 100644
--- a/files/config/suckless/dwm/config.h
+++ b/files/config/suckless/dwm/config.h
@@ -65,10 +65,13 @@ static const char *termcmd[] = { "st", NULL };
static const char *audioup[] = { "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%", NULL };
static const char *audiodown[] = { "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%", NULL };
static const char *ncmpcppcmd[] = { "st", "-e", "ncmpcpp", NULL };
+static const char *emailcmd[] = { "thunderbird", NULL };
static const Key keys[] = {
/* modifier key function argument */
- { MODKEY, XK_Insert, spawn, SHCMD("xdotool type $(grep -v '^#' ~/.local/share/larbs/snippets | dmenu -i -l 50 | cut -d' ' -f1)") },
+ { MODKEY|ShiftMask, XK_Insert, spawn, SHCMD("sh ~/.Scripts/bookmark.sh") },
+ { MODKEY, XK_Insert, spawn, SHCMD("xdotool type $(grep -v '^#' ~/.bookmarks | dmenu -i -l 50 | cut -d' ' -f1)") },
+ { MODKEY, XK_t, spawn, {.v = emailcmd } },
{ MODKEY, XK_F3, spawn, {.v = audioup } },
{ MODKEY, XK_F2, spawn, {.v = audiodown } },
{ MODKEY, XK_p, spawn, {.v = audiocmd } },
diff --git a/files/config/suckless/dwm/drw.o b/files/config/suckless/dwm/drw.o
index 1776e24..c47f38a 100644
--- a/files/config/suckless/dwm/drw.o
+++ b/files/config/suckless/dwm/drw.o
Binary files differ
diff --git a/files/config/suckless/dwm/dwm b/files/config/suckless/dwm/dwm
index 0583777..9ef9b4f 100755
--- a/files/config/suckless/dwm/dwm
+++ b/files/config/suckless/dwm/dwm
Binary files differ
diff --git a/files/config/suckless/dwm/dwm.o b/files/config/suckless/dwm/dwm.o
index 759c22c..2d17a7a 100644
--- a/files/config/suckless/dwm/dwm.o
+++ b/files/config/suckless/dwm/dwm.o
Binary files differ
diff --git a/files/config/suckless/dwm/util.o b/files/config/suckless/dwm/util.o
index 0493952..6f31208 100644
--- a/files/config/suckless/dwm/util.o
+++ b/files/config/suckless/dwm/util.o
Binary files differ
diff --git a/files/config/suckless/slock b/files/config/suckless/slock
new file mode 160000
+Subproject a70d5d2429abf8dcb70a8817990975dc9a621d2
diff --git a/files/config/suckless/slstatus/config.h b/files/config/suckless/slstatus/config.h
index d66e13a..21de753 100644
--- a/files/config/suckless/slstatus/config.h
+++ b/files/config/suckless/slstatus/config.h
@@ -66,10 +66,8 @@ static const char unknown_str[] = "n/a";
static const struct arg args[] = {
/* function format argument */
-
- /*{ netspeed_rx, "  %s |", "eno0:" },
- { netspeed_tx, "  %s |", "eno0:" }, */
-
+ { netspeed_rx, "  %s |", "enp4s0" },
+ { netspeed_tx, "  %s |", "enp4s0" },
{ cpu_perc, "  CPU:%s%% |", NULL },
{ ram_perc, "  RAM:%s%% |", NULL },
{ run_command, " :%4s | ", "amixer sget Master | awk -F\"[][]\" '/%/ { print $2 }' | head -n1" },
diff --git a/files/config/suckless/slstatus/slstatus b/files/config/suckless/slstatus/slstatus
index e98836f..ffd1191 100755
--- a/files/config/suckless/slstatus/slstatus
+++ b/files/config/suckless/slstatus/slstatus
Binary files differ
diff --git a/files/config/suckless/slstatus/slstatus.o b/files/config/suckless/slstatus/slstatus.o
index ec66111..ed64649 100644
--- a/files/config/suckless/slstatus/slstatus.o
+++ b/files/config/suckless/slstatus/slstatus.o
Binary files differ
diff --git a/files/config/suckless/st/Makefile.orig b/files/config/suckless/st/Makefile.orig
index 15db421..a64b4c2 100644
--- a/files/config/suckless/st/Makefile.orig
+++ b/files/config/suckless/st/Makefile.orig
@@ -4,7 +4,7 @@
include config.mk
-SRC = st.c x.c
+SRC = st.c x.c boxdraw.c
OBJ = $(SRC:.c=.o)
all: st
@@ -17,6 +17,7 @@ config.h:
st.o: config.h st.h win.h
x.o: arg.h config.h st.h win.h
+boxdraw.o: config.h st.h boxdraw_data.h
$(OBJ): config.h config.mk
diff --git a/files/config/suckless/st/Makefile.rej b/files/config/suckless/st/Makefile.rej
new file mode 100644
index 0000000..bb559e5
--- /dev/null
+++ b/files/config/suckless/st/Makefile.rej
@@ -0,0 +1,20 @@
+--- Makefile
++++ Makefile
+@@ -4,7 +4,7 @@
+
+ include config.mk
+
+-SRC = st.c x.c
++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c
+ OBJ = $(SRC:.c=.o)
+
+ all: st
+@@ -16,7 +16,7 @@ config.h:
+ $(CC) $(STCFLAGS) -c $<
+
+ st.o: config.h st.h win.h
+-x.o: arg.h config.h st.h win.h
++x.o: arg.h config.h st.h win.h graphics.h
+
+ $(OBJ): config.h config.mk
+
diff --git a/files/config/suckless/st/boxdraw.o b/files/config/suckless/st/boxdraw.o
deleted file mode 100644
index 7bb31a9..0000000
--- a/files/config/suckless/st/boxdraw.o
+++ /dev/null
Binary files differ
diff --git a/files/config/suckless/st/config.def.h b/files/config/suckless/st/config.def.h
index 73f0706..e47e401 100644
--- a/files/config/suckless/st/config.def.h
+++ b/files/config/suckless/st/config.def.h
@@ -8,6 +8,13 @@
static char *font = "Hack Nerd Font:pixelsize=10: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;
@@ -176,18 +184,46 @@ static unsigned int mousebg = 0;
static unsigned int defaultattr = 11;
/*
+ * Graphics configuration
+ */
+
+/// The template for the cache directory.
+const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX";
+/// The max size of a single image file, in bytes.
+unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024;
+/// The max size of the cache, in bytes.
+unsigned graphics_total_file_cache_size = 300 * 1024 * 1024;
+/// The max ram size of an image or placement, in bytes.
+unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024;
+/// The max total size of all images loaded into RAM.
+unsigned graphics_max_total_ram_size = 300 * 1024 * 1024;
+/// The max total number of image placements and images.
+unsigned graphics_max_total_placements = 4096;
+/// The ratio by which limits can be exceeded. This is to reduce the frequency
+/// of image removal.
+double graphics_excess_tolerance_ratio = 0.05;
+/// The minimum delay between redraws caused by animations, in milliseconds.
+unsigned graphics_animation_min_delay = 20;
+
+/*
* Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
* Note that if you want to use ShiftMask with selmasks, set this to an other
* modifier, set to 0 to not use it.
*/
static uint forcemousemod = ShiftMask;
+/* Internal keyboard shortcuts. */
+#define MODKEY Mod1Mask
+#define TERMMOD (ControlMask|ShiftMask)
+
/*
* Internal mouse shortcuts.
* Beware that overloading Button1 will disable the selection.
*/
static MouseShortcut mshortcuts[] = {
/* mask button function argument release */
+ { TERMMOD, Button3, previewimage, {.s = "feh"} },
+ { TERMMOD, Button2, showimageinfo, {}, 1 },
{ XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
{ ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
{ XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
@@ -195,10 +231,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} },
diff --git a/files/config/suckless/st/config.def.h.orig b/files/config/suckless/st/config.def.h.orig
new file mode 100644
index 0000000..73f0706
--- /dev/null
+++ b/files/config/suckless/st/config.def.h.orig
@@ -0,0 +1,488 @@
+/* See LICENSE file for copyright and license details. */
+
+/*
+ * appearance
+ *
+ * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html
+ */
+static char *font = "Hack Nerd Font:pixelsize=10:antialias=true:autohint=true";
+static int borderpx = 2;
+
+/*
+ * 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 */
+char *vtiden = "\033[?6c";
+
+/* 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;
+
+/*
+ * 1: render most of the lines/blocks characters without using the font for
+ * perfect alignment between cells (U2500 - U259F except dashes/diagonals).
+ * Bold affects lines thickness if boxdraw_bold is not 0. Italic is ignored.
+ * 0: disable (render all U25XX glyphs normally from the font).
+ */
+const int boxdraw = 0;
+const int boxdraw_bold = 0;
+
+/* braille (U28XX): 1: render as adjacent "pixels", 0: use font */
+const int boxdraw_braille = 0;
+
+/*
+ * bell volume. It must be a value between -100 and 100. Use 0 for disabling
+ * it
+ */
+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;
+
+/*
+ * 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 mouse shortcuts.
+ * Beware that overloading Button1 will disable the selection.
+ */
+static MouseShortcut mshortcuts[] = {
+ /* mask button function argument release */
+ { 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"} },
+};
+
+/* 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} },
+ { 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} },
+ { ShiftMask, XK_Page_Up, kscrollup, {.i = -1} },
+ { ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} },
+};
+
+/*
+ * 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/files/config/suckless/st/config.def.h.rej b/files/config/suckless/st/config.def.h.rej
new file mode 100644
index 0000000..faaaae1
--- /dev/null
+++ b/files/config/suckless/st/config.def.h.rej
@@ -0,0 +1,13 @@
+--- config.def.h
++++ config.def.h
+@@ -233,6 +265,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/files/config/suckless/st/config.mk b/files/config/suckless/st/config.mk
index fdc29a7..cb2875c 100644
--- a/files/config/suckless/st/config.mk
+++ b/files/config/suckless/st/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/files/config/suckless/st/graphics.c b/files/config/suckless/st/graphics.c
new file mode 100644
index 0000000..64e6fe0
--- /dev/null
+++ b/files/config/suckless/st/graphics.c
@@ -0,0 +1,3812 @@
+/* The MIT License
+
+ Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// This file implements a subset of the kitty graphics protocol.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <zlib.h>
+#include <Imlib2.h>
+#include <X11/Xlib.h>
+#include <X11/extensions/Xrender.h>
+#include <assert.h>
+#include <ctype.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "graphics.h"
+#include "khash.h"
+#include "kvec.h"
+
+extern char **environ;
+
+#define MAX_FILENAME_SIZE 256
+#define MAX_INFO_LEN 256
+#define MAX_IMAGE_RECTS 20
+
+/// The type used in this file to represent time. Used both for time differences
+/// and absolute times (as milliseconds since an arbitrary point in time, see
+/// `initialization_time`).
+typedef int64_t Milliseconds;
+
+enum ScaleMode {
+ SCALE_MODE_UNSET = 0,
+ /// Stretch or shrink the image to fill the box, ignoring aspect ratio.
+ SCALE_MODE_FILL = 1,
+ /// Preserve aspect ratio and fit to width or to height so that the
+ /// whole image is visible.
+ SCALE_MODE_CONTAIN = 2,
+ /// Do not scale. The image may be cropped if the box is too small.
+ SCALE_MODE_NONE = 3,
+ /// Do not scale, unless the box is too small, in which case the image
+ /// will be shrunk like with `SCALE_MODE_CONTAIN`.
+ SCALE_MODE_NONE_OR_CONTAIN = 4,
+};
+
+enum AnimationState {
+ ANIMATION_STATE_UNSET = 0,
+ /// The animation is stopped. Display the current frame, but don't
+ /// advance to the next one.
+ ANIMATION_STATE_STOPPED = 1,
+ /// Run the animation to then end, then wait for the next frame.
+ ANIMATION_STATE_LOADING = 2,
+ /// Run the animation in a loop.
+ ANIMATION_STATE_LOOPING = 3,
+};
+
+/// The status of an image. Each image uploaded to the terminal is cached on
+/// disk, then it is loaded to ram when needed.
+enum ImageStatus {
+ STATUS_UNINITIALIZED = 0,
+ STATUS_UPLOADING = 1,
+ STATUS_UPLOADING_ERROR = 2,
+ STATUS_UPLOADING_SUCCESS = 3,
+ STATUS_RAM_LOADING_ERROR = 4,
+ STATUS_RAM_LOADING_IN_PROGRESS = 5,
+ STATUS_RAM_LOADING_SUCCESS = 6,
+};
+
+const char *image_status_strings[6] = {
+ "STATUS_UNINITIALIZED",
+ "STATUS_UPLOADING",
+ "STATUS_UPLOADING_ERROR",
+ "STATUS_UPLOADING_SUCCESS",
+ "STATUS_RAM_LOADING_ERROR",
+ "STATUS_RAM_LOADING_SUCCESS",
+};
+
+enum ImageUploadingFailure {
+ ERROR_OVER_SIZE_LIMIT = 1,
+ ERROR_CANNOT_OPEN_CACHED_FILE = 2,
+ ERROR_UNEXPECTED_SIZE = 3,
+ ERROR_CANNOT_COPY_FILE = 4,
+};
+
+const char *image_uploading_failure_strings[5] = {
+ "NO_ERROR",
+ "ERROR_OVER_SIZE_LIMIT",
+ "ERROR_CANNOT_OPEN_CACHED_FILE",
+ "ERROR_UNEXPECTED_SIZE",
+ "ERROR_CANNOT_COPY_FILE",
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// We use the following structures to represent images and placements:
+//
+// - Image: this is the main structure representing an image, usually created
+// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id,
+// specified by 'i='). An image may have multiple frames (ImageFrame) and
+// placements (ImagePlacement).
+//
+// - ImageFrame: represents a single frame of an image, usually created by
+// the action 'a=f' (and the first frame is created with the image itself).
+// Each frame has an index and also:
+// - a file containing the frame data (considered to be "on disk", although
+// it's probably in tmpfs),
+// - an imlib object containing the fully composed frame (i.e. the frame
+// data from the file composed onto the background frame or color). It is
+// not ready for display yet, because it needs to be scaled and uploaded
+// to the X server.
+//
+// - ImagePlacement: represents a placement of an image, created by 'a=p' and
+// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also
+// each placement has an array of pixmaps: one for each frame of the image.
+// Each pixmap is a scaled and uploaded image ready to be displayed.
+//
+// Images are store in the `images` hash table, mapping image ids to Image
+// objects (allocated on the heap).
+//
+// Placements are stored in the `placements` hash table of each Image object,
+// mapping placement ids to ImagePlacement objects (also allocated on the heap).
+//
+// ImageFrames are stored in the `first_frame` field and in the
+// `frames_beyond_the_first` array of each Image object. They are stored by
+// value, so ImageFrame pointer may be invalidated when frames are
+// added/deleted, be careful.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+struct Image;
+struct ImageFrame;
+struct ImagePlacement;
+
+KHASH_MAP_INIT_INT(id2image, struct Image *)
+KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *)
+
+typedef struct ImageFrame {
+ /// The image this frame belongs to.
+ struct Image *image;
+ /// The 1-based index of the frame. Zero if the frame isn't initialized.
+ int index;
+ /// The last time when the frame was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The background color of the frame in the 0xRRGGBBAA format.
+ uint32_t background_color;
+ /// The index of the background frame. Zero to use the color instead.
+ int background_frame_index;
+ /// The duration of the frame in milliseconds.
+ int gap;
+ /// The expected size of the frame image file (specified with 'S='),
+ /// used to check if uploading succeeded.
+ unsigned expected_size;
+ /// Format specification (see the `f=` key).
+ int format;
+ /// Pixel width and height of the non-composed (on-disk) frame data. May
+ /// differ from the image (i.e. first frame) dimensions.
+ int data_pix_width, data_pix_height;
+ /// The offset of the frame relative to the first frame.
+ int x, y;
+ /// Compression mode (see the `o=` key).
+ char compression;
+ /// The status (see `ImageStatus`).
+ char status;
+ /// The reason of uploading failure (see `ImageUploadingFailure`).
+ char uploading_failure;
+ /// Whether failures and successes should be reported ('q=').
+ char quiet;
+ /// Whether to blend the frame with the background or replace it.
+ char blend;
+ /// The file corresponding to the on-disk cache, used when uploading.
+ FILE *open_file;
+ /// The size of the corresponding file cached on disk.
+ unsigned disk_size;
+ /// The imlib object containing the fully composed frame. It's not
+ /// scaled for screen display yet.
+ Imlib_Image imlib_object;
+} ImageFrame;
+
+typedef struct Image {
+ /// The client id (the one specified with 'i='). Must be nonzero.
+ uint32_t image_id;
+ /// The client id specified in the query command (`a=q`). This one must
+ /// be used to create the response if it's non-zero.
+ uint32_t query_id;
+ /// The number specified in the transmission command (`I=`). If
+ /// non-zero, it may be used to identify the image instead of the
+ /// image_id, and it also should be mentioned in responses.
+ uint32_t image_number;
+ /// The last time when the image was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The total duration of the animation in milliseconds.
+ int total_duration;
+ /// The total size of cached image files for all frames.
+ int total_disk_size;
+ /// The global index of the creation command. Used to decide which image
+ /// is newer if they have the same image number.
+ uint64_t global_command_index;
+ /// The 1-based index of the currently displayed frame.
+ int current_frame;
+ /// The state of the animation, see `AnimationState`.
+ char animation_state;
+ /// The absolute time that is assumed to be the start of the current
+ /// frame (in ms since initialization).
+ Milliseconds current_frame_time;
+ /// The absolute time of the last redraw (in ms since initialization).
+ /// Used to check whether it's the first time we draw the image in the
+ /// current redraw cycle.
+ Milliseconds last_redraw;
+ /// The absolute time of the next redraw (in ms since initialization).
+ /// 0 means no redraw is scheduled.
+ Milliseconds next_redraw;
+ /// The unscaled pixel width and height of the image. Usually inherited
+ /// from the first frame.
+ int pix_width, pix_height;
+ /// The first frame.
+ ImageFrame first_frame;
+ /// The array of frames beyond the first one.
+ kvec_t(ImageFrame) frames_beyond_the_first;
+ /// Image placements.
+ khash_t(id2placement) *placements;
+ /// The default placement.
+ uint32_t default_placement;
+ /// The initial placement id, specified with the transmission command,
+ /// used to report success or failure.
+ uint32_t initial_placement_id;
+} Image;
+
+typedef struct ImagePlacement {
+ /// The image this placement belongs to.
+ Image *image;
+ /// The id of the placement. Must be nonzero.
+ uint32_t placement_id;
+ /// The last time when the placement was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The 1-based index of the protected pixmap. We protect a pixmap in
+ /// gr_load_pixmap to avoid unloading it right after it was loaded.
+ int protected_frame;
+ /// Whether the placement is used only for Unicode placeholders.
+ char virtual;
+ /// The scaling mode (see `ScaleMode`).
+ char scale_mode;
+ /// Height and width in cells.
+ uint16_t rows, cols;
+ /// Top-left corner of the source rectangle ('x=' and 'y=').
+ int src_pix_x, src_pix_y;
+ /// Height and width of the source rectangle (zero if full image).
+ int src_pix_width, src_pix_height;
+ /// The image appropriately scaled and uploaded to the X server. This
+ /// pixmap is premultiplied by alpha.
+ Pixmap first_pixmap;
+ /// The array of pixmaps beyond the first one.
+ kvec_t(Pixmap) pixmaps_beyond_the_first;
+ /// The dimensions of the cell used to scale the image. If cell
+ /// dimensions are changed (font change), the image will be rescaled.
+ uint16_t scaled_cw, scaled_ch;
+ /// If true, do not move the cursor when displaying this placement
+ /// (non-virtual placements only).
+ char do_not_move_cursor;
+} ImagePlacement;
+
+/// A rectangular piece of an image to be drawn.
+typedef struct {
+ uint32_t image_id;
+ uint32_t placement_id;
+ /// The position of the rectangle in pixels.
+ int screen_x_pix, screen_y_pix;
+ /// The starting row on the screen.
+ int screen_y_row;
+ /// The part of the whole image to be drawn, in cells. Starts are
+ /// zero-based, ends are exclusive.
+ int img_start_col, img_end_col, img_start_row, img_end_row;
+ /// The current cell width and height in pixels.
+ int cw, ch;
+ /// Whether colors should be inverted.
+ int reverse;
+} ImageRect;
+
+/// Executes `code` for each frame of an image. Example:
+///
+/// foreach_frame(image, frame, {
+/// printf("Frame %d\n", frame->index);
+/// });
+///
+#define foreach_frame(image, framevar, code) { size_t __i; \
+ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \
+ ImageFrame *framevar = \
+ __i == 0 ? &(image).first_frame \
+ : &kv_A((image).frames_beyond_the_first, __i - 1); \
+ code; \
+ } }
+
+/// Executes `code` for each pixmap of a placement. Example:
+///
+/// foreach_pixmap(placement, pixmap, {
+/// ...
+/// });
+///
+#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \
+ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \
+ Pixmap pixmapvar = \
+ __i == 0 ? (placement).first_pixmap \
+ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \
+ code; \
+ } }
+
+
+static Image *gr_find_image(uint32_t image_id);
+static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len);
+static void gr_delete_image(Image *img);
+static void gr_check_limits();
+static char *gr_base64dec(const char *src, size_t *size);
+static void sanitize_str(char *str, size_t max_len);
+static const char *sanitized_filename(const char *str);
+
+/// The array of image rectangles to draw. It is reset each frame.
+static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}};
+/// The known images (including the ones being uploaded).
+static khash_t(id2image) *images = NULL;
+/// The total number of placements in all images.
+static unsigned total_placement_count = 0;
+/// The total size of all image files stored in the on-disk cache.
+static int64_t images_disk_size = 0;
+/// The total size of all images and placements loaded into ram.
+static int64_t images_ram_size = 0;
+/// The id of the last loaded image.
+static uint32_t last_image_id = 0;
+/// Current cell width and heigh in pixels.
+static int current_cw = 0, current_ch = 0;
+/// The id of the currently uploaded image (when using direct uploading).
+static uint32_t current_upload_image_id = 0;
+/// The index of the frame currently being uploaded.
+static int current_upload_frame_index = 0;
+/// The time when the graphics module was initialized.
+static struct timespec initialization_time = {0};
+/// The time when the current frame drawing started, used for debugging fps and
+/// to calculate the current frame for animations.
+static Milliseconds drawing_start_time;
+/// The global index of the current command.
+static uint64_t global_command_counter = 0;
+/// The next redraw times for each row of the terminal. Used for animations.
+/// 0 means no redraw is scheduled.
+static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL};
+/// The number of files loaded in the current redraw cycle.
+static int this_redraw_cycle_loaded_files = 0;
+/// The number of pixmaps loaded in the current redraw cycle.
+static int this_redraw_cycle_loaded_pixmaps = 0;
+
+/// The directory where the cache files are stored.
+static char cache_dir[MAX_FILENAME_SIZE - 16];
+
+/// The table used for color inversion.
+static unsigned char reverse_table[256];
+
+// Declared in the header.
+GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE;
+char graphics_display_images = 1;
+GraphicsCommandResult graphics_command_result = {0};
+int graphics_next_redraw_delay = INT_MAX;
+
+// Defined in config.h
+extern const char graphics_cache_dir_template[];
+extern unsigned graphics_max_single_image_file_size;
+extern unsigned graphics_total_file_cache_size;
+extern unsigned graphics_max_single_image_ram_size;
+extern unsigned graphics_max_total_ram_size;
+extern unsigned graphics_max_total_placements;
+extern double graphics_excess_tolerance_ratio;
+extern unsigned graphics_animation_min_delay;
+
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic helpers.
+////////////////////////////////////////////////////////////////////////////////
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) < (b) ? (b) : (a))
+
+/// Returns the difference between `end` and `start` in milliseconds.
+static int64_t gr_timediff_ms(const struct timespec *end,
+ const struct timespec *start) {
+ return (end->tv_sec - start->tv_sec) * 1000 +
+ (end->tv_nsec - start->tv_nsec) / 1000000;
+}
+
+/// Returns the current time in milliseconds since the initialization.
+static Milliseconds gr_now_ms() {
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ return gr_timediff_ms(&now, &initialization_time);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Logging.
+////////////////////////////////////////////////////////////////////////////////
+
+#define GR_LOG(...) \
+ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0)
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic image management functions (create, delete, find, etc).
+////////////////////////////////////////////////////////////////////////////////
+
+/// Returns the 1-based index of the last frame. Note that you may want to use
+/// `gr_last_uploaded_frame_index` instead since the last frame may be not
+/// fully uploaded yet.
+static inline int gr_last_frame_index(Image *img) {
+ return kv_size(img->frames_beyond_the_first) + 1;
+}
+
+/// Returns the frame with the given index. Returns NULL if the index is out of
+/// bounds. The index is 1-based.
+static ImageFrame *gr_get_frame(Image *img, int index) {
+ if (!img)
+ return NULL;
+ if (index == 1)
+ return &img->first_frame;
+ if (2 <= index && index <= gr_last_frame_index(img))
+ return &kv_A(img->frames_beyond_the_first, index - 2);
+ return NULL;
+}
+
+/// Returns the last frame of the image. Returns NULL if `img` is NULL.
+static ImageFrame *gr_get_last_frame(Image *img) {
+ if (!img)
+ return NULL;
+ return gr_get_frame(img, gr_last_frame_index(img));
+}
+
+/// Returns the 1-based index of the last frame or the second-to-last frame if
+/// the last frame is not fully uploaded yet.
+static inline int gr_last_uploaded_frame_index(Image *img) {
+ int last_index = gr_last_frame_index(img);
+ if (last_index > 1 &&
+ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS)
+ return last_index - 1;
+ return last_index;
+}
+
+/// Returns the pixmap for the frame with the given index. Returns 0 if the
+/// index is out of bounds. The index is 1-based.
+static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) {
+ if (index == 1)
+ return placement->first_pixmap;
+ if (2 <= index &&
+ index <= kv_size(placement->pixmaps_beyond_the_first) + 1)
+ return kv_A(placement->pixmaps_beyond_the_first, index - 2);
+ return 0;
+}
+
+/// Sets the pixmap for the frame with the given index. The index is 1-based.
+/// The array of pixmaps is resized if needed.
+static void gr_set_frame_pixmap(ImagePlacement *placement, int index,
+ Pixmap pixmap) {
+ if (index == 1) {
+ placement->first_pixmap = pixmap;
+ return;
+ }
+ // Resize the array if needed.
+ size_t old_size = kv_size(placement->pixmaps_beyond_the_first);
+ if (old_size < index - 1) {
+ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2);
+ for (size_t i = old_size; i < index - 1; i++)
+ kv_A(placement->pixmaps_beyond_the_first, i) = 0;
+ }
+ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap;
+}
+
+/// Finds the image corresponding to the client id. Returns NULL if cannot find.
+static Image *gr_find_image(uint32_t image_id) {
+ khiter_t k = kh_get(id2image, images, image_id);
+ if (k == kh_end(images))
+ return NULL;
+ Image *res = kh_value(images, k);
+ return res;
+}
+
+/// Finds the newest image corresponding to the image number. Returns NULL if
+/// cannot find.
+static Image *gr_find_image_by_number(uint32_t image_number) {
+ if (image_number == 0)
+ return NULL;
+ Image *newest_img = NULL;
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ if (img->image_number == image_number &&
+ (!newest_img || newest_img->global_command_index <
+ img->global_command_index))
+ newest_img = img;
+ });
+ if (!newest_img)
+ GR_LOG("Image number %u not found\n", image_number);
+ else
+ GR_LOG("Found image number %u, its id is %u\n", image_number,
+ img->image_id);
+ return newest_img;
+}
+
+/// Finds the placement corresponding to the id. If the placement id is 0,
+/// returns some default placement.
+static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) {
+ if (!img)
+ return NULL;
+ if (placement_id == 0) {
+ // Try to get the default placement.
+ ImagePlacement *dflt = NULL;
+ if (img->default_placement != 0)
+ dflt = gr_find_placement(img, img->default_placement);
+ if (dflt)
+ return dflt;
+ // If there is no default placement, return the first one and
+ // set it as the default.
+ kh_foreach_value(img->placements, dflt, {
+ img->default_placement = dflt->placement_id;
+ return dflt;
+ });
+ // If there are no placements, return NULL.
+ return NULL;
+ }
+ khiter_t k = kh_get(id2placement, img->placements, placement_id);
+ if (k == kh_end(img->placements))
+ return NULL;
+ ImagePlacement *res = kh_value(img->placements, k);
+ return res;
+}
+
+/// Finds the placement by image id and placement id.
+static ImagePlacement *gr_find_image_and_placement(uint32_t image_id,
+ uint32_t placement_id) {
+ return gr_find_placement(gr_find_image(image_id), placement_id);
+}
+
+/// Writes the name of the on-disk cache file to `out`. `max_len` should be the
+/// size of `out`. The name will be something like
+/// "/tmp/st-images-xxx/img-ID-FRAME".
+static void gr_get_frame_filename(ImageFrame *frame, char *out,
+ size_t max_len) {
+ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir,
+ frame->image->image_id, frame->index);
+}
+
+/// Returns the (estimation) of the RAM size used by the frame right now.
+static unsigned gr_frame_current_ram_size(ImageFrame *frame) {
+ if (!frame->imlib_object)
+ return 0;
+ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4;
+}
+
+/// Returns the (estimation) of the RAM size used by a single frame pixmap.
+static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) {
+ return (unsigned)placement->rows * placement->cols *
+ placement->scaled_ch * placement->scaled_cw * 4;
+}
+
+/// Returns the (estimation) of the RAM size used by the placemenet right now.
+static unsigned gr_placement_current_ram_size(ImagePlacement *placement) {
+ unsigned single_frame_size =
+ gr_placement_single_frame_ram_size(placement);
+ unsigned result = 0;
+ foreach_pixmap(*placement, pixmap, {
+ if (pixmap)
+ result += single_frame_size;
+ });
+ return result;
+}
+
+/// Unload the frame from RAM (i.e. delete the corresponding imlib object).
+/// If the on-disk file of the frame is preserved, it can be reloaded later.
+static void gr_unload_frame(ImageFrame *frame) {
+ if (!frame->imlib_object)
+ return;
+
+ unsigned frame_ram_size = gr_frame_current_ram_size(frame);
+ images_ram_size -= frame_ram_size;
+
+ imlib_context_set_image(frame->imlib_object);
+ imlib_free_image_and_decache();
+ frame->imlib_object = NULL;
+
+ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ frame->image->image_id, frame->index,
+ drawing_start_time - frame->atime, images_ram_size / 1024,
+ frame_ram_size / 1024);
+}
+
+/// Unload all frames of the image.
+static void gr_unload_all_frames(Image *img) {
+ foreach_frame(*img, frame, {
+ gr_unload_frame(frame);
+ });
+}
+
+/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps).
+/// If the on-disk files or imlib objects of the corresponding image are
+/// preserved, the placement can be reloaded later.
+static void gr_unload_placement(ImagePlacement *placement) {
+ unsigned placement_ram_size = gr_placement_current_ram_size(placement);
+ images_ram_size -= placement_ram_size;
+
+ Display *disp = imlib_context_get_display();
+ foreach_pixmap(*placement, pixmap, {
+ if (pixmap)
+ XFreePixmap(disp, pixmap);
+ });
+
+ placement->first_pixmap = 0;
+ placement->pixmaps_beyond_the_first.n = 0;
+ placement->scaled_ch = placement->scaled_cw = 0;
+
+ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ placement->image->image_id, placement->placement_id,
+ drawing_start_time - placement->atime, images_ram_size / 1024,
+ placement_ram_size / 1024);
+}
+
+/// Unload a single pixmap of the placement from RAM.
+static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) {
+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
+ if (!pixmap)
+ return;
+
+ Display *disp = imlib_context_get_display();
+ XFreePixmap(disp, pixmap);
+ gr_set_frame_pixmap(placement, frameidx, 0);
+ images_ram_size -= gr_placement_single_frame_ram_size(placement);
+
+ GR_LOG("After unloading pixmap %ld of "
+ "placement %u/%u (atime %ld ms ago) "
+ "frame %u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ pixmap, placement->image->image_id, placement->placement_id,
+ drawing_start_time - placement->atime, frameidx,
+ drawing_start_time -
+ gr_get_frame(placement->image, frameidx)->atime,
+ images_ram_size / 1024,
+ gr_placement_single_frame_ram_size(placement) / 1024);
+}
+
+/// Deletes the on-disk cache file corresponding to the frame. The in-ram image
+/// object (if it exists) is not deleted, placements are not unloaded either.
+static void gr_delete_imagefile(ImageFrame *frame) {
+ // It may still be being loaded. Close the file in this case.
+ if (frame->open_file) {
+ fclose(frame->open_file);
+ frame->open_file = NULL;
+ }
+
+ if (frame->disk_size == 0)
+ return;
+
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ remove(filename);
+
+ unsigned disk_size = frame->disk_size;
+ images_disk_size -= disk_size;
+ frame->image->total_disk_size -= disk_size;
+ frame->disk_size = 0;
+
+ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) "
+ "disk: %ld KiB (- %u KiB)\n",
+ frame->image->image_id, frame->index,
+ drawing_start_time - frame->atime, images_disk_size / 1024,
+ disk_size / 1024);
+}
+
+/// Deletes all on-disk cache files of the image (for each frame).
+static void gr_delete_imagefiles(Image *img) {
+ foreach_frame(*img, frame, {
+ gr_delete_imagefile(frame);
+ });
+}
+
+/// Deletes the given placement: unloads, frees the object, but doesn't change
+/// the `placements` hash table.
+static void gr_delete_placement_keep_id(ImagePlacement *placement) {
+ if (!placement)
+ return;
+ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id,
+ placement->placement_id);
+ gr_unload_placement(placement);
+ kv_destroy(placement->pixmaps_beyond_the_first);
+ free(placement);
+ total_placement_count--;
+}
+
+/// Deletes all placements of `img`.
+static void gr_delete_all_placements(Image *img) {
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(img->placements, placement, {
+ gr_delete_placement_keep_id(placement);
+ });
+ kh_clear(id2placement, img->placements);
+}
+
+/// Deletes the given image: unloads, deletes the file, frees the Image object,
+/// but doesn't change the `images` hash table.
+static void gr_delete_image_keep_id(Image *img) {
+ if (!img)
+ return;
+ GR_LOG("Deleting image %u\n", img->image_id);
+ foreach_frame(*img, frame, {
+ gr_delete_imagefile(frame);
+ gr_unload_frame(frame);
+ });
+ kv_destroy(img->frames_beyond_the_first);
+ gr_delete_all_placements(img);
+ kh_destroy(id2placement, img->placements);
+ free(img);
+}
+
+/// Deletes the given image: unloads, deletes the file, frees the Image object,
+/// and also removes it from `images`.
+static void gr_delete_image(Image *img) {
+ if (!img)
+ return;
+ uint32_t id = img->image_id;
+ gr_delete_image_keep_id(img);
+ khiter_t k = kh_get(id2image, images, id);
+ kh_del(id2image, images, k);
+}
+
+/// Deletes the given placement: unloads, frees the object, and also removes it
+/// from `placements`.
+static void gr_delete_placement(ImagePlacement *placement) {
+ if (!placement)
+ return;
+ uint32_t id = placement->placement_id;
+ Image *img = placement->image;
+ gr_delete_placement_keep_id(placement);
+ khiter_t k = kh_get(id2placement, img->placements, id);
+ kh_del(id2placement, img->placements, k);
+}
+
+/// Deletes all images and clears `images`.
+static void gr_delete_all_images() {
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ gr_delete_image_keep_id(img);
+ });
+ kh_clear(id2image, images);
+}
+
+/// Update the atime of the image.
+static void gr_touch_image(Image *img) {
+ img->atime = gr_now_ms();
+}
+
+/// Update the atime of the frame.
+static void gr_touch_frame(ImageFrame *frame) {
+ frame->image->atime = frame->atime = gr_now_ms();
+}
+
+/// Update the atime of the placement. Touches the images too.
+static void gr_touch_placement(ImagePlacement *placement) {
+ placement->image->atime = placement->atime = gr_now_ms();
+}
+
+/// Creates a new image with the given id. If an image with that id already
+/// exists, it is deleted first. If the provided id is 0, generates a
+/// random id.
+static Image *gr_new_image(uint32_t id) {
+ if (id == 0) {
+ do {
+ id = rand();
+ // Avoid IDs that don't need full 32 bits.
+ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 ||
+ gr_find_image(id));
+ GR_LOG("Generated random image id %u\n", id);
+ }
+ Image *img = gr_find_image(id);
+ gr_delete_image_keep_id(img);
+ GR_LOG("Creating image %u\n", id);
+ img = malloc(sizeof(Image));
+ memset(img, 0, sizeof(Image));
+ img->placements = kh_init(id2placement);
+ int ret;
+ khiter_t k = kh_put(id2image, images, id, &ret);
+ kh_value(images, k) = img;
+ img->image_id = id;
+ gr_touch_image(img);
+ img->global_command_index = global_command_counter;
+ return img;
+}
+
+/// Creates a new frame at the end of the frame array. It may be the first frame
+/// if there are no frames yet.
+static ImageFrame *gr_append_new_frame(Image *img) {
+ ImageFrame *frame = NULL;
+ if (img->first_frame.index == 0 &&
+ kv_size(img->frames_beyond_the_first) == 0) {
+ frame = &img->first_frame;
+ frame->index = 1;
+ } else {
+ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first);
+ memset(frame, 0, sizeof(ImageFrame));
+ frame->index = kv_size(img->frames_beyond_the_first) + 1;
+ }
+ frame->image = img;
+ gr_touch_frame(frame);
+ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id);
+ return frame;
+}
+
+/// Creates a new placement with the given id. If a placement with that id
+/// already exists, it is deleted first. If the provided id is 0, generates a
+/// random id.
+static ImagePlacement *gr_new_placement(Image *img, uint32_t id) {
+ if (id == 0) {
+ do {
+ // Currently we support only 24-bit IDs.
+ id = rand() & 0xFFFFFF;
+ // Avoid IDs that need only one byte.
+ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id));
+ }
+ ImagePlacement *placement = gr_find_placement(img, id);
+ gr_delete_placement_keep_id(placement);
+ GR_LOG("Creating placement %u/%u\n", img->image_id, id);
+ placement = malloc(sizeof(ImagePlacement));
+ memset(placement, 0, sizeof(ImagePlacement));
+ total_placement_count++;
+ int ret;
+ khiter_t k = kh_put(id2placement, img->placements, id, &ret);
+ kh_value(img->placements, k) = placement;
+ placement->image = img;
+ placement->placement_id = id;
+ gr_touch_placement(placement);
+ if (img->default_placement == 0)
+ img->default_placement = id;
+ return placement;
+}
+
+static int64_t ceil_div(int64_t a, int64_t b) {
+ return (a + b - 1) / b;
+}
+
+/// Computes the best number of rows and columns for a placement if it's not
+/// specified, and also adjusts the source rectangle size.
+static void gr_infer_placement_size_maybe(ImagePlacement *placement) {
+ // The size of the image.
+ int image_pix_width = placement->image->pix_width;
+ int image_pix_height = placement->image->pix_height;
+ // Negative values are not allowed. Quietly set them to 0.
+ if (placement->src_pix_x < 0)
+ placement->src_pix_x = 0;
+ if (placement->src_pix_y < 0)
+ placement->src_pix_y = 0;
+ if (placement->src_pix_width < 0)
+ placement->src_pix_width = 0;
+ if (placement->src_pix_height < 0)
+ placement->src_pix_height = 0;
+ // If the source rectangle is outside the image, truncate it.
+ if (placement->src_pix_x > image_pix_width)
+ placement->src_pix_x = image_pix_width;
+ if (placement->src_pix_y > image_pix_height)
+ placement->src_pix_y = image_pix_height;
+ // If the source rectangle is not specified, use the whole image. If
+ // it's partially outside the image, truncate it.
+ if (placement->src_pix_width == 0 ||
+ placement->src_pix_x + placement->src_pix_width > image_pix_width)
+ placement->src_pix_width =
+ image_pix_width - placement->src_pix_x;
+ if (placement->src_pix_height == 0 ||
+ placement->src_pix_y + placement->src_pix_height > image_pix_height)
+ placement->src_pix_height =
+ image_pix_height - placement->src_pix_y;
+
+ if (placement->cols != 0 && placement->rows != 0)
+ return;
+ if (placement->src_pix_width == 0 || placement->src_pix_height == 0)
+ return;
+ if (current_cw == 0 || current_ch == 0)
+ return;
+
+ // If no size is specified, use the image size.
+ if (placement->cols == 0 && placement->rows == 0) {
+ placement->cols =
+ ceil_div(placement->src_pix_width, current_cw);
+ placement->rows =
+ ceil_div(placement->src_pix_height, current_ch);
+ return;
+ }
+
+ // Some applications specify only one of the dimensions.
+ if (placement->scale_mode == SCALE_MODE_CONTAIN) {
+ // If we preserve aspect ratio and fit to width/height, the most
+ // logical thing is to find the minimum size of the
+ // non-specified dimension that allows the image to fit the
+ // specified dimension.
+ if (placement->cols == 0) {
+ placement->cols = ceil_div(
+ placement->src_pix_width * placement->rows *
+ current_ch,
+ placement->src_pix_height * current_cw);
+ return;
+ }
+ if (placement->rows == 0) {
+ placement->rows =
+ ceil_div(placement->src_pix_height *
+ placement->cols * current_cw,
+ placement->src_pix_width * current_ch);
+ return;
+ }
+ } else {
+ // Otherwise we stretch the image or preserve the original size.
+ // In both cases we compute the best number of columns from the
+ // pixel size and cell size.
+ // TODO: In the case of stretching it's not the most logical
+ // thing to do, may need to revisit in the future.
+ // Currently we switch to SCALE_MODE_CONTAIN when only one
+ // of the dimensions is specified, so this case shouldn't
+ // happen in practice.
+ if (!placement->cols)
+ placement->cols =
+ ceil_div(placement->src_pix_width, current_cw);
+ if (!placement->rows)
+ placement->rows =
+ ceil_div(placement->src_pix_height, current_ch);
+ }
+}
+
+/// Adjusts the current frame index if enough time has passed since the display
+/// of the current frame. Also computes the time of the next redraw of this
+/// image (`img->next_redraw`). The current time is passed as an argument so
+/// that all animations are in sync.
+static void gr_update_frame_index(Image *img, Milliseconds now) {
+ if (img->current_frame == 0) {
+ img->current_frame_time = now;
+ img->current_frame = 1;
+ img->next_redraw = now + MAX(1, img->first_frame.gap);
+ return;
+ }
+ // If the animation is stopped, show the current frame.
+ if (!img->animation_state ||
+ img->animation_state == ANIMATION_STATE_STOPPED ||
+ img->animation_state == ANIMATION_STATE_UNSET) {
+ // The next redraw is never (unless the state is changed).
+ img->next_redraw = 0;
+ return;
+ }
+ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img);
+ // If we are loading and we reached the last frame, show the last frame.
+ if (img->animation_state == ANIMATION_STATE_LOADING &&
+ img->current_frame == last_uploaded_frame_index) {
+ // The next redraw is never (unless the state is changed or
+ // frames are added).
+ img->next_redraw = 0;
+ return;
+ }
+
+ // Check how many milliseconds passed since the current frame was shown.
+ int passed_ms = now - img->current_frame_time;
+ // If the animation is looping and too much time has passes, we can
+ // make a shortcut.
+ if (img->animation_state == ANIMATION_STATE_LOOPING &&
+ img->total_duration > 0 && passed_ms >= img->total_duration) {
+ passed_ms %= img->total_duration;
+ img->current_frame_time = now - passed_ms;
+ }
+ // Find the next frame.
+ int original_frame_index = img->current_frame;
+ while (1) {
+ ImageFrame *frame = gr_get_frame(img, img->current_frame);
+ if (!frame) {
+ // The frame doesn't exist, go to the first frame.
+ img->current_frame = 1;
+ img->current_frame_time = now;
+ img->next_redraw = now + MAX(1, img->first_frame.gap);
+ return;
+ }
+ if (frame->gap >= 0 && passed_ms < frame->gap) {
+ // Not enough time has passed, we are still in the same
+ // frame, and it's not a gapless frame.
+ img->next_redraw =
+ img->current_frame_time + MAX(1, frame->gap);
+ return;
+ }
+ // Otherwise go to the next frame.
+ passed_ms -= MAX(0, frame->gap);
+ if (img->current_frame >= last_uploaded_frame_index) {
+ // It's the last frame, if the animation is loading,
+ // remain on it.
+ if (img->animation_state == ANIMATION_STATE_LOADING) {
+ img->next_redraw = 0;
+ return;
+ }
+ // Otherwise the animation is looping.
+ img->current_frame = 1;
+ // TODO: Support finite number of loops.
+ } else {
+ img->current_frame++;
+ }
+ // Make sure we don't get stuck in an infinite loop.
+ if (img->current_frame == original_frame_index) {
+ // We looped through all frames, but haven't reached the
+ // next frame yet. This may happen if too much time has
+ // passed since the last redraw or all the frames are
+ // gapless. Just move on to the next frame.
+ img->current_frame++;
+ if (img->current_frame >
+ last_uploaded_frame_index)
+ img->current_frame = 1;
+ img->current_frame_time = now;
+ img->next_redraw = now + MAX(
+ 1, gr_get_frame(img, img->current_frame)->gap);
+ return;
+ }
+ // Adjust the start time of the frame. The next redraw time will
+ // be set in the next iteration.
+ img->current_frame_time += MAX(0, frame->gap);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Unloading and deleting images to save resources.
+////////////////////////////////////////////////////////////////////////////////
+
+/// A helper to compare frames by atime for qsort.
+static int gr_cmp_frames_by_atime(const void *a, const void *b) {
+ ImageFrame *frame_a = *(ImageFrame *const *)a;
+ ImageFrame *frame_b = *(ImageFrame *const *)b;
+ if (frame_a->atime == frame_b->atime)
+ return frame_a->image->global_command_index -
+ frame_b->image->global_command_index;
+ return frame_a->atime - frame_b->atime;
+}
+
+/// A helper to compare images by atime for qsort.
+static int gr_cmp_images_by_atime(const void *a, const void *b) {
+ Image *img_a = *(Image *const *)a;
+ Image *img_b = *(Image *const *)b;
+ if (img_a->atime == img_b->atime)
+ return img_a->global_command_index -
+ img_b->global_command_index;
+ return img_a->atime - img_b->atime;
+}
+
+/// A helper to compare placements by atime for qsort.
+static int gr_cmp_placements_by_atime(const void *a, const void *b) {
+ ImagePlacement *p_a = *(ImagePlacement **)a;
+ ImagePlacement *p_b = *(ImagePlacement **)b;
+ if (p_a->atime == p_b->atime)
+ return p_a->image->global_command_index -
+ p_b->image->global_command_index;
+ return p_a->atime - p_b->atime;
+}
+
+typedef kvec_t(Image *) ImageVec;
+typedef kvec_t(ImagePlacement *) ImagePlacementVec;
+typedef kvec_t(ImageFrame *) ImageFrameVec;
+
+/// Returns an array of pointers to all images sorted by atime.
+static ImageVec gr_get_images_sorted_by_atime() {
+ ImageVec vec;
+ kv_init(vec);
+ if (kh_size(images) == 0)
+ return vec;
+ kv_resize(Image *, vec, kh_size(images));
+ Image *img = NULL;
+ kh_foreach_value(images, img, { kv_push(Image *, vec, img); });
+ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime);
+ return vec;
+}
+
+/// Returns an array of pointers to all placements sorted by atime.
+static ImagePlacementVec gr_get_placements_sorted_by_atime() {
+ ImagePlacementVec vec;
+ kv_init(vec);
+ if (total_placement_count == 0)
+ return vec;
+ kv_resize(ImagePlacement *, vec, total_placement_count);
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ kh_foreach_value(img->placements, placement, {
+ kv_push(ImagePlacement *, vec, placement);
+ });
+ });
+ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *),
+ gr_cmp_placements_by_atime);
+ return vec;
+}
+
+/// Returns an array of pointers to all frames sorted by atime.
+static ImageFrameVec gr_get_frames_sorted_by_atime() {
+ ImageFrameVec frames;
+ kv_init(frames);
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ foreach_frame(*img, frame, {
+ kv_push(ImageFrame *, frames, frame);
+ });
+ });
+ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *),
+ gr_cmp_frames_by_atime);
+ return frames;
+}
+
+/// An object that can be unloaded from RAM.
+typedef struct {
+ /// Some score, probably based on access time. The lower the score, the
+ /// more likely that the object should be unloaded.
+ int64_t score;
+ union {
+ ImagePlacement *placement;
+ ImageFrame *frame;
+ };
+ /// If zero, the object is the imlib object of `frame`, if non-zero,
+ /// the object is a pixmap of `frameidx`-th frame of `placement`.
+ int frameidx;
+} UnloadableObject;
+
+typedef kvec_t(UnloadableObject) UnloadableObjectVec;
+
+/// A helper to compare unloadable objects by score for qsort.
+static int gr_cmp_unloadable_objects(const void *a, const void *b) {
+ UnloadableObject *obj_a = (UnloadableObject *)a;
+ UnloadableObject *obj_b = (UnloadableObject *)b;
+ return obj_a->score - obj_b->score;
+}
+
+/// Unloads an unloadable object from RAM.
+static void gr_unload_object(UnloadableObject *obj) {
+ if (obj->frameidx) {
+ if (obj->placement->protected_frame == obj->frameidx)
+ return;
+ gr_unload_pixmap(obj->placement, obj->frameidx);
+ } else {
+ gr_unload_frame(obj->frame);
+ }
+}
+
+/// Returns the recency threshold for an image. Frames that were accessed within
+/// this threshold from now are considered recent and may be handled
+/// differently because we may need them again very soon.
+static Milliseconds gr_recency_threshold(Image *img) {
+ return img->total_duration * 2 + 1000;
+}
+
+/// Creates an unloadable object for the imlib object of a frame.
+static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now,
+ ImageFrame *frame) {
+ UnloadableObject obj = {0};
+ obj.frameidx = 0;
+ obj.frame = frame;
+ Milliseconds atime = frame->atime;
+ obj.score = atime;
+ if (atime >= now - gr_recency_threshold(frame->image)) {
+ // This is a recent frame, probably from an active animation.
+ // Score it above `now` to prefer unloading non-active frames.
+ // Randomize the score because it's not very clear in which
+ // order we want to unload them: reloading a frame may require
+ // reloading other frames.
+ obj.score = now + 1000 + rand() % 1000;
+ }
+ return obj;
+}
+
+/// Creates an unloadable object for a pixmap.
+static UnloadableObject
+gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame,
+ ImagePlacement *placement) {
+ UnloadableObject obj = {0};
+ obj.frameidx = frame->index;
+ obj.placement = placement;
+ obj.score = placement->atime;
+ // Since we don't store pixmap atimes, use the
+ // oldest atime of the frame and the placement.
+ Milliseconds atime = MIN(placement->atime, frame->atime);
+ obj.score = atime;
+ if (atime >= now - gr_recency_threshold(frame->image)) {
+ // This is a recent pixmap, probably from an active animation.
+ // Score it above `now` to prefer unloading non-active frames.
+ // Also assign higher scores to frames that are closer to the
+ // current frame (more likely to be used soon).
+ int num_frames = gr_last_frame_index(frame->image);
+ int dist = frame->index - frame->image->current_frame;
+ if (dist < 0)
+ dist += num_frames;
+ obj.score =
+ now + 1000 + (num_frames - dist) * 1000 / num_frames;
+ // If the pixmap is much larger than the imlib image, prefer to
+ // unload the pixmap by adding up to -1000 to the score. If the
+ // imlib image is larger, add up to +1000.
+ float imlib_size = gr_frame_current_ram_size(frame);
+ float pixmap_size =
+ gr_placement_single_frame_ram_size(placement);
+ obj.score +=
+ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5);
+ }
+ return obj;
+}
+
+/// Returns an array of unloadable objects sorted by score.
+static UnloadableObjectVec
+gr_get_unloadable_objects_sorted_by_score(Milliseconds now) {
+ UnloadableObjectVec objects;
+ kv_init(objects);
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ foreach_frame(*img, frame, {
+ if (!frame->imlib_object)
+ continue;
+ kv_push(UnloadableObject, objects,
+ gr_unloadable_object_for_frame(now, frame));
+ int frameidx = frame->index;
+ kh_foreach_value(img->placements, placement, {
+ if (!gr_get_frame_pixmap(placement, frameidx))
+ continue;
+ kv_push(UnloadableObject, objects,
+ gr_unloadable_object_for_pixmap(
+ now, frame, placement));
+ });
+ });
+ });
+ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject),
+ gr_cmp_unloadable_objects);
+ return objects;
+}
+
+/// Returns the limit adjusted by the excess tolerance ratio.
+static inline unsigned apply_tolerance(unsigned limit) {
+ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio);
+}
+
+/// Checks RAM and disk cache limits and deletes/unloads some images.
+static void gr_check_limits() {
+ Milliseconds now = gr_now_ms();
+ ImageVec images_sorted = {0};
+ ImagePlacementVec placements_sorted = {0};
+ ImageFrameVec frames_sorted = {0};
+ UnloadableObjectVec objects_sorted = {0};
+ int images_begin = 0;
+ int placements_begin = 0;
+ char changed = 0;
+ // First reduce the number of images if there are too many.
+ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) {
+ GR_LOG("Too many images: %d\n", kh_size(images));
+ changed = 1;
+ images_sorted = gr_get_images_sorted_by_atime();
+ int to_delete = kv_size(images_sorted) -
+ graphics_max_total_placements;
+ for (; images_begin < to_delete; images_begin++)
+ gr_delete_image(images_sorted.a[images_begin]);
+ }
+ // Then reduce the number of placements if there are too many.
+ if (total_placement_count >
+ apply_tolerance(graphics_max_total_placements)) {
+ GR_LOG("Too many placements: %d\n", total_placement_count);
+ changed = 1;
+ placements_sorted = gr_get_placements_sorted_by_atime();
+ int to_delete = kv_size(placements_sorted) -
+ graphics_max_total_placements;
+ for (; placements_begin < to_delete; placements_begin++) {
+ ImagePlacement *placement =
+ placements_sorted.a[placements_begin];
+ if (placement->protected_frame)
+ break;
+ gr_delete_placement(placement);
+ }
+ }
+ // Then reduce the size of the image file cache. The files correspond to
+ // image frames.
+ if (images_disk_size >
+ apply_tolerance(graphics_total_file_cache_size)) {
+ GR_LOG("Too big disk cache: %ld KiB\n",
+ images_disk_size / 1024);
+ changed = 1;
+ frames_sorted = gr_get_frames_sorted_by_atime();
+ for (int i = 0; i < kv_size(frames_sorted); i++) {
+ if (images_disk_size <= graphics_total_file_cache_size)
+ break;
+ gr_delete_imagefile(kv_A(frames_sorted, i));
+ }
+ }
+ // Then unload images from RAM.
+ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) {
+ changed = 1;
+ int frames_begin = 0;
+ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024);
+ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now);
+ for (int i = 0; i < kv_size(objects_sorted); i++) {
+ if (images_ram_size <= graphics_max_total_ram_size)
+ break;
+ gr_unload_object(&kv_A(objects_sorted, i));
+ }
+ }
+ if (changed) {
+ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
+ "img count: %d placement count: %d\n",
+ images_ram_size / 1024, images_disk_size / 1024,
+ kh_size(images), total_placement_count);
+ }
+ kv_destroy(images_sorted);
+ kv_destroy(placements_sorted);
+ kv_destroy(frames_sorted);
+ kv_destroy(objects_sorted);
+}
+
+/// Unloads all images by user request.
+void gr_unload_images_to_reduce_ram() {
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ kh_foreach_value(img->placements, placement, {
+ if (placement->protected_frame)
+ continue;
+ gr_unload_placement(placement);
+ });
+ gr_unload_all_frames(img);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Image loading.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2
+/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted
+/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout
+/// on little-endian architectures).
+static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format,
+ size_t num_pixels) {
+ size_t pixel_size = format == 24 ? 3 : 4;
+ if (format == 32) {
+ for (unsigned i = 0; i < num_pixels; ++i) {
+ unsigned byte_i = i * pixel_size;
+ to[i] = ((DATA32)from[byte_i + 2]) |
+ ((DATA32)from[byte_i + 1]) << 8 |
+ ((DATA32)from[byte_i]) << 16 |
+ ((DATA32)from[byte_i + 3]) << 24;
+ }
+ } else {
+ for (unsigned i = 0; i < num_pixels; ++i) {
+ unsigned byte_i = i * pixel_size;
+ to[i] = ((DATA32)from[byte_i + 2]) |
+ ((DATA32)from[byte_i + 1]) << 8 |
+ ((DATA32)from[byte_i]) << 16 | 0xFF000000;
+ }
+ }
+}
+
+/// Loads uncompressed RGB or RGBA image data from a file.
+static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file,
+ int format,
+ size_t total_pixels) {
+ unsigned char chunk[BUFSIZ];
+ size_t pixel_size = format == 24 ? 3 : 4;
+ size_t chunk_size_pix = BUFSIZ / 4;
+ size_t chunk_size_bytes = chunk_size_pix * pixel_size;
+ size_t bytes = total_pixels * pixel_size;
+ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels;
+ chunk_start_pix += chunk_size_pix) {
+ size_t read_size = fread(chunk, 1, chunk_size_bytes, file);
+ size_t read_pixels = read_size / pixel_size;
+ if (chunk_start_pix + read_pixels > total_pixels)
+ read_pixels = total_pixels - chunk_start_pix;
+ gr_copy_pixels(data + chunk_start_pix, chunk, format,
+ read_pixels);
+ }
+}
+
+#define COMPRESSED_CHUNK_SIZE BUFSIZ
+#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4)
+
+/// Loads compressed RGB or RGBA image data from a file.
+static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file,
+ int format, size_t total_pixels) {
+ size_t pixel_size = format == 24 ? 3 : 4;
+ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE];
+ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE];
+
+ z_stream strm;
+ strm.zalloc = Z_NULL;
+ strm.zfree = Z_NULL;
+ strm.opaque = Z_NULL;
+ strm.next_out = decompressed_chunk;
+ strm.avail_out = DECOMPRESSED_CHUNK_SIZE;
+ strm.avail_in = 0;
+ strm.next_in = Z_NULL;
+ int ret = inflateInit(&strm);
+ if (ret != Z_OK)
+ return 1;
+
+ int error = 0;
+ int progress = 0;
+ size_t total_copied_pixels = 0;
+ while (1) {
+ // If we don't have enough data in the input buffer, try to read
+ // from the file.
+ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) {
+ // Move the existing data to the beginning.
+ memmove(compressed_chunk, strm.next_in, strm.avail_in);
+ strm.next_in = compressed_chunk;
+ // Read more data.
+ size_t bytes_read = fread(
+ compressed_chunk + strm.avail_in, 1,
+ COMPRESSED_CHUNK_SIZE - strm.avail_in, file);
+ strm.avail_in += bytes_read;
+ if (bytes_read != 0)
+ progress = 1;
+ }
+
+ // Try to inflate the data.
+ int ret = inflate(&strm, Z_SYNC_FLUSH);
+ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
+ error = 1;
+ fprintf(stderr,
+ "error: could not decompress the image, error "
+ "%s\n",
+ ret == Z_MEM_ERROR ? "Z_MEM_ERROR"
+ : "Z_DATA_ERROR");
+ break;
+ }
+
+ // Copy the data from the output buffer to the image.
+ size_t full_pixels =
+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size;
+ // Make sure we don't overflow the image.
+ if (full_pixels > total_pixels - total_copied_pixels)
+ full_pixels = total_pixels - total_copied_pixels;
+ if (full_pixels > 0) {
+ // Copy pixels.
+ gr_copy_pixels(data, decompressed_chunk, format,
+ full_pixels);
+ data += full_pixels;
+ total_copied_pixels += full_pixels;
+ if (total_copied_pixels >= total_pixels) {
+ // We filled the whole image, there may be some
+ // data left, but we just truncate it.
+ break;
+ }
+ // Move the remaining data to the beginning.
+ size_t copied_bytes = full_pixels * pixel_size;
+ size_t leftover =
+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) -
+ copied_bytes;
+ memmove(decompressed_chunk,
+ decompressed_chunk + copied_bytes, leftover);
+ strm.next_out -= copied_bytes;
+ strm.avail_out += copied_bytes;
+ progress = 1;
+ }
+
+ // If we haven't made any progress, then we have reached the end
+ // of both the file and the inflated data.
+ if (!progress)
+ break;
+ progress = 0;
+ }
+
+ inflateEnd(&strm);
+ return error;
+}
+
+#undef COMPRESSED_CHUNK_SIZE
+#undef DECOMPRESSED_CHUNK_SIZE
+
+/// Load the image from a file containing raw pixel data (RGB or RGBA), the data
+/// may be compressed.
+static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame,
+ const char *filename) {
+ size_t total_pixels = frame->data_pix_width * frame->data_pix_height;
+ if (total_pixels * 4 > graphics_max_single_image_ram_size) {
+ fprintf(stderr,
+ "error: image %u frame %u is too big too load: %zu > %u\n",
+ frame->image->image_id, frame->index, total_pixels * 4,
+ graphics_max_single_image_ram_size);
+ return NULL;
+ }
+
+ FILE* file = fopen(filename, "rb");
+ if (!file) {
+ fprintf(stderr,
+ "error: could not open image file: %s\n",
+ sanitized_filename(filename));
+ return NULL;
+ }
+
+ Imlib_Image image = imlib_create_image(frame->data_pix_width,
+ frame->data_pix_height);
+ if (!image) {
+ fprintf(stderr,
+ "error: could not create an image of size %d x %d\n",
+ frame->data_pix_width, frame->data_pix_height);
+ fclose(file);
+ return NULL;
+ }
+
+ imlib_context_set_image(image);
+ imlib_image_set_has_alpha(1);
+ DATA32* data = imlib_image_get_data();
+
+ // The default format is 32.
+ int format = frame->format ? frame->format : 32;
+
+ if (frame->compression == 0) {
+ gr_load_raw_pixel_data_uncompressed(data, file, format,
+ total_pixels);
+ } else {
+ int ret = gr_load_raw_pixel_data_compressed(data, file, format,
+ total_pixels);
+ if (ret != 0) {
+ imlib_image_put_back_data(data);
+ imlib_free_image();
+ fclose(file);
+ return NULL;
+ }
+ }
+
+ fclose(file);
+ imlib_image_put_back_data(data);
+ return image;
+}
+
+/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object
+/// is fully composed on top of the background frame. If the frame is already
+/// loaded, does nothing. Loading may fail, in which case the status of the
+/// frame will be set to STATUS_RAM_LOADING_ERROR.
+static void gr_load_imlib_object(ImageFrame *frame) {
+ if (frame->imlib_object)
+ return;
+
+ // If the image is uninitialized or uploading has failed, or the file
+ // has been deleted, we cannot load the image.
+ if (frame->status < STATUS_UPLOADING_SUCCESS)
+ return;
+ if (frame->disk_size == 0) {
+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
+ fprintf(stderr,
+ "error: cached image was deleted: %u frame %u\n",
+ frame->image->image_id, frame->index);
+ }
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+
+ // Prevent recursive dependences between frames.
+ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) {
+ fprintf(stderr,
+ "error: recursive loading of image %u frame %u\n",
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ frame->status = STATUS_RAM_LOADING_IN_PROGRESS;
+
+ // Load the background frame if needed. Hopefully it's not recursive.
+ ImageFrame *bg_frame = NULL;
+ if (frame->background_frame_index) {
+ bg_frame = gr_get_frame(frame->image,
+ frame->background_frame_index);
+ if (!bg_frame) {
+ fprintf(stderr,
+ "error: could not find background "
+ "frame %d for image %u frame %d\n",
+ frame->background_frame_index,
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ gr_load_imlib_object(bg_frame);
+ if (!bg_frame->imlib_object) {
+ fprintf(stderr,
+ "error: could not load background frame %d for "
+ "image %u frame %d\n",
+ frame->background_frame_index,
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ }
+
+ // Load the frame data image.
+ Imlib_Image frame_data_image = NULL;
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ GR_LOG("Loading image: %s\n", sanitized_filename(filename));
+ if (frame->format == 100 || frame->format == 0)
+ frame_data_image = imlib_load_image(filename);
+ if (frame->format == 32 || frame->format == 24 ||
+ (!frame_data_image && frame->format == 0))
+ frame_data_image = gr_load_raw_pixel_data(frame, filename);
+ this_redraw_cycle_loaded_files++;
+
+ if (!frame_data_image) {
+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
+ fprintf(stderr, "error: could not load image: %s\n",
+ sanitized_filename(filename));
+ }
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+
+ imlib_context_set_image(frame_data_image);
+ int frame_data_width = imlib_image_get_width();
+ int frame_data_height = imlib_image_get_height();
+ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width,
+ frame_data_height);
+ // If imlib loading succeeded, and it is the first frame, set the
+ // information about the original image size, unless it's already set.
+ if (frame->index == 1 && frame->image->pix_width == 0 &&
+ frame->image->pix_height == 0) {
+ frame->image->pix_width = frame_data_width;
+ frame->image->pix_height = frame_data_height;
+ }
+
+ int image_width = frame->image->pix_width;
+ int image_height = frame->image->pix_height;
+
+ // Compose the image with the background color or frame.
+ if (frame->background_color != 0 || bg_frame ||
+ image_width != frame_data_width ||
+ image_height != frame_data_height) {
+ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n",
+ frame->background_color, frame->background_frame_index);
+ Imlib_Image composed_image = imlib_create_image(
+ image_width, image_height);
+ imlib_context_set_image(composed_image);
+ imlib_image_set_has_alpha(1);
+ imlib_context_set_anti_alias(0);
+
+ // Start with the background frame or color.
+ imlib_context_set_blend(0);
+ if (bg_frame && bg_frame->imlib_object) {
+ imlib_blend_image_onto_image(
+ bg_frame->imlib_object, 1, 0, 0,
+ image_width, image_height, 0, 0,
+ image_width, image_height);
+ } else {
+ int r = (frame->background_color >> 24) & 0xFF;
+ int g = (frame->background_color >> 16) & 0xFF;
+ int b = (frame->background_color >> 8) & 0xFF;
+ int a = frame->background_color & 0xFF;
+ imlib_context_set_color(r, g, b, a);
+ imlib_image_fill_rectangle(0, 0, image_width,
+ image_height);
+ }
+
+ // Blend the frame data image onto the background.
+ imlib_context_set_blend(1);
+ imlib_blend_image_onto_image(
+ frame_data_image, 1, 0, 0, frame->data_pix_width,
+ frame->data_pix_height, frame->x, frame->y,
+ frame->data_pix_width, frame->data_pix_height);
+
+ // Free the frame data image.
+ imlib_context_set_image(frame_data_image);
+ imlib_free_image();
+
+ frame_data_image = composed_image;
+ }
+
+ frame->imlib_object = frame_data_image;
+
+ images_ram_size += gr_frame_current_ram_size(frame);
+ frame->status = STATUS_RAM_LOADING_SUCCESS;
+
+ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n",
+ frame->image->image_id, frame->index,
+ images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024);
+}
+
+/// Premultiplies the alpha channel of the image data. The data is an array of
+/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB.
+static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) {
+ for (size_t i = 0; i < num_pixels; ++i) {
+ DATA32 pixel = data[i];
+ unsigned char a = pixel >> 24;
+ if (a == 0) {
+ data[i] = 0;
+ } else if (a != 255) {
+ unsigned char b = (pixel & 0xFF) * a / 255;
+ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255;
+ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255;
+ data[i] = (a << 24) | (r << 16) | (g << 8) | b;
+ }
+ }
+}
+
+/// Creates a pixmap for the frame of an image placement. The pixmap contain the
+/// image data correctly scaled and fit to the box defined by the number of
+/// rows/columns of the image placement and the provided cell dimensions in
+/// pixels. If the placement is already loaded, it will be reloaded only if the
+/// cell dimensions have changed.
+Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) {
+ Image *img = placement->image;
+ ImageFrame *frame = gr_get_frame(img, frameidx);
+
+ // Update the atime uncoditionally.
+ gr_touch_placement(placement);
+ if (frame)
+ gr_touch_frame(frame);
+
+ // If cw or ch are different, unload all the pixmaps.
+ if (placement->scaled_cw != cw || placement->scaled_ch != ch) {
+ gr_unload_placement(placement);
+ placement->scaled_cw = cw;
+ placement->scaled_ch = ch;
+ }
+
+ // If it's already loaded, do nothing.
+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
+ if (pixmap)
+ return pixmap;
+
+ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id,
+ placement->placement_id, frameidx);
+
+ // Load the imlib object for the frame.
+ if (!frame) {
+ fprintf(stderr,
+ "error: could not find frame %u for image %u\n",
+ frameidx, img->image_id);
+ return 0;
+ }
+ gr_load_imlib_object(frame);
+ if (!frame->imlib_object)
+ return 0;
+
+ // Infer the placement size if needed.
+ gr_infer_placement_size_maybe(placement);
+
+ // Create the scaled image. This is temporary, we will scale it
+ // appropriately, upload to the X server, and then delete immediately.
+ int scaled_w = (int)placement->cols * cw;
+ int scaled_h = (int)placement->rows * ch;
+ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) {
+ fprintf(stderr,
+ "error: placement %u/%u would be too big to load: %d x "
+ "%d x 4 > %u\n",
+ img->image_id, placement->placement_id, scaled_w,
+ scaled_h, graphics_max_single_image_ram_size);
+ return 0;
+ }
+ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h);
+ if (!scaled_image) {
+ fprintf(stderr,
+ "error: imlib_create_image(%d, %d) returned "
+ "null\n",
+ scaled_w, scaled_h);
+ return 0;
+ }
+ imlib_context_set_image(scaled_image);
+ imlib_image_set_has_alpha(1);
+
+ // First fill the scaled image with the transparent color.
+ imlib_context_set_blend(0);
+ imlib_context_set_color(0, 0, 0, 0);
+ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h);
+ imlib_context_set_anti_alias(1);
+ imlib_context_set_blend(1);
+
+ // The source rectangle.
+ int src_x = placement->src_pix_x;
+ int src_y = placement->src_pix_y;
+ int src_w = placement->src_pix_width;
+ int src_h = placement->src_pix_height;
+ // Whether the box is too small to use the true size of the image.
+ char box_too_small = scaled_w < src_w || scaled_h < src_h;
+ char mode = placement->scale_mode;
+
+ // Then blend the original image onto the transparent background.
+ if (src_w <= 0 || src_h <= 0) {
+ fprintf(stderr, "warning: image of zero size\n");
+ } else if (mode == SCALE_MODE_FILL) {
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, 0, 0,
+ scaled_w, scaled_h);
+ } else if (mode == SCALE_MODE_NONE ||
+ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) {
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, 0, 0, src_w,
+ src_h);
+ } else {
+ if (mode != SCALE_MODE_CONTAIN &&
+ mode != SCALE_MODE_NONE_OR_CONTAIN) {
+ fprintf(stderr,
+ "warning: unknown scale mode %u, using "
+ "'contain' instead\n",
+ mode);
+ }
+ int dest_x, dest_y;
+ int dest_w, dest_h;
+ if (scaled_w * src_h > src_w * scaled_h) {
+ // If the box is wider than the original image, fit to
+ // height.
+ dest_h = scaled_h;
+ dest_y = 0;
+ dest_w = src_w * scaled_h / src_h;
+ dest_x = (scaled_w - dest_w) / 2;
+ } else {
+ // Otherwise, fit to width.
+ dest_w = scaled_w;
+ dest_x = 0;
+ dest_h = src_h * scaled_w / src_w;
+ dest_y = (scaled_h - dest_h) / 2;
+ }
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, dest_x,
+ dest_y, dest_w, dest_h);
+ }
+
+ // XRender needs the alpha channel premultiplied.
+ DATA32 *data = imlib_image_get_data();
+ gr_premultiply_alpha(data, scaled_w * scaled_h);
+
+ // Upload the image to the X server.
+ Display *disp = imlib_context_get_display();
+ Visual *vis = imlib_context_get_visual();
+ Colormap cmap = imlib_context_get_colormap();
+ Drawable drawable = imlib_context_get_drawable();
+ if (!drawable)
+ drawable = DefaultRootWindow(disp);
+ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32);
+ XVisualInfo visinfo;
+ XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo);
+ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
+ (char *)data, scaled_w, scaled_h, 32, 0);
+ GC gc = XCreateGC(disp, pixmap, 0, NULL);
+ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w,
+ scaled_h);
+ XFreeGC(disp, gc);
+ // XDestroyImage will free the data as well, but it is managed by imlib,
+ // so set it to NULL.
+ ximage->data = NULL;
+ XDestroyImage(ximage);
+ imlib_image_put_back_data(data);
+ imlib_free_image();
+
+ // Assign the pixmap to the frame and increase the ram size.
+ gr_set_frame_pixmap(placement, frameidx, pixmap);
+ images_ram_size += gr_placement_single_frame_ram_size(placement);
+ this_redraw_cycle_loaded_pixmaps++;
+
+ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
+ "KiB)\n",
+ frame->image->image_id, placement->placement_id, frame->index,
+ images_ram_size / 1024,
+ gr_placement_single_frame_ram_size(placement) / 1024);
+
+ // Free up ram if needed, but keep the pixmap we've loaded no matter
+ // what.
+ placement->protected_frame = frameidx;
+ gr_check_limits();
+ placement->protected_frame = 0;
+
+ return pixmap;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Initialization and deinitialization.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Creates a temporary directory.
+static int gr_create_cache_dir() {
+ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir));
+ if (!mkdtemp(cache_dir)) {
+ fprintf(stderr,
+ "error: could not create temporary dir from template "
+ "%s\n",
+ sanitized_filename(cache_dir));
+ return 0;
+ }
+ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir);
+ return 1;
+}
+
+/// Checks whether `tmp_dir` exists and recreates it if it doesn't.
+static void gr_make_sure_tmpdir_exists() {
+ struct stat st;
+ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode))
+ return;
+ fprintf(stderr,
+ "error: %s is not a directory, will need to create a new "
+ "graphics cache directory\n",
+ sanitized_filename(cache_dir));
+ gr_create_cache_dir();
+}
+
+/// Initialize the graphics module.
+void gr_init(Display *disp, Visual *vis, Colormap cm) {
+ // Set the initialization time.
+ clock_gettime(CLOCK_MONOTONIC, &initialization_time);
+
+ // Create the temporary dir.
+ if (!gr_create_cache_dir())
+ abort();
+
+ // Initialize imlib.
+ imlib_context_set_display(disp);
+ imlib_context_set_visual(vis);
+ imlib_context_set_colormap(cm);
+ imlib_context_set_anti_alias(1);
+ imlib_context_set_blend(1);
+ // Imlib2 checks only the file name when caching, which is not enough
+ // for us since we reuse file names. Disable caching.
+ imlib_set_cache_size(0);
+
+ // Prepare for color inversion.
+ for (size_t i = 0; i < 256; ++i)
+ reverse_table[i] = 255 - i;
+
+ // Create data structures.
+ images = kh_init(id2image);
+ kv_init(next_redraw_times);
+
+ atexit(gr_deinit);
+}
+
+/// Deinitialize the graphics module.
+void gr_deinit() {
+ // Remove the cache dir.
+ remove(cache_dir);
+ kv_destroy(next_redraw_times);
+ if (images) {
+ // Delete all images.
+ gr_delete_all_images();
+ // Destroy the data structures.
+ kh_destroy(id2image, images);
+ images = NULL;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Dumping, debugging, and image preview.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Returns a string containing a time difference in a human-readable format.
+/// Uses a static buffer, so be careful.
+static const char *gr_ago(Milliseconds diff) {
+ static char result[32];
+ double seconds = (double)diff / 1000.0;
+ if (seconds < 1)
+ snprintf(result, sizeof(result), "%.2f sec ago", seconds);
+ else if (seconds < 60)
+ snprintf(result, sizeof(result), "%d sec ago", (int)seconds);
+ else if (seconds < 3600)
+ snprintf(result, sizeof(result), "%d min %d sec ago",
+ (int)(seconds / 60), (int)(seconds) % 60);
+ else {
+ snprintf(result, sizeof(result), "%d hr %d min %d sec ago",
+ (int)(seconds / 3600), (int)(seconds) % 3600 / 60,
+ (int)(seconds) % 60);
+ }
+ return result;
+}
+
+/// Prints to `file` with an indentation of `ind` spaces.
+static void fprintf_ind(FILE *file, int ind, const char *format, ...) {
+ fprintf(file, "%*s", ind, "");
+ va_list args;
+ va_start(args, format);
+ vfprintf(file, format, args);
+ va_end(args);
+}
+
+/// Dumps the image info to `file` with an indentation of `ind` spaces.
+static void gr_dump_image_info(FILE *file, Image *img, int ind) {
+ if (!img) {
+ fprintf_ind(file, ind, "Image is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Image %u\n", img->image_id);
+ ind += 4;
+ fprintf_ind(file, ind, "number: %u\n", img->image_number);
+ fprintf_ind(file, ind, "global command index: %lu\n",
+ img->global_command_index);
+ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime,
+ gr_ago(now - img->atime));
+ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width,
+ img->pix_height);
+ fprintf_ind(file, ind, "cur frame start time: %ld %s\n",
+ img->current_frame_time,
+ gr_ago(now - img->current_frame_time));
+ if (img->next_redraw)
+ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n",
+ img->next_redraw, img->next_redraw - now);
+ fprintf_ind(file, ind, "total disk size: %u KiB\n",
+ img->total_disk_size / 1024);
+ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration);
+ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img));
+ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame);
+ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state);
+ fprintf_ind(file, ind, "default_placement: %u\n",
+ img->default_placement);
+}
+
+/// Dumps the frame info to `file` with an indentation of `ind` spaces.
+static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) {
+ if (!frame) {
+ fprintf_ind(file, ind, "Frame is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Frame %d\n", frame->index);
+ ind += 4;
+ if (frame->index == 0) {
+ fprintf_ind(file, ind, "NOT INITIALIZED\n");
+ return;
+ }
+ if (frame->uploading_failure)
+ fprintf_ind(file, ind, "uploading failure: %s\n",
+ image_uploading_failure_strings
+ [frame->uploading_failure]);
+ fprintf_ind(file, ind, "gap: %d\n", frame->gap);
+ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime,
+ gr_ago(now - frame->atime));
+ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width,
+ frame->data_pix_height);
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ if (access(filename, F_OK) != -1)
+ fprintf_ind(file, ind, "file: %s\n",
+ sanitized_filename(filename));
+ else
+ fprintf_ind(file, ind, "not on disk\n");
+ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024);
+ if (frame->imlib_object) {
+ unsigned ram_size = gr_frame_current_ram_size(frame);
+ fprintf_ind(file, ind,
+ "loaded into ram, size: %d "
+ "KiB\n",
+ ram_size / 1024);
+ } else {
+ fprintf_ind(file, ind, "not loaded into ram\n");
+ }
+}
+
+/// Dumps the placement info to `file` with an indentation of `ind` spaces.
+static void gr_dump_placement_info(FILE *file, ImagePlacement *placement,
+ int ind) {
+ if (!placement) {
+ fprintf_ind(file, ind, "Placement is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id);
+ ind += 4;
+ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime,
+ gr_ago(now - placement->atime));
+ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode);
+ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols,
+ placement->rows);
+ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw,
+ placement->scaled_ch);
+ fprintf_ind(file, ind, "ram per frame: %u KiB\n",
+ gr_placement_single_frame_ram_size(placement) / 1024);
+ unsigned ram_size = gr_placement_current_ram_size(placement);
+ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024);
+}
+
+/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces.
+static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement,
+ int ind) {
+ if (!placement)
+ return;
+ int frameidx = 1;
+ foreach_pixmap(*placement, pixmap, {
+ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx,
+ pixmap);
+ ++frameidx;
+ });
+}
+
+/// Dumps the internal state (images and placements) to stderr.
+void gr_dump_state() {
+ FILE *file = stderr;
+ int ind = 0;
+ fprintf_ind(file, ind, "======= Graphics module state dump =======\n");
+ fprintf_ind(file, ind,
+ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu "
+ "sizeof(ImagePlacement) = %lu\n",
+ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement));
+ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images));
+ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count);
+ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n",
+ images_ram_size / 1024);
+ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n",
+ images_disk_size / 1024);
+
+ Milliseconds now = gr_now_ms();
+
+ int64_t images_ram_size_computed = 0;
+ int64_t images_disk_size_computed = 0;
+
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ fprintf_ind(file, ind, "----------------\n");
+ gr_dump_image_info(file, img, 0);
+ int64_t total_disk_size_computed = 0;
+ int total_duration_computed = 0;
+ foreach_frame(*img, frame, {
+ gr_dump_frame_info(file, frame, 4);
+ if (frame->image != img)
+ fprintf_ind(file, 8,
+ "ERROR: WRONG IMAGE POINTER\n");
+ total_duration_computed += frame->gap;
+ images_disk_size_computed += frame->disk_size;
+ total_disk_size_computed += frame->disk_size;
+ if (frame->imlib_object)
+ images_ram_size_computed +=
+ gr_frame_current_ram_size(frame);
+ });
+ if (img->total_disk_size != total_disk_size_computed) {
+ fprintf_ind(file, ind,
+ " ERROR: total_disk_size is %u, but "
+ "computed value is %ld\n",
+ img->total_disk_size, total_disk_size_computed);
+ }
+ if (img->total_duration != total_duration_computed) {
+ fprintf_ind(file, ind,
+ " ERROR: total_duration is %d, but computed "
+ "value is %d\n",
+ img->total_duration, total_duration_computed);
+ }
+ kh_foreach_value(img->placements, placement, {
+ gr_dump_placement_info(file, placement, 4);
+ if (placement->image != img)
+ fprintf_ind(file, 8,
+ "ERROR: WRONG IMAGE POINTER\n");
+ fprintf_ind(file, 8,
+ "Pixmaps:\n");
+ gr_dump_placement_pixmaps(file, placement, 12);
+ unsigned ram_size =
+ gr_placement_current_ram_size(placement);
+ images_ram_size_computed += ram_size;
+ });
+ });
+ if (images_ram_size != images_ram_size_computed) {
+ fprintf_ind(file, ind,
+ "ERROR: images_ram_size is %ld, but computed value "
+ "is %ld\n",
+ images_ram_size, images_ram_size_computed);
+ }
+ if (images_disk_size != images_disk_size_computed) {
+ fprintf_ind(file, ind,
+ "ERROR: images_disk_size is %ld, but computed value "
+ "is %ld\n",
+ images_disk_size, images_disk_size_computed);
+ }
+ fprintf_ind(file, ind, "===========================================\n");
+}
+
+/// Executes `command` with the name of the file corresponding to `image_id` as
+/// the argument. Executes xmessage with an error message on failure.
+// TODO: Currently we do this for the first frame only. Not sure what to do with
+// animations.
+void gr_preview_image(uint32_t image_id, const char *exec) {
+ char command[256];
+ size_t len;
+ Image *img = gr_find_image(image_id);
+ if (img) {
+ ImageFrame *frame = &img->first_frame;
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ if (frame->disk_size == 0) {
+ len = snprintf(command, 255,
+ "xmessage 'Image with id=%u is not "
+ "fully copied to %s'",
+ image_id, sanitized_filename(filename));
+ } else {
+ len = snprintf(command, 255, "%s %s &", exec,
+ sanitized_filename(filename));
+ }
+ } else {
+ len = snprintf(command, 255,
+ "xmessage 'Cannot find image with id=%u'",
+ image_id);
+ }
+ if (len > 255) {
+ fprintf(stderr, "error: command too long: %s\n", command);
+ snprintf(command, 255, "xmessage 'error: command too long'");
+ }
+ if (system(command) != 0) {
+ fprintf(stderr, "error: could not execute command %s\n",
+ command);
+ }
+}
+
+/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
+/// containing the information about an image and placement, and <st> is
+/// specified with `st_executable`.
+void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
+ uint32_t imgcol, uint32_t imgrow,
+ char is_classic_placeholder, int32_t diacritic_count,
+ char *st_executable) {
+ char filename[MAX_FILENAME_SIZE];
+ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id);
+ FILE *file = fopen(filename, "w");
+ if (!file) {
+ perror("fopen");
+ return;
+ }
+ // Basic information about the cell.
+ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id);
+ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id);
+ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow);
+ fprintf(file, "classic/unicode placeholder = %s\n",
+ is_classic_placeholder ? "classic" : "unicode");
+ fprintf(file, "original diacritic count = %d\n", diacritic_count);
+ // Information about the image and the placement.
+ Image *img = gr_find_image(image_id);
+ ImagePlacement *placement = gr_find_placement(img, placement_id);
+ gr_dump_image_info(file, img, 0);
+ gr_dump_placement_info(file, placement, 0);
+ if (img) {
+ fprintf(file, "Frames:\n");
+ foreach_frame(*img, frame, {
+ gr_dump_frame_info(file, frame, 4);
+ });
+ }
+ if (placement) {
+ fprintf(file, "Placement pixmaps:\n");
+ gr_dump_placement_pixmaps(file, placement, 4);
+ }
+ fclose(file);
+ char *argv[] = {st_executable, "-e", "less", filename, NULL};
+ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) {
+ perror("posix_spawnp");
+ return;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Appending and displaying image rectangles.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Displays debug information in the rectangle using colors col1 and col2.
+static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2,
+ const char *message) {
+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ char info[MAX_INFO_LEN];
+ if (rect->placement_id)
+ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message,
+ rect->image_id, rect->placement_id,
+ rect->img_start_col, rect->img_end_col,
+ rect->img_start_row, rect->img_end_row);
+ else
+ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message,
+ rect->image_id, rect->img_start_col, rect->img_end_col,
+ rect->img_start_row, rect->img_end_row);
+ XSetForeground(disp, gc, col1);
+ XDrawString(disp, buf, gc, rect->screen_x_pix + 4,
+ rect->screen_y_pix + h_pix - 3, info, strlen(info));
+ XSetForeground(disp, gc, col2);
+ XDrawString(disp, buf, gc, rect->screen_x_pix + 2,
+ rect->screen_y_pix + h_pix - 5, info, strlen(info));
+ XFreeGC(disp, gc);
+}
+
+/// Draws a rectangle (bounding box) for debugging.
+static void gr_showrect(Drawable buf, ImageRect *rect) {
+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ XSetForeground(disp, gc, 0xFF00FF00);
+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix,
+ w_pix - 1, h_pix - 1);
+ XSetForeground(disp, gc, 0xFFFF0000);
+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1,
+ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3);
+ XFreeGC(disp, gc);
+}
+
+/// Updates the next redraw time for the given row. Resizes the
+/// next_redraw_times array if needed.
+static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) {
+ if (next_redraw == 0)
+ return;
+ if (row >= kv_size(next_redraw_times)) {
+ size_t old_size = kv_size(next_redraw_times);
+ kv_a(Milliseconds, next_redraw_times, row);
+ for (size_t i = old_size; i <= row; ++i)
+ kv_A(next_redraw_times, i) = 0;
+ }
+ Milliseconds old_value = kv_A(next_redraw_times, row);
+ if (old_value == 0 || old_value > next_redraw)
+ kv_A(next_redraw_times, row) = next_redraw;
+}
+
+/// Draws the given part of an image.
+static void gr_drawimagerect(Drawable buf, ImageRect *rect) {
+ ImagePlacement *placement =
+ gr_find_image_and_placement(rect->image_id, rect->placement_id);
+ // If the image does not exist or image display is switched off, draw
+ // the bounding box.
+ if (!placement || !graphics_display_images) {
+ gr_showrect(buf, rect);
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ return;
+ }
+
+ Image *img = placement->image;
+
+ if (img->last_redraw < drawing_start_time) {
+ // This is the first time we draw this image in this redraw
+ // cycle. Update the frame index we are going to display. Note
+ // that currently all image placements are synchronized.
+ int old_frame = img->current_frame;
+ gr_update_frame_index(img, drawing_start_time);
+ img->last_redraw = drawing_start_time;
+ }
+
+ // Adjust next redraw times for the rows of this image rect.
+ if (img->next_redraw) {
+ for (int row = rect->screen_y_row;
+ row <= rect->screen_y_row + rect->img_end_row -
+ rect->img_start_row - 1; ++row) {
+ gr_update_next_redraw_time(
+ row, img->next_redraw);
+ }
+ }
+
+ // Load the frame.
+ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw,
+ rect->ch);
+
+ // If the image couldn't be loaded, display the bounding box.
+ if (!pixmap) {
+ gr_showrect(buf, rect);
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ return;
+ }
+
+ int src_x = rect->img_start_col * rect->cw;
+ int src_y = rect->img_start_row * rect->ch;
+ int width = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int height = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ int dst_x = rect->screen_x_pix;
+ int dst_y = rect->screen_y_pix;
+
+ // Display the image.
+ Display *disp = imlib_context_get_display();
+ Visual *vis = imlib_context_get_visual();
+
+ // Create an xrender picture for the window.
+ XRenderPictFormat *win_format =
+ XRenderFindVisualFormat(disp, vis);
+ Picture window_pic =
+ XRenderCreatePicture(disp, buf, win_format, 0, NULL);
+
+ // If needed, invert the image pixmap. Note that this naive approach of
+ // inverting the pixmap is not entirely correct, because the pixmap is
+ // premultiplied. But the result is good enough to visually indicate
+ // selection.
+ if (rect->reverse) {
+ unsigned pixmap_w =
+ (unsigned)placement->cols * placement->scaled_cw;
+ unsigned pixmap_h =
+ (unsigned)placement->rows * placement->scaled_ch;
+ Pixmap invpixmap =
+ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32);
+ XGCValues gcv = {.function = GXcopyInverted};
+ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv);
+ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w,
+ pixmap_h, 0, 0);
+ XFreeGC(disp, gc);
+ pixmap = invpixmap;
+ }
+
+ // Create a picture for the image pixmap.
+ XRenderPictFormat *pic_format =
+ XRenderFindStandardFormat(disp, PictStandardARGB32);
+ Picture pixmap_pic =
+ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL);
+
+ // Composite the image onto the window. In the reverse mode we ignore
+ // the alpha channel of the image because the naive inversion above
+ // seems to invert the alpha channel as well.
+ int pictop = rect->reverse ? PictOpSrc : PictOpOver;
+ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic,
+ src_x, src_y, src_x, src_y, dst_x, dst_y, width,
+ height);
+
+ // Free resources
+ XRenderFreePicture(disp, pixmap_pic);
+ XRenderFreePicture(disp, window_pic);
+ if (rect->reverse)
+ XFreePixmap(disp, pixmap);
+
+ // In debug mode always draw bounding boxes and print info.
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) {
+ gr_showrect(buf, rect);
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ }
+}
+
+/// Removes the given image rectangle.
+static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); }
+
+/// Returns the bottom coordinate of the rect.
+static int gr_getrectbottom(ImageRect *rect) {
+ return rect->screen_y_pix +
+ (rect->img_end_row - rect->img_start_row) * rect->ch;
+}
+
+/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
+void gr_start_drawing(Drawable buf, int cw, int ch) {
+ current_cw = cw;
+ current_ch = ch;
+ this_redraw_cycle_loaded_files = 0;
+ this_redraw_cycle_loaded_pixmaps = 0;
+ drawing_start_time = gr_now_ms();
+ imlib_context_set_drawable(buf);
+}
+
+/// Finish image drawing. This functions will draw all the rectangles left to
+/// draw.
+void gr_finish_drawing(Drawable buf) {
+ // Draw and then delete all known image rectangles.
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (!rect->image_id)
+ continue;
+ gr_drawimagerect(buf, rect);
+ gr_freerect(rect);
+ }
+
+ // Compute the delay until the next redraw as the minimum of the next
+ // redraw delays for all rows.
+ Milliseconds drawing_end_time = gr_now_ms();
+ graphics_next_redraw_delay = INT_MAX;
+ for (int row = 0; row < kv_size(next_redraw_times); ++row) {
+ Milliseconds row_next_redraw = kv_A(next_redraw_times, row);
+ if (row_next_redraw > 0) {
+ int delay = MAX(graphics_animation_min_delay,
+ row_next_redraw - drawing_end_time);
+ graphics_next_redraw_delay =
+ MIN(graphics_next_redraw_delay, delay);
+ }
+ }
+
+ // In debug mode display additional info.
+ if (graphics_debug_mode) {
+ int milliseconds = drawing_end_time - drawing_start_time;
+
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ const char *debug_mode_str =
+ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES
+ ? "(boxes shown) "
+ : "";
+ int redraw_delay = graphics_next_redraw_delay == INT_MAX
+ ? -1
+ : graphics_next_redraw_delay;
+ char info[MAX_INFO_LEN];
+ snprintf(info, MAX_INFO_LEN,
+ "%sRender time: %d ms ram %ld K disk %ld K count "
+ "%d cell %dx%d delay %d",
+ debug_mode_str, milliseconds, images_ram_size / 1024,
+ images_disk_size / 1024, kh_size(images), current_cw,
+ current_ch, redraw_delay);
+ XSetForeground(disp, gc, 0xFF000000);
+ XFillRectangle(disp, buf, gc, 0, 0, 600, 16);
+ XSetForeground(disp, gc, 0xFFFFFFFF);
+ XDrawString(disp, buf, gc, 0, 14, info, strlen(info));
+ XFreeGC(disp, gc);
+
+ if (milliseconds > 0) {
+ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n",
+ info, this_redraw_cycle_loaded_files,
+ this_redraw_cycle_loaded_pixmaps);
+ }
+ }
+
+ // Check the limits in case we have used too much ram for placements.
+ gr_check_limits();
+}
+
+// Add an image rectangle to the list of rectangles to draw.
+void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
+ int img_start_col, int img_end_col, int img_start_row,
+ int img_end_row, int x_col, int y_row, int x_pix,
+ int y_pix, int cw, int ch, int reverse) {
+ current_cw = cw;
+ current_ch = ch;
+
+ ImageRect new_rect;
+ new_rect.image_id = image_id;
+ new_rect.placement_id = placement_id;
+ new_rect.img_start_col = img_start_col;
+ new_rect.img_end_col = img_end_col;
+ new_rect.img_start_row = img_start_row;
+ new_rect.img_end_row = img_end_row;
+ new_rect.screen_y_row = y_row;
+ new_rect.screen_x_pix = x_pix;
+ new_rect.screen_y_pix = y_pix;
+ new_rect.ch = ch;
+ new_rect.cw = cw;
+ new_rect.reverse = reverse;
+
+ // Display some red text in debug mode.
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? ");
+
+ // If it's the empty image (image_id=0) or an empty rectangle, do
+ // nothing.
+ if (image_id == 0 || img_end_col - img_start_col <= 0 ||
+ img_end_row - img_start_row <= 0)
+ return;
+ // Try to find a rect to merge with.
+ ImageRect *free_rect = NULL;
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (rect->image_id == 0) {
+ if (!free_rect)
+ free_rect = rect;
+ continue;
+ }
+ if (rect->image_id != image_id ||
+ rect->placement_id != placement_id || rect->cw != cw ||
+ rect->ch != ch || rect->reverse != reverse)
+ continue;
+ // We only support the case when the new stripe is added to the
+ // bottom of an existing rectangle and they are perfectly
+ // aligned.
+ if (rect->img_end_row == img_start_row &&
+ gr_getrectbottom(rect) == y_pix) {
+ if (rect->img_start_col == img_start_col &&
+ rect->img_end_col == img_end_col &&
+ rect->screen_x_pix == x_pix) {
+ rect->img_end_row = img_end_row;
+ return;
+ }
+ }
+ }
+ // If we haven't merged the new rect with any existing rect, and there
+ // is no free rect, we have to render one of the existing rects.
+ if (!free_rect) {
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (!free_rect || gr_getrectbottom(free_rect) >
+ gr_getrectbottom(rect))
+ free_rect = rect;
+ }
+ gr_drawimagerect(buf, free_rect);
+ gr_freerect(free_rect);
+ }
+ // Start a new rectangle in `free_rect`.
+ *free_rect = new_rect;
+}
+
+/// Mark rows containing animations as dirty if it's time to redraw them. Must
+/// be called right after `gr_start_drawing`.
+void gr_mark_dirty_animations(int *dirty, int rows) {
+ if (rows < kv_size(next_redraw_times))
+ kv_size(next_redraw_times) = rows;
+ if (rows * 2 < kv_max(next_redraw_times))
+ kv_resize(Milliseconds, next_redraw_times, rows);
+ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) {
+ if (dirty[i]) {
+ kv_A(next_redraw_times, i) = 0;
+ continue;
+ }
+ Milliseconds next_update = kv_A(next_redraw_times, i);
+ if (next_update > 0 && next_update <= drawing_start_time) {
+ dirty[i] = 1;
+ kv_A(next_redraw_times, i) = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Command parsing and handling.
+////////////////////////////////////////////////////////////////////////////////
+
+/// A parsed kitty graphics protocol command.
+typedef struct {
+ /// The command itself, without the 'G'.
+ char *command;
+ /// The payload (after ';').
+ char *payload;
+ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'.
+ char action;
+ /// 'q=', 1 to suppress OK response, 2 to suppress errors too.
+ int quiet;
+ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with
+ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to
+ /// 32-bit pixel data.
+ int format;
+ /// 'o=', may be 'z' for RFC 1950 ZLIB.
+ int compression;
+ /// 't=', may be 'f', 't' or 'd'.
+ char transmission_medium;
+ /// 'd='
+ char delete_specifier;
+ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'.
+ /// When 'a=f', this is the size of the frame rectangle when composed on
+ /// top of another frame.
+ int frame_pix_width, frame_pix_height;
+ /// 'x=', 'y=' - top-left corner of the source rectangle.
+ int src_pix_x, src_pix_y;
+ /// 'w=', 'h=' - width and height of the source rectangle.
+ int src_pix_width, src_pix_height;
+ /// 'r=', 'c='
+ int rows, columns;
+ /// 'i='
+ uint32_t image_id;
+ /// 'I='
+ uint32_t image_number;
+ /// 'p='
+ uint32_t placement_id;
+ /// 'm=', may be 0 or 1.
+ int more;
+ /// True if either 'm=0' or 'm=1' is specified.
+ char is_data_transmission;
+ /// True if turns out that this command is a continuation of a data
+ /// transmission and not the first one for this image. Populated by
+ /// `gr_handle_transmit_command`.
+ char is_direct_transmission_continuation;
+ /// 'S=', used to check the size of uploaded data.
+ int size;
+ /// 'U=', whether it's a virtual placement for Unicode placeholders.
+ int virtual;
+ /// 'C=', if true, do not move the cursor when displaying this placement
+ /// (non-virtual placements only).
+ char do_not_move_cursor;
+ // ---------------------------------------------------------------------
+ // Animation-related fields. Their keys often overlap with keys of other
+ // commands, so these make sense only if the action is 'a=f' (frame
+ // transmission) or 'a=a' (animation control).
+ //
+ // 'x=' and 'y=', the relative position of the frame image when it's
+ // composed on top of another frame.
+ int frame_dst_pix_x, frame_dst_pix_y;
+ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of
+ /// the background color or frame.
+ char replace_instead_of_blending;
+ /// 'Y=', the background color in the 0xRRGGBBAA format (still
+ /// transmitted as a decimal number).
+ uint32_t background_color;
+ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame.
+ int background_frame;
+ /// (Only for 'a=a'). 'c=', sets the index of the current frame.
+ int current_frame;
+ /// 'r=', the 1-based index of the frame to edit.
+ int edit_frame;
+ /// 'z=', the duration of the frame. Zero if not specified, negative if
+ /// the frame is gapless (i.e. skipped).
+ int gap;
+ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the
+ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop.
+ int animation_state;
+ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the
+ /// animation will loop. 1 to loop infinitely, N to loop N-1 times.
+ int loops;
+} GraphicsCommand;
+
+/// Replaces all non-printed characters in `str` with '?' and truncates the
+/// string to `max_size`, maybe inserting ellipsis at the end.
+static void sanitize_str(char *str, size_t max_size) {
+ assert(max_size >= 4);
+ for (size_t i = 0; i < max_size; ++i) {
+ unsigned c = str[i];
+ if (c == '\0')
+ return;
+ if (c >= 128 || !isprint(c))
+ str[i] = '?';
+ }
+ str[max_size - 1] = '\0';
+ str[max_size - 2] = '.';
+ str[max_size - 3] = '.';
+ str[max_size - 4] = '.';
+}
+
+/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be
+/// careful.
+static const char *sanitized_filename(const char *str) {
+ static char buf[MAX_FILENAME_SIZE];
+ strncpy(buf, str, sizeof(buf));
+ sanitize_str(buf, sizeof(buf));
+ return buf;
+}
+
+/// Creates a response to the current command in `graphics_command_result`.
+static void gr_createresponse(uint32_t image_id, uint32_t image_number,
+ uint32_t placement_id, const char *msg) {
+ if (!image_id && !image_number && !placement_id) {
+ // Nobody expects the response in this case, so just print it to
+ // stderr.
+ fprintf(stderr,
+ "error: No image id or image number or placement_id, "
+ "but still there is a response: %s\n",
+ msg);
+ return;
+ }
+ char *buf = graphics_command_result.response;
+ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN;
+ size_t written;
+ written = snprintf(buf, maxlen, "\033_G");
+ buf += written;
+ maxlen -= written;
+ if (image_id) {
+ written = snprintf(buf, maxlen, "i=%u,", image_id);
+ buf += written;
+ maxlen -= written;
+ }
+ if (image_number) {
+ written = snprintf(buf, maxlen, "I=%u,", image_number);
+ buf += written;
+ maxlen -= written;
+ }
+ if (placement_id) {
+ written = snprintf(buf, maxlen, "p=%u,", placement_id);
+ buf += written;
+ maxlen -= written;
+ }
+ buf[-1] = ';';
+ written = snprintf(buf, maxlen, "%s\033\\", msg);
+ buf += written;
+ maxlen -= written;
+ buf[-2] = '\033';
+ buf[-1] = '\\';
+}
+
+/// Creates the 'OK' response to the current command, unless suppressed or a
+/// non-final data transmission.
+static void gr_reportsuccess_cmd(GraphicsCommand *cmd) {
+ if (cmd->quiet < 1 && !cmd->more)
+ gr_createresponse(cmd->image_id, cmd->image_number,
+ cmd->placement_id, "OK");
+}
+
+/// Creates the 'OK' response to the current command (unless suppressed).
+static void gr_reportsuccess_frame(ImageFrame *frame) {
+ uint32_t id = frame->image->query_id ? frame->image->query_id
+ : frame->image->image_id;
+ if (frame->quiet < 1)
+ gr_createresponse(id, frame->image->image_number,
+ frame->image->initial_placement_id, "OK");
+}
+
+/// Creates an error response to the current command (unless suppressed).
+static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) {
+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
+ graphics_command_result.error = 1;
+ va_list args;
+ va_start(args, format);
+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
+ va_end(args);
+
+ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command);
+ if (cmd->quiet < 2)
+ gr_createresponse(cmd->image_id, cmd->image_number,
+ cmd->placement_id, errmsg);
+}
+
+/// Creates an error response to the current command (unless suppressed).
+static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) {
+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
+ graphics_command_result.error = 1;
+ va_list args;
+ va_start(args, format);
+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
+ va_end(args);
+
+ if (!frame) {
+ fprintf(stderr, "%s\n", errmsg);
+ gr_createresponse(0, 0, 0, errmsg);
+ } else {
+ uint32_t id = frame->image->query_id ? frame->image->query_id
+ : frame->image->image_id;
+ fprintf(stderr, "%s id=%u\n", errmsg, id);
+ if (frame->quiet < 2)
+ gr_createresponse(id, frame->image->image_number,
+ frame->image->initial_placement_id,
+ errmsg);
+ }
+}
+
+/// Loads an image and creates a success/failure response. Returns `frame`, or
+/// NULL if it's a query action and the image was deleted.
+static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) {
+ gr_load_imlib_object(frame);
+ if (!frame->imlib_object) {
+ gr_reporterror_frame(frame, "EBADF: could not load image");
+ } else {
+ gr_reportsuccess_frame(frame);
+ }
+ // If it was a query action, discard the image.
+ if (frame->image->query_id) {
+ gr_delete_image(frame->image);
+ return NULL;
+ }
+ return frame;
+}
+
+/// Creates an appropriate uploading failure response to the current command.
+static void gr_reportuploaderror(ImageFrame *frame) {
+ switch (frame->uploading_failure) {
+ case 0:
+ return;
+ case ERROR_CANNOT_OPEN_CACHED_FILE:
+ gr_reporterror_frame(frame,
+ "EIO: could not create a file for image");
+ break;
+ case ERROR_OVER_SIZE_LIMIT:
+ gr_reporterror_frame(
+ frame,
+ "EFBIG: the size of the uploaded image exceeded "
+ "the image size limit %u",
+ graphics_max_single_image_file_size);
+ break;
+ case ERROR_UNEXPECTED_SIZE:
+ gr_reporterror_frame(frame,
+ "EINVAL: the size of the uploaded image %u "
+ "doesn't match the expected size %u",
+ frame->disk_size, frame->expected_size);
+ break;
+ };
+}
+
+/// Displays a non-virtual placement. This functions records the information in
+/// `graphics_command_result`, the placeholder itself is created by the terminal
+/// after handling the current command in the graphics module.
+static void gr_display_nonvirtual_placement(ImagePlacement *placement) {
+ if (placement->virtual)
+ return;
+ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS)
+ return;
+ // Infer the placement size if needed.
+ gr_infer_placement_size_maybe(placement);
+ // Populate the information about the placeholder which will be created
+ // by the terminal.
+ graphics_command_result.create_placeholder = 1;
+ graphics_command_result.placeholder.image_id = placement->image->image_id;
+ graphics_command_result.placeholder.placement_id = placement->placement_id;
+ graphics_command_result.placeholder.columns = placement->cols;
+ graphics_command_result.placeholder.rows = placement->rows;
+ graphics_command_result.placeholder.do_not_move_cursor =
+ placement->do_not_move_cursor;
+ GR_LOG("Creating a placeholder for %u/%u %d x %d\n",
+ placement->image->image_id, placement->placement_id,
+ placement->cols, placement->rows);
+}
+
+/// Marks the rows that are occupied by the image as dirty.
+static void gr_schedule_image_redraw(Image *img) {
+ if (!img)
+ return;
+ gr_schedule_image_redraw_by_id(img->image_id);
+}
+
+/// Appends data from `payload` to the frame `frame` when using direct
+/// transmission. Note that we report errors only for the final command
+/// (`!more`) to avoid spamming the client. If the frame is not specified, use
+/// the image id and frame index we are currently uploading.
+static void gr_append_data(ImageFrame *frame, const char *payload, int more) {
+ if (!frame) {
+ Image *img = gr_find_image(current_upload_image_id);
+ frame = gr_get_frame(img, current_upload_frame_index);
+ GR_LOG("Appending data to image %u frame %d\n",
+ current_upload_image_id, current_upload_frame_index);
+ if (!img)
+ GR_LOG("ERROR: this image doesn't exist\n");
+ if (!frame)
+ GR_LOG("ERROR: this frame doesn't exist\n");
+ }
+ if (!more) {
+ current_upload_image_id = 0;
+ current_upload_frame_index = 0;
+ }
+ if (!frame) {
+ if (!more)
+ gr_reporterror_frame(NULL, "ENOENT: could not find the "
+ "image to append data to");
+ return;
+ }
+ if (frame->status != STATUS_UPLOADING) {
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+
+ // Decode the data.
+ size_t data_size = 0;
+ char *data = gr_base64dec(payload, &data_size);
+
+ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size,
+ frame->disk_size + data_size);
+
+ // Do not append this data if the image exceeds the size limit.
+ if (frame->disk_size + data_size >
+ graphics_max_single_image_file_size ||
+ frame->expected_size > graphics_max_single_image_file_size) {
+ free(data);
+ gr_delete_imagefile(frame);
+ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+
+ // If there is no open file corresponding to the image, create it.
+ if (!frame->open_file) {
+ gr_make_sure_tmpdir_exists();
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ FILE *file = fopen(filename, frame->disk_size ? "a" : "w");
+ if (!file) {
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+ frame->open_file = file;
+ }
+
+ // Write data to the file and update disk size variables.
+ fwrite(data, 1, data_size, frame->open_file);
+ free(data);
+ frame->disk_size += data_size;
+ frame->image->total_disk_size += data_size;
+ images_disk_size += data_size;
+ gr_touch_frame(frame);
+
+ if (more) {
+ current_upload_image_id = frame->image->image_id;
+ current_upload_frame_index = frame->index;
+ } else {
+ current_upload_image_id = 0;
+ current_upload_frame_index = 0;
+ // Close the file.
+ if (frame->open_file) {
+ fclose(frame->open_file);
+ frame->open_file = NULL;
+ }
+ frame->status = STATUS_UPLOADING_SUCCESS;
+ uint32_t placement_id = frame->image->default_placement;
+ if (frame->expected_size &&
+ frame->expected_size != frame->disk_size) {
+ // Report failure if the uploaded image size doesn't
+ // match the expected size.
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
+ gr_reportuploaderror(frame);
+ } else {
+ // Make sure to redraw all existing image instances.
+ gr_schedule_image_redraw(frame->image);
+ // Try to load the image into ram and report the result.
+ frame = gr_loadimage_and_report(frame);
+ // If there is a non-virtual image placement, we may
+ // need to display it.
+ if (frame && frame->index == 1) {
+ Image *img = frame->image;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(img->placements, placement, {
+ gr_display_nonvirtual_placement(placement);
+ });
+ }
+ }
+ }
+
+ // Check whether we need to delete old images.
+ gr_check_limits();
+}
+
+/// Finds the image either by id or by number specified in the command and sets
+/// the image_id of `cmd` if the image was found.
+static Image *gr_find_image_for_command(GraphicsCommand *cmd) {
+ if (cmd->image_id)
+ return gr_find_image(cmd->image_id);
+ Image *img = NULL;
+ // If the image number is not specified, we can't find the image, unless
+ // it's a put command, in which case we will try the last image.
+ if (cmd->image_number == 0 && cmd->action == 'p')
+ img = gr_find_image(last_image_id);
+ else
+ img = gr_find_image_by_number(cmd->image_number);
+ if (img)
+ cmd->image_id = img->image_id;
+ return img;
+}
+
+/// Creates a new image or a new frame in an existing image (depending on the
+/// command's action) and initializes its parameters from the command.
+static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) {
+ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 &&
+ cmd->compression != 0) {
+ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only "
+ "for raw pixel data (f=32 or f=24)");
+ // Even though we report an error, we still create an image.
+ }
+
+ Image *img = NULL;
+ if (cmd->action == 'f') {
+ // If it's a frame transmission action, there must be an
+ // existing image.
+ img = gr_find_image_for_command(cmd);
+ if (!img) {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return NULL;
+ }
+ } else {
+ // Otherwise create a new image object. If the action is `q`,
+ // we'll use random id instead of the one specified in the
+ // command.
+ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id;
+ img = gr_new_image(image_id);
+ if (!img)
+ return NULL;
+ if (cmd->action == 'q')
+ img->query_id = cmd->image_id;
+ else if (!cmd->image_id)
+ cmd->image_id = img->image_id;
+ // Set the image number.
+ img->image_number = cmd->image_number;
+ }
+
+ ImageFrame *frame = gr_append_new_frame(img);
+ // Initialize the frame.
+ frame->expected_size = cmd->size;
+ frame->format = cmd->format;
+ frame->compression = cmd->compression;
+ frame->background_color = cmd->background_color;
+ frame->background_frame_index = cmd->background_frame;
+ frame->gap = cmd->gap;
+ img->total_duration += frame->gap;
+ frame->blend = !cmd->replace_instead_of_blending;
+ frame->data_pix_width = cmd->frame_pix_width;
+ frame->data_pix_height = cmd->frame_pix_height;
+ if (cmd->action == 'f') {
+ frame->x = cmd->frame_dst_pix_x;
+ frame->y = cmd->frame_dst_pix_y;
+ }
+ // We save the quietness information in the frame because for direct
+ // transmission subsequent transmission command won't contain this info.
+ frame->quiet = cmd->quiet;
+ return frame;
+}
+
+/// Removes a file if it actually looks like a temporary file.
+static void gr_delete_tmp_file(const char *filename) {
+ if (strstr(filename, "tty-graphics-protocol") == NULL)
+ return;
+ if (strstr(filename, "/tmp/") != filename) {
+ const char *tmpdir = getenv("TMPDIR");
+ if (!tmpdir || !tmpdir[0] ||
+ strstr(filename, tmpdir) != filename)
+ return;
+ }
+ unlink(filename);
+}
+
+/// Handles a data transmission command.
+static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) {
+ // The default is direct transmission.
+ if (!cmd->transmission_medium)
+ cmd->transmission_medium = 'd';
+
+ // If neither id, nor image number is specified, and the transmission
+ // medium is 'd' (or unspecified), and there is an active direct upload,
+ // this is a continuation of the upload.
+ if (current_upload_image_id != 0 && cmd->image_id == 0 &&
+ cmd->image_number == 0 && cmd->transmission_medium == 'd') {
+ cmd->image_id = current_upload_image_id;
+ GR_LOG("No images id is specified, continuing uploading %u\n",
+ cmd->image_id);
+ }
+
+ ImageFrame *frame = NULL;
+ if (cmd->transmission_medium == 'f' ||
+ cmd->transmission_medium == 't') {
+ // File transmission.
+ // Create a new image or a new frame of an existing image.
+ frame = gr_new_image_or_frame_from_command(cmd);
+ if (!frame)
+ return NULL;
+ last_image_id = frame->image->image_id;
+ // Decode the filename.
+ char *original_filename = gr_base64dec(cmd->payload, NULL);
+ GR_LOG("Copying image %s\n",
+ sanitized_filename(original_filename));
+ // Stat the file and check that it's a regular file and not too
+ // big.
+ struct stat st;
+ int stat_res = stat(original_filename, &st);
+ const char *stat_error = NULL;
+ if (stat_res)
+ stat_error = strerror(errno);
+ else if (!S_ISREG(st.st_mode))
+ stat_error = "Not a regular file";
+ else if (st.st_size == 0)
+ stat_error = "The size of the file is zero";
+ else if (st.st_size > graphics_max_single_image_file_size)
+ stat_error = "The file is too large";
+ if (stat_error) {
+ gr_reporterror_cmd(cmd,
+ "EBADF: %s", stat_error);
+ fprintf(stderr, "Could not load the file %s\n",
+ sanitized_filename(original_filename));
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
+ } else {
+ gr_make_sure_tmpdir_exists();
+ // Build the filename for the cached copy of the file.
+ char cache_filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, cache_filename,
+ MAX_FILENAME_SIZE);
+ // We will create a symlink to the original file, and
+ // then copy the file to the temporary cache dir. We do
+ // this symlink trick mostly to be able to use cp for
+ // copying, and avoid escaping file name characters when
+ // calling system at the same time.
+ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0};
+ strcat(tmp_filename_symlink, cache_filename);
+ strcat(tmp_filename_symlink, ".sym");
+ char command[MAX_FILENAME_SIZE + 256];
+ size_t len =
+ snprintf(command, MAX_FILENAME_SIZE + 255,
+ "cp '%s' '%s'", tmp_filename_symlink,
+ cache_filename);
+ if (len > MAX_FILENAME_SIZE + 255 ||
+ symlink(original_filename, tmp_filename_symlink) ||
+ system(command) != 0) {
+ gr_reporterror_cmd(cmd,
+ "EBADF: could not copy the "
+ "image to the cache dir");
+ fprintf(stderr,
+ "Could not copy the image "
+ "%s (symlink %s) to %s",
+ sanitized_filename(original_filename),
+ tmp_filename_symlink, cache_filename);
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
+ } else {
+ // Get the file size of the copied file.
+ frame->status = STATUS_UPLOADING_SUCCESS;
+ frame->disk_size = st.st_size;
+ frame->image->total_disk_size += st.st_size;
+ images_disk_size += frame->disk_size;
+ if (frame->expected_size &&
+ frame->expected_size != frame->disk_size) {
+ // The file has unexpected size.
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure =
+ ERROR_UNEXPECTED_SIZE;
+ gr_reportuploaderror(frame);
+ } else {
+ // Everything seems fine, try to load
+ // and redraw existing instances.
+ gr_schedule_image_redraw(frame->image);
+ frame = gr_loadimage_and_report(frame);
+ }
+ }
+ // Delete the symlink.
+ unlink(tmp_filename_symlink);
+ // Delete the original file if it's temporary.
+ if (cmd->transmission_medium == 't')
+ gr_delete_tmp_file(original_filename);
+ }
+ free(original_filename);
+ gr_check_limits();
+ } else if (cmd->transmission_medium == 'd') {
+ // Direct transmission (default if 't' is not specified).
+ frame = gr_get_last_frame(gr_find_image_for_command(cmd));
+ if (frame && frame->status == STATUS_UPLOADING) {
+ // This is a continuation of the previous transmission.
+ cmd->is_direct_transmission_continuation = 1;
+ gr_append_data(frame, cmd->payload, cmd->more);
+ return frame;
+ }
+ // If no action is specified, it's not the first transmission
+ // command. If we couldn't find the image, something went wrong
+ // and we should just drop this command.
+ if (cmd->action == 0)
+ return NULL;
+ // Otherwise create a new image or frame structure.
+ frame = gr_new_image_or_frame_from_command(cmd);
+ if (!frame)
+ return NULL;
+ last_image_id = frame->image->image_id;
+ frame->status = STATUS_UPLOADING;
+ // Start appending data.
+ gr_append_data(frame, cmd->payload, cmd->more);
+ } else {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: transmission medium '%c' is not supported",
+ cmd->transmission_medium);
+ return NULL;
+ }
+
+ return frame;
+}
+
+/// Handles the 'put' command by creating a placement.
+static void gr_handle_put_command(GraphicsCommand *cmd) {
+ if (cmd->image_id == 0 && cmd->image_number == 0) {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: neither image id nor image number "
+ "are specified or both are zero");
+ return;
+ }
+
+ // Find the image with the id or number.
+ Image *img = gr_find_image_for_command(cmd);
+ if (!img) {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return;
+ }
+
+ // Create a placement. If a placement with the same id already exists,
+ // it will be deleted. If the id is zero, a random id will be generated.
+ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id);
+ placement->virtual = cmd->virtual;
+ placement->src_pix_x = cmd->src_pix_x;
+ placement->src_pix_y = cmd->src_pix_y;
+ placement->src_pix_width = cmd->src_pix_width;
+ placement->src_pix_height = cmd->src_pix_height;
+ placement->cols = cmd->columns;
+ placement->rows = cmd->rows;
+ placement->do_not_move_cursor = cmd->do_not_move_cursor;
+
+ if (placement->virtual) {
+ placement->scale_mode = SCALE_MODE_CONTAIN;
+ } else if (placement->cols && placement->rows) {
+ // For classic placements the default is to stretch the image if
+ // both cols and rows are specified.
+ placement->scale_mode = SCALE_MODE_FILL;
+ } else if (placement->cols || placement->rows) {
+ // But if only one of them is specified, the default is to
+ // contain.
+ placement->scale_mode = SCALE_MODE_CONTAIN;
+ } else {
+ // If none of them are specified, the default is to use the
+ // original size.
+ placement->scale_mode = SCALE_MODE_NONE;
+ }
+
+ // Display the placement unless it's virtual.
+ gr_display_nonvirtual_placement(placement);
+
+ // Report success.
+ gr_reportsuccess_cmd(cmd);
+}
+
+/// Information about what to delete.
+typedef struct DeletionData {
+ uint32_t image_id;
+ uint32_t placement_id;
+ /// If true, delete the image object if there are no more placements.
+ char delete_image_if_no_ref;
+} DeletionData;
+
+/// The callback called for each cell to perform deletion.
+static int gr_deletion_callback(void *data, uint32_t image_id,
+ uint32_t placement_id, int col,
+ int row, char is_classic) {
+ DeletionData *del_data = data;
+ // Leave unicode placeholders alone.
+ if (!is_classic)
+ return 0;
+ if (del_data->image_id && del_data->image_id != image_id)
+ return 0;
+ if (del_data->placement_id && del_data->placement_id != placement_id)
+ return 0;
+ Image *img = gr_find_image(image_id);
+ // If the image is already deleted, just erase the placeholder.
+ if (!img)
+ return 1;
+ // Delete the placement.
+ if (placement_id)
+ gr_delete_placement(gr_find_placement(img, placement_id));
+ // Delete the image if image deletion is requested (uppercase delete
+ // specifier) and there are no more placements.
+ if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0)
+ gr_delete_image(img);
+ return 1;
+}
+
+/// Handles the delete command.
+static void gr_handle_delete_command(GraphicsCommand *cmd) {
+ DeletionData del_data = {0};
+ del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0;
+ char d = tolower(cmd->delete_specifier);
+
+ if (d == 'n') {
+ d = 'i';
+ Image *img = gr_find_image_by_number(cmd->image_number);
+ if (!img)
+ return;
+ del_data.image_id = img->image_id;
+ }
+
+ if (!d || d == 'a') {
+ // Delete all visible placements.
+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
+ } else if (d == 'i') {
+ // Delete the specified image by image id and maybe placement
+ // id.
+ if (!del_data.image_id)
+ del_data.image_id = cmd->image_id;
+ if (!del_data.image_id) {
+ fprintf(stderr,
+ "ERROR: image id is not specified in the "
+ "delete command\n");
+ return;
+ }
+ del_data.placement_id = cmd->placement_id;
+ // NOTE: It's not very clear whether we should delete the image
+ // even if there are no _visible_ placements to delete. We do
+ // this because otherwise there is no way to delete an image
+ // with virtual placements in one command.
+ if (!del_data.placement_id && del_data.delete_image_if_no_ref)
+ gr_delete_image(gr_find_image(cmd->image_id));
+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
+ } else {
+ fprintf(stderr,
+ "WARNING: unsupported value of the d key: '%c'. The "
+ "command is ignored.\n",
+ cmd->delete_specifier);
+ }
+}
+
+static void gr_handle_animation_control_command(GraphicsCommand *cmd) {
+ if (cmd->image_id == 0 && cmd->image_number == 0) {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: neither image id nor image number "
+ "are specified or both are zero");
+ return;
+ }
+
+ // Find the image with the id or number.
+ Image *img = gr_find_image_for_command(cmd);
+ if (!img) {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return;
+ }
+
+ // Find the frame to edit, if requested.
+ ImageFrame *frame = NULL;
+ if (cmd->edit_frame)
+ frame = gr_get_frame(img, cmd->edit_frame);
+ if (cmd->edit_frame || cmd->gap) {
+ if (!frame) {
+ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found",
+ cmd->edit_frame);
+ return;
+ }
+ if (cmd->gap) {
+ img->total_duration -= frame->gap;
+ frame->gap = cmd->gap;
+ img->total_duration += frame->gap;
+ }
+ }
+
+ // Set animation-related parameters of the image.
+ if (cmd->current_frame)
+ img->current_frame = cmd->current_frame;
+ if (cmd->animation_state) {
+ if (cmd->animation_state == 1) {
+ img->animation_state = ANIMATION_STATE_STOPPED;
+ } else if (cmd->animation_state == 2) {
+ img->animation_state = ANIMATION_STATE_LOADING;
+ } else if (cmd->animation_state == 3) {
+ img->animation_state = ANIMATION_STATE_LOOPING;
+ } else {
+ gr_reporterror_cmd(
+ cmd, "EINVAL: invalid animation state: %d",
+ cmd->animation_state);
+ }
+ }
+ // TODO: Set the number of loops to cmd->loops
+
+ // Make sure we redraw all instances of the image.
+ gr_schedule_image_redraw(img);
+}
+
+/// Handles a command.
+static void gr_handle_command(GraphicsCommand *cmd) {
+ if (!cmd->image_id && !cmd->image_number) {
+ // If there is no image id or image number, nobody expects a
+ // response, so set quiet to 2.
+ cmd->quiet = 2;
+ }
+ ImageFrame *frame = NULL;
+ switch (cmd->action) {
+ case 0:
+ // If no action is specified, it may be a data transmission
+ // command if 'm=' is specified.
+ if (cmd->is_data_transmission) {
+ gr_handle_transmit_command(cmd);
+ break;
+ }
+ gr_reporterror_cmd(cmd, "EINVAL: no action specified");
+ break;
+ case 't':
+ case 'q':
+ case 'f':
+ // Transmit data. 'q' means query, which is basically the same
+ // as transmit, but the image is discarded, and the id is fake.
+ // 'f' appends a frame to an existing image.
+ gr_handle_transmit_command(cmd);
+ break;
+ case 'p':
+ // Display (put) the image.
+ gr_handle_put_command(cmd);
+ break;
+ case 'T':
+ // Transmit and display.
+ frame = gr_handle_transmit_command(cmd);
+ if (frame && !cmd->is_direct_transmission_continuation) {
+ gr_handle_put_command(cmd);
+ if (cmd->placement_id)
+ frame->image->initial_placement_id =
+ cmd->placement_id;
+ }
+ break;
+ case 'd':
+ gr_handle_delete_command(cmd);
+ break;
+ case 'a':
+ gr_handle_animation_control_command(cmd);
+ break;
+ default:
+ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c",
+ cmd->action);
+ return;
+ }
+}
+
+/// A partially parsed key-value pair.
+typedef struct KeyAndValue {
+ char *key_start;
+ char *val_start;
+ unsigned key_len, val_len;
+} KeyAndValue;
+
+/// Parses the value of a key and assigns it to the appropriate field of `cmd`.
+static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) {
+ char *key_start = kv->key_start;
+ char *key_end = key_start + kv->key_len;
+ char *value_start = kv->val_start;
+ char *value_end = value_start + kv->val_len;
+ // Currently all keys are one-character.
+ if (key_end - key_start != 1) {
+ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s",
+ key_end - key_start, key_start);
+ return;
+ }
+ long num = 0;
+ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' ||
+ *key_start == 'o') {
+ // Some keys have one-character values.
+ if (value_end - value_start != 1) {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: value of 'a', 't' or 'd' must be a "
+ "single char: %s",
+ key_start);
+ return;
+ }
+ } else {
+ // All the other keys have integer values.
+ char *num_end = NULL;
+ num = strtol(value_start, &num_end, 10);
+ if (num_end != value_end) {
+ gr_reporterror_cmd(
+ cmd, "EINVAL: could not parse number value: %s",
+ key_start);
+ return;
+ }
+ }
+ switch (*key_start) {
+ case 'a':
+ cmd->action = *value_start;
+ break;
+ case 't':
+ cmd->transmission_medium = *value_start;
+ break;
+ case 'd':
+ cmd->delete_specifier = *value_start;
+ break;
+ case 'q':
+ cmd->quiet = num;
+ break;
+ case 'f':
+ cmd->format = num;
+ if (num != 0 && num != 24 && num != 32 && num != 100) {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: unsupported format specification: %s",
+ key_start);
+ }
+ break;
+ case 'o':
+ cmd->compression = *value_start;
+ if (cmd->compression != 'z') {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: unsupported compression "
+ "specification: %s",
+ key_start);
+ }
+ break;
+ case 's':
+ if (cmd->action == 'a')
+ cmd->animation_state = num;
+ else
+ cmd->frame_pix_width = num;
+ break;
+ case 'v':
+ if (cmd->action == 'a')
+ cmd->loops = num;
+ else
+ cmd->frame_pix_height = num;
+ break;
+ case 'i':
+ cmd->image_id = num;
+ break;
+ case 'I':
+ cmd->image_number = num;
+ break;
+ case 'p':
+ cmd->placement_id = num;
+ break;
+ case 'x':
+ cmd->src_pix_x = num;
+ cmd->frame_dst_pix_x = num;
+ break;
+ case 'y':
+ if (cmd->action == 'f')
+ cmd->frame_dst_pix_y = num;
+ else
+ cmd->src_pix_y = num;
+ break;
+ case 'w':
+ cmd->src_pix_width = num;
+ break;
+ case 'h':
+ cmd->src_pix_height = num;
+ break;
+ case 'c':
+ if (cmd->action == 'f')
+ cmd->background_frame = num;
+ else if (cmd->action == 'a')
+ cmd->current_frame = num;
+ else
+ cmd->columns = num;
+ break;
+ case 'r':
+ if (cmd->action == 'f' || cmd->action == 'a')
+ cmd->edit_frame = num;
+ else
+ cmd->rows = num;
+ break;
+ case 'm':
+ cmd->is_data_transmission = 1;
+ cmd->more = num;
+ break;
+ case 'S':
+ cmd->size = num;
+ break;
+ case 'U':
+ cmd->virtual = num;
+ break;
+ case 'X':
+ if (cmd->action == 'f')
+ cmd->replace_instead_of_blending = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'Y':
+ if (cmd->action == 'f')
+ cmd->background_color = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'z':
+ if (cmd->action == 'f' || cmd->action == 'a')
+ cmd->gap = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'C':
+ cmd->do_not_move_cursor = num;
+ break;
+ default:
+ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s",
+ key_start);
+ return;
+ }
+}
+
+/// Parse and execute a graphics command. `buf` must start with 'G' and contain
+/// at least `len + 1` characters. Returns 1 on success.
+int gr_parse_command(char *buf, size_t len) {
+ if (buf[0] != 'G')
+ return 0;
+
+ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
+
+ global_command_counter++;
+ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
+
+ // Eat the 'G'.
+ ++buf;
+ --len;
+
+ GraphicsCommand cmd = {.command = buf};
+ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to
+ // parse the payload.
+ char state = 'k';
+ // An array of partially parsed key-value pairs.
+ KeyAndValue key_vals[32];
+ unsigned key_vals_count = 0;
+ char *key_start = buf;
+ char *key_end = NULL;
+ char *val_start = NULL;
+ char *val_end = NULL;
+ char *c = buf;
+ while (c - buf < len + 1) {
+ if (state == 'k') {
+ switch (*c) {
+ case ',':
+ case ';':
+ case '\0':
+ state = *c == ',' ? 'k' : 'p';
+ key_end = c;
+ gr_reporterror_cmd(
+ &cmd, "EINVAL: key without value: %s ",
+ key_start);
+ break;
+ case '=':
+ key_end = c;
+ state = 'v';
+ val_start = c + 1;
+ break;
+ default:
+ break;
+ }
+ } else if (state == 'v') {
+ switch (*c) {
+ case ',':
+ case ';':
+ case '\0':
+ state = *c == ',' ? 'k' : 'p';
+ val_end = c;
+ if (key_vals_count >=
+ sizeof(key_vals) / sizeof(*key_vals)) {
+ gr_reporterror_cmd(&cmd,
+ "EINVAL: too many "
+ "key-value pairs");
+ break;
+ }
+ key_vals[key_vals_count].key_start = key_start;
+ key_vals[key_vals_count].val_start = val_start;
+ key_vals[key_vals_count].key_len =
+ key_end - key_start;
+ key_vals[key_vals_count].val_len =
+ val_end - val_start;
+ ++key_vals_count;
+ key_start = c + 1;
+ break;
+ default:
+ break;
+ }
+ } else if (state == 'p') {
+ cmd.payload = c;
+ // break out of the loop, we don't check the payload
+ break;
+ }
+ ++c;
+ }
+
+ // Set the action key ('a=') first because we need it to disambiguate
+ // some keys. Also set 'i=' and 'I=' for better error reporting.
+ for (unsigned i = 0; i < key_vals_count; ++i) {
+ if (key_vals[i].key_len == 1) {
+ char *start = key_vals[i].key_start;
+ if (*start == 'a' || *start == 'i' || *start == 'I') {
+ gr_set_keyvalue(&cmd, &key_vals[i]);
+ break;
+ }
+ }
+ }
+ // Set the rest of the keys.
+ for (unsigned i = 0; i < key_vals_count; ++i)
+ gr_set_keyvalue(&cmd, &key_vals[i]);
+
+ if (!cmd.payload)
+ cmd.payload = buf + len;
+
+ if (cmd.payload && cmd.payload[0])
+ GR_LOG(" payload size: %ld\n", strlen(cmd.payload));
+
+ if (!graphics_command_result.error)
+ gr_handle_command(&cmd);
+
+ if (graphics_debug_mode) {
+ fprintf(stderr, "Response: ");
+ for (const char *resp = graphics_command_result.response;
+ *resp != '\0'; ++resp) {
+ if (isprint(*resp))
+ fprintf(stderr, "%c", *resp);
+ else
+ fprintf(stderr, "(0x%x)", *resp);
+ }
+ fprintf(stderr, "\n");
+ }
+
+ // Make sure that we suppress response if needed. Usually cmd.quiet is
+ // taken into account when creating the response, but it's not very
+ // reliable in the current implementation.
+ if (cmd.quiet) {
+ if (!graphics_command_result.error || cmd.quiet >= 2)
+ graphics_command_result.response[0] = '\0';
+ }
+
+ return 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// base64 decoding part is basically copied from st.c
+////////////////////////////////////////////////////////////////////////////////
+
+static const char gr_base64_digits[] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
+ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2,
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+static char gr_base64_getc(const char **src) {
+ while (**src && !isprint(**src))
+ (*src)++;
+ return **src ? *((*src)++) : '='; /* emulate padding if string ends */
+}
+
+char *gr_base64dec(const char *src, size_t *size) {
+ size_t in_len = strlen(src);
+ char *result, *dst;
+
+ result = dst = malloc((in_len + 3) / 4 * 3 + 1);
+ while (*src) {
+ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+
+ if (a == -1 || b == -1)
+ break;
+
+ *dst++ = (a << 2) | ((b & 0x30) >> 4);
+ if (c == -1)
+ break;
+ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
+ if (d == -1)
+ break;
+ *dst++ = ((c & 0x03) << 6) | d;
+ }
+ *dst = '\0';
+ if (size) {
+ *size = dst - result;
+ }
+ return result;
+}
diff --git a/files/config/suckless/st/graphics.h b/files/config/suckless/st/graphics.h
new file mode 100644
index 0000000..2e75dea
--- /dev/null
+++ b/files/config/suckless/st/graphics.h
@@ -0,0 +1,107 @@
+
+#include <stdint.h>
+#include <sys/types.h>
+#include <X11/Xlib.h>
+
+/// Initialize the graphics module.
+void gr_init(Display *disp, Visual *vis, Colormap cm);
+/// Deinitialize the graphics module.
+void gr_deinit();
+
+/// Add an image rectangle to a list if rectangles to draw. This function may
+/// actually draw some rectangles, or it may wait till more rectangles are
+/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`.
+/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the
+/// part of the image to draw (row/col indices are zero-based, ends are
+/// excluded).
+/// - `x_col` and `y_row` are the coordinates of the top-left corner of the
+/// image in the terminal grid.
+/// - `x_pix` and `y_pix` are the same but in pixels.
+/// - `reverse` indicates whether colors should be inverted.
+void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
+ int img_start_col, int img_end_col, int img_start_row,
+ int img_end_row, int x_col, int y_row, int x_pix,
+ int y_pix, int cw, int ch, int reverse);
+/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
+void gr_start_drawing(Drawable buf, int cw, int ch);
+/// Finish image drawing. This functions will draw all the rectangles left to
+/// draw.
+void gr_finish_drawing(Drawable buf);
+/// Mark rows containing animations as dirty if it's time to redraw them. Must
+/// be called right after `gr_start_drawing`.
+void gr_mark_dirty_animations(int *dirty, int rows);
+
+/// Parse and execute a graphics command. `buf` must start with 'G' and contain
+/// at least `len + 1` characters (including '\0'). Returns 1 on success.
+/// Additional informations is returned through `graphics_command_result`.
+int gr_parse_command(char *buf, size_t len);
+
+/// Executes `command` with the name of the file corresponding to `image_id` as
+/// the argument. Executes xmessage with an error message on failure.
+void gr_preview_image(uint32_t image_id, const char *command);
+
+/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
+/// containing the information about an image and placement, and <st> is
+/// specified with `st_executable`.
+void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
+ uint32_t imgcol, uint32_t imgrow,
+ char is_classic_placeholder, int32_t diacritic_count,
+ char *st_executable);
+
+/// Dumps the internal state (images and placements) to stderr.
+void gr_dump_state();
+
+/// Unloads images to reduce RAM usage.
+void gr_unload_images_to_reduce_ram();
+
+/// Executes `callback` for each image cell. `callback` may return 1 to erase
+/// the cell or 0 to keep it. This function is implemented in `st.c`.
+void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
+ uint32_t placement_id, int col,
+ int row, char is_classic),
+ void *data);
+
+/// Marks all the rows containing the image with `image_id` as dirty.
+void gr_schedule_image_redraw_by_id(uint32_t image_id);
+
+typedef enum {
+ GRAPHICS_DEBUG_NONE = 0,
+ GRAPHICS_DEBUG_LOG = 1,
+ GRAPHICS_DEBUG_LOG_AND_BOXES = 2,
+} GraphicsDebugMode;
+
+/// Print additional information, draw bounding bounding boxes, etc.
+extern GraphicsDebugMode graphics_debug_mode;
+
+/// Whether to display images or just draw bounding boxes.
+extern char graphics_display_images;
+
+/// The time in milliseconds until the next redraw to update animations.
+/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`.
+extern int graphics_next_redraw_delay;
+
+#define MAX_GRAPHICS_RESPONSE_LEN 256
+
+/// A structure representing the result of a graphics command.
+typedef struct {
+ /// Indicates if the terminal needs to be redrawn.
+ char redraw;
+ /// The response of the command that should be sent back to the client
+ /// (may be empty if the quiet flag is set).
+ char response[MAX_GRAPHICS_RESPONSE_LEN];
+ /// Whether there was an error executing this command (not very useful,
+ /// the response must be sent back anyway).
+ char error;
+ /// Whether the terminal has to create a placeholder for a non-virtual
+ /// placement.
+ char create_placeholder;
+ /// The placeholder that needs to be created.
+ struct {
+ uint32_t rows, columns;
+ uint32_t image_id, placement_id;
+ char do_not_move_cursor;
+ } placeholder;
+} GraphicsCommandResult;
+
+/// The result of a graphics command.
+extern GraphicsCommandResult graphics_command_result;
diff --git a/files/config/suckless/st/icat-mini.sh b/files/config/suckless/st/icat-mini.sh
new file mode 100755
index 0000000..0a8ebab
--- /dev/null
+++ b/files/config/suckless/st/icat-mini.sh
@@ -0,0 +1,801 @@
+#!/bin/sh
+
+# vim: shiftwidth=4
+
+script_name="$(basename "$0")"
+
+short_help="Usage: $script_name [OPTIONS] <image_file>
+
+This is a script to display images in the terminal using the kitty graphics
+protocol with Unicode placeholders. It is very basic, please use something else
+if you have alternatives.
+
+Options:
+ -h Show this help.
+ -s SCALE The scale of the image, may be floating point.
+ -c N, --cols N The number of columns.
+ -r N, --rows N The number of rows.
+ --max-cols N The maximum number of columns.
+ --max-rows N The maximum number of rows.
+ --cell-size WxH The cell size in pixels.
+ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'.
+ --speed SPEED The multiplier for the animation speed (float).
+"
+
+# Exit the script on keyboard interrupt
+trap "echo 'icat-mini was interrupted' >&2; exit 1" INT
+
+cols=""
+rows=""
+file=""
+tty="/dev/tty"
+uploading_method="auto"
+cell_size=""
+scale=1
+max_cols=""
+max_rows=""
+speed=""
+
+# Parse the command line.
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -c|--columns|--cols)
+ cols="$2"
+ shift 2
+ ;;
+ -r|--rows|-l|--lines)
+ rows="$2"
+ shift 2
+ ;;
+ -s|--scale)
+ scale="$2"
+ shift 2
+ ;;
+ -h|--help)
+ echo "$short_help"
+ exit 0
+ ;;
+ -m|--upload-method|--uploading-method)
+ uploading_method="$2"
+ shift 2
+ ;;
+ --cell-size)
+ cell_size="$2"
+ shift 2
+ ;;
+ --max-cols)
+ max_cols="$2"
+ shift 2
+ ;;
+ --max-rows)
+ max_rows="$2"
+ shift 2
+ ;;
+ --speed)
+ speed="$2"
+ shift 2
+ ;;
+ --)
+ file="$2"
+ shift 2
+ ;;
+ -*)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ *)
+ if [ -n "$file" ]; then
+ echo "Multiple image files are not supported: $file and $1" >&2
+ exit 1
+ fi
+ file="$1"
+ shift
+ ;;
+ esac
+done
+
+file="$(realpath "$file")"
+
+#####################################################################
+# Adjust the terminal state
+#####################################################################
+
+stty_orig="$(stty -g < "$tty")"
+stty -echo < "$tty"
+# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some
+# horrible issues otherwise.
+stty susp undef < "$tty"
+stty -icanon < "$tty"
+
+restore_echo() {
+ [ -n "$stty_orig" ] || return
+ stty $stty_orig < "$tty"
+}
+
+trap restore_echo EXIT TERM
+
+#####################################################################
+# Detect imagemagick
+#####################################################################
+
+# If there is the 'magick' command, use it instead of separate 'convert' and
+# 'identify' commands.
+if command -v magick > /dev/null; then
+ identify="magick identify"
+ convert="magick"
+else
+ identify="identify"
+ convert="convert"
+fi
+
+#####################################################################
+# Detect tmux
+#####################################################################
+
+# Check if we are inside tmux.
+inside_tmux=""
+if [ -n "$TMUX" ]; then
+ case "$TERM" in
+ *tmux*|*screen*)
+ inside_tmux=1
+ ;;
+ esac
+fi
+
+#####################################################################
+# Compute the number of rows and columns
+#####################################################################
+
+is_pos_int() {
+ if [ -z "$1" ]; then
+ return 1 # false
+ fi
+ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then
+ if [ "$1" -gt 0 ]; then
+ return 0 # true
+ fi
+ fi
+ return 1 # false
+}
+
+if [ -n "$cols" ] || [ -n "$rows" ]; then
+ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then
+ echo "You can't specify both max-cols/rows and cols/rows" >&2
+ exit 1
+ fi
+fi
+
+# Get the max number of cols and rows.
+[ -n "$max_cols" ] || max_cols="$(tput cols)"
+[ -n "$max_rows" ] || max_rows="$(tput lines)"
+if [ "$max_rows" -gt 255 ]; then
+ max_rows=255
+fi
+
+python_ioctl_command="import array, fcntl, termios
+buf = array.array('H', [0, 0, 0, 0])
+fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
+print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))"
+
+# Get the cell size in pixels if either cols or rows are not specified.
+if [ -z "$cols" ] || [ -z "$rows" ]; then
+ cell_width=""
+ cell_height=""
+ # If the cell size is specified, use it.
+ if [ -n "$cell_size" ]; then
+ cell_width="${cell_size%x*}"
+ cell_height="${cell_size#*x}"
+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
+ echo "Invalid cell size: $cell_size" >&2
+ exit 1
+ fi
+ fi
+ # Otherwise try to use TIOCGWINSZ ioctl via python.
+ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
+ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)"
+ cell_width="${cell_size_ioctl% *}"
+ cell_height="${cell_size_ioctl#* }"
+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
+ cell_width=""
+ cell_height=""
+ fi
+ fi
+ # If it didn't work, try to use csi XTWINOPS.
+ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
+ if [ -n "$inside_tmux" ]; then
+ printf '\ePtmux;\e\e[16t\e\\' >> "$tty"
+ else
+ printf '\e[16t' >> "$tty"
+ fi
+ # The expected response will look like ^[[6;<height>;<width>t
+ term_response=""
+ while true; do
+ char=$(dd bs=1 count=1 <"$tty" 2>/dev/null)
+ if [ "$char" = "t" ]; then
+ break
+ fi
+ term_response="$term_response$char"
+ done
+ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)"
+ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)"
+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
+ cell_width=8
+ cell_height=16
+ fi
+ fi
+fi
+
+# Compute a formula with bc and round to the nearest integer.
+bc_round() {
+ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)"
+}
+
+# Compute the number of rows and columns of the image.
+if [ -z "$cols" ] || [ -z "$rows" ]; then
+ # Get the size of the image and its resolution. If it's an animation, use
+ # the first frame.
+ format_output="$($identify -format '%w %h\n' "$file" | head -1)"
+ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)"
+ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)"
+ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then
+ echo "Couldn't get image size from identify: $format_output" >&2
+ echo >&2
+ exit 1
+ fi
+ opt_cols_expr="(${scale}*${img_width}/${cell_width})"
+ opt_rows_expr="(${scale}*${img_height}/${cell_height})"
+ if [ -z "$cols" ] && [ -z "$rows" ]; then
+ # If columns and rows are not specified, compute the optimal values
+ # using the information about rows and columns per inch.
+ cols="$(bc_round "$opt_cols_expr")"
+ rows="$(bc_round "$opt_rows_expr")"
+ # Make sure that automatically computed rows and columns are within some
+ # sane limits
+ if [ "$cols" -gt "$max_cols" ]; then
+ rows="$(bc_round "$rows * $max_cols / $cols")"
+ cols="$max_cols"
+ fi
+ if [ "$rows" -gt "$max_rows" ]; then
+ cols="$(bc_round "$cols * $max_rows / $rows")"
+ rows="$max_rows"
+ fi
+ elif [ -z "$cols" ]; then
+ # If only one dimension is specified, compute the other one to match the
+ # aspect ratio as close as possible.
+ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")"
+ elif [ -z "$rows" ]; then
+ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")"
+ fi
+
+ if [ "$cols" -lt 1 ]; then
+ cols=1
+ fi
+ if [ "$rows" -lt 1 ]; then
+ rows=1
+ fi
+fi
+
+#####################################################################
+# Generate an image id
+#####################################################################
+
+image_id=""
+while [ -z "$image_id" ]; do
+ image_id="$(shuf -i 16777217-4294967295 -n 1)"
+ # Check that the id requires 24-bit fg colors.
+ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then
+ image_id=""
+ fi
+done
+
+#####################################################################
+# Uploading the image
+#####################################################################
+
+# Choose the uploading method
+if [ "$uploading_method" = "auto" ]; then
+ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then
+ uploading_method="direct"
+ else
+ uploading_method="file"
+ fi
+fi
+
+# Functions to emit the start and the end of a graphics command.
+if [ -n "$inside_tmux" ]; then
+ # If we are in tmux we have to wrap the command in Ptmux.
+ graphics_command_start='\ePtmux;\e\e_G'
+ graphics_command_end='\e\e\\\e\\'
+else
+ graphics_command_start='\e_G'
+ graphics_command_end='\e\\'
+fi
+
+start_gr_command() {
+ printf "$graphics_command_start" >> "$tty"
+}
+end_gr_command() {
+ printf "$graphics_command_end" >> "$tty"
+}
+
+# Send a graphics command with the correct start and end
+gr_command() {
+ start_gr_command
+ printf '%s' "$1" >> "$tty"
+ end_gr_command
+}
+
+# Send an uploading command. Usage: gr_upload <action> <command> <file>
+# Where <action> is a part of command that specifies the action, it will be
+# repeated for every chunk (if the method is direct), and <command> is the rest
+# of the command that specifies the image parameters. <action> and <command>
+# must not include the transmission method or ';'.
+# Example:
+# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
+gr_upload() {
+ arg_action="$1"
+ arg_command="$2"
+ arg_file="$3"
+ if [ "$uploading_method" = "file" ]; then
+ # base64-encode the filename
+ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0)
+ gr_command "${arg_action},${arg_command},t=f;${encoded_filename}"
+ fi
+ if [ "$uploading_method" = "direct" ]; then
+ # Create a temporary directory to store the chunked image.
+ chunkdir="$(mktemp -d)"
+ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then
+ echo "Can't create a temp dir" >&2
+ exit 1
+ fi
+ # base64-encode the file and split it into chunks. The size of each
+ # graphics command shouldn't be more than 4096, so we set the size of an
+ # encoded chunk to be 3968, slightly less than that.
+ chunk_size=3968
+ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_"
+
+ # Issue a command indicating that we want to start data transmission for
+ # a new image.
+ gr_command "${arg_action},${arg_command},t=d,m=1"
+
+ # Transmit chunks.
+ for chunk in "$chunkdir/chunk_"*; do
+ start_gr_command
+ printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty"
+ cat "$chunk" >> "$tty"
+ end_gr_command
+ rm "$chunk"
+ done
+
+ # Tell the terminal that we are done.
+ gr_command "${arg_action},i=$image_id,m=0"
+
+ # Remove the temporary directory.
+ rmdir "$chunkdir"
+ fi
+}
+
+delayed_frame_dir_cleanup() {
+ arg_frame_dir="$1"
+ sleep 2
+ if [ -n "$arg_frame_dir" ]; then
+ for frame in "$arg_frame_dir"/frame_*.png; do
+ rm "$frame"
+ done
+ rmdir "$arg_frame_dir"
+ fi
+}
+
+upload_image_and_print_placeholder() {
+ # Check if the file is an animation.
+ frame_count=$($identify -format '%n\n' "$file" | head -n 1)
+ if [ "$frame_count" -gt 1 ]; then
+ # The file is an animation, decompose into frames and upload each frame.
+ frame_dir="$(mktemp -d)"
+ frame_dir="$HOME/temp/frames${frame_dir}"
+ mkdir -p "$frame_dir"
+ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then
+ echo "Can't create a temp dir for frames" >&2
+ exit 1
+ fi
+
+ # Decompose the animation into separate frames.
+ $convert "$file" -coalesce "$frame_dir/frame_%06d.png"
+
+ # Get all frame delays at once, in centiseconds, as a space-separated
+ # string.
+ delays=$($identify -format "%T " "$file")
+
+ frame_number=1
+ for frame in "$frame_dir"/frame_*.png; do
+ # Read the delay for the current frame and convert it from
+ # centiseconds to milliseconds.
+ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number")
+ delay=$((delay * 10))
+ # If the delay is 0, set it to 100ms.
+ if [ "$delay" -eq 0 ]; then
+ delay=100
+ fi
+
+ if [ -n "$speed" ]; then
+ delay=$(bc_round "$delay / $speed")
+ fi
+
+ if [ "$frame_number" -eq 1 ]; then
+ # Upload the first frame with a=T
+ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame"
+ # Set the delay for the first frame and also play the animation
+ # in loading mode (s=2).
+ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}"
+ # Print the placeholder after the first frame to reduce the wait
+ # time.
+ print_placeholder
+ else
+ # Upload subsequent frames with a=f
+ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame"
+ fi
+
+ frame_number=$((frame_number + 1))
+ done
+
+ # Play the animation in loop mode (s=3).
+ gr_command "a=a,v=1,s=3,i=${image_id}"
+
+ # Remove the temporary directory, but do it in the background with a
+ # delay to avoid removing files before they are loaded by the terminal.
+ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null &
+ else
+ # The file is not an animation, upload it directly
+ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
+ # Print the placeholder
+ print_placeholder
+ fi
+}
+
+#####################################################################
+# Printing the image placeholder
+#####################################################################
+
+print_placeholder() {
+ # Each line starts with the escape sequence to set the foreground color to
+ # the image id.
+ blue="$(expr "$image_id" % 256 )"
+ green="$(expr \( "$image_id" / 256 \) % 256 )"
+ red="$(expr \( "$image_id" / 65536 \) % 256 )"
+ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")"
+ line_end="$(printf "\e[39;m")"
+
+ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )"
+ eval "id_diacritic=\$d${id4th}"
+
+ # Reset the brush state, mostly to reset the underline color.
+ printf "\e[0m"
+
+ # Fill the output with characters representing the image
+ for y in $(seq 0 "$(expr "$rows" - 1)"); do
+ eval "row_diacritic=\$d${y}"
+ printf '%s' "$line_start"
+ for x in $(seq 0 "$(expr "$cols" - 1)"); do
+ eval "col_diacritic=\$d${x}"
+ # Note that when $x is out of bounds, the column diacritic will
+ # be empty, meaning that the column should be guessed by the
+ # terminal.
+ if [ "$x" -ge "$num_diacritics" ]; then
+ printf '%s' "${placeholder}${row_diacritic}"
+ else
+ printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}"
+ fi
+ done
+ printf '%s\n' "$line_end"
+ done
+
+ printf "\e[0m"
+}
+
+d0="̅"
+d1="̍"
+d2="̎"
+d3="̐"
+d4="̒"
+d5="̽"
+d6="̾"
+d7="̿"
+d8="͆"
+d9="͊"
+d10="͋"
+d11="͌"
+d12="͐"
+d13="͑"
+d14="͒"
+d15="͗"
+d16="͛"
+d17="ͣ"
+d18="ͤ"
+d19="ͥ"
+d20="ͦ"
+d21="ͧ"
+d22="ͨ"
+d23="ͩ"
+d24="ͪ"
+d25="ͫ"
+d26="ͬ"
+d27="ͭ"
+d28="ͮ"
+d29="ͯ"
+d30="҃"
+d31="҄"
+d32="҅"
+d33="҆"
+d34="҇"
+d35="֒"
+d36="֓"
+d37="֔"
+d38="֕"
+d39="֗"
+d40="֘"
+d41="֙"
+d42="֜"
+d43="֝"
+d44="֞"
+d45="֟"
+d46="֠"
+d47="֡"
+d48="֨"
+d49="֩"
+d50="֫"
+d51="֬"
+d52="֯"
+d53="ׄ"
+d54="ؐ"
+d55="ؑ"
+d56="ؒ"
+d57="ؓ"
+d58="ؔ"
+d59="ؕ"
+d60="ؖ"
+d61="ؗ"
+d62="ٗ"
+d63="٘"
+d64="ٙ"
+d65="ٚ"
+d66="ٛ"
+d67="ٝ"
+d68="ٞ"
+d69="ۖ"
+d70="ۗ"
+d71="ۘ"
+d72="ۙ"
+d73="ۚ"
+d74="ۛ"
+d75="ۜ"
+d76="۟"
+d77="۠"
+d78="ۡ"
+d79="ۢ"
+d80="ۤ"
+d81="ۧ"
+d82="ۨ"
+d83="۫"
+d84="۬"
+d85="ܰ"
+d86="ܲ"
+d87="ܳ"
+d88="ܵ"
+d89="ܶ"
+d90="ܺ"
+d91="ܽ"
+d92="ܿ"
+d93="݀"
+d94="݁"
+d95="݃"
+d96="݅"
+d97="݇"
+d98="݉"
+d99="݊"
+d100="߫"
+d101="߬"
+d102="߭"
+d103="߮"
+d104="߯"
+d105="߰"
+d106="߱"
+d107="߳"
+d108="ࠖ"
+d109="ࠗ"
+d110="࠘"
+d111="࠙"
+d112="ࠛ"
+d113="ࠜ"
+d114="ࠝ"
+d115="ࠞ"
+d116="ࠟ"
+d117="ࠠ"
+d118="ࠡ"
+d119="ࠢ"
+d120="ࠣ"
+d121="ࠥ"
+d122="ࠦ"
+d123="ࠧ"
+d124="ࠩ"
+d125="ࠪ"
+d126="ࠫ"
+d127="ࠬ"
+d128="࠭"
+d129="॑"
+d130="॓"
+d131="॔"
+d132="ྂ"
+d133="ྃ"
+d134="྆"
+d135="྇"
+d136="፝"
+d137="፞"
+d138="፟"
+d139="៝"
+d140="᤺"
+d141="ᨗ"
+d142="᩵"
+d143="᩶"
+d144="᩷"
+d145="᩸"
+d146="᩹"
+d147="᩺"
+d148="᩻"
+d149="᩼"
+d150="᭫"
+d151="᭭"
+d152="᭮"
+d153="᭯"
+d154="᭰"
+d155="᭱"
+d156="᭲"
+d157="᭳"
+d158="᳐"
+d159="᳑"
+d160="᳒"
+d161="᳚"
+d162="᳛"
+d163="᳠"
+d164="᷀"
+d165="᷁"
+d166="᷃"
+d167="᷄"
+d168="᷅"
+d169="᷆"
+d170="᷇"
+d171="᷈"
+d172="᷉"
+d173="᷋"
+d174="᷌"
+d175="᷑"
+d176="᷒"
+d177="ᷓ"
+d178="ᷔ"
+d179="ᷕ"
+d180="ᷖ"
+d181="ᷗ"
+d182="ᷘ"
+d183="ᷙ"
+d184="ᷚ"
+d185="ᷛ"
+d186="ᷜ"
+d187="ᷝ"
+d188="ᷞ"
+d189="ᷟ"
+d190="ᷠ"
+d191="ᷡ"
+d192="ᷢ"
+d193="ᷣ"
+d194="ᷤ"
+d195="ᷥ"
+d196="ᷦ"
+d197="᷾"
+d198="⃐"
+d199="⃑"
+d200="⃔"
+d201="⃕"
+d202="⃖"
+d203="⃗"
+d204="⃛"
+d205="⃜"
+d206="⃡"
+d207="⃧"
+d208="⃩"
+d209="⃰"
+d210="⳯"
+d211="⳰"
+d212="⳱"
+d213="ⷠ"
+d214="ⷡ"
+d215="ⷢ"
+d216="ⷣ"
+d217="ⷤ"
+d218="ⷥ"
+d219="ⷦ"
+d220="ⷧ"
+d221="ⷨ"
+d222="ⷩ"
+d223="ⷪ"
+d224="ⷫ"
+d225="ⷬ"
+d226="ⷭ"
+d227="ⷮ"
+d228="ⷯ"
+d229="ⷰ"
+d230="ⷱ"
+d231="ⷲ"
+d232="ⷳ"
+d233="ⷴ"
+d234="ⷵ"
+d235="ⷶ"
+d236="ⷷ"
+d237="ⷸ"
+d238="ⷹ"
+d239="ⷺ"
+d240="ⷻ"
+d241="ⷼ"
+d242="ⷽ"
+d243="ⷾ"
+d244="ⷿ"
+d245="꙯"
+d246="꙼"
+d247="꙽"
+d248="꛰"
+d249="꛱"
+d250="꣠"
+d251="꣡"
+d252="꣢"
+d253="꣣"
+d254="꣤"
+d255="꣥"
+d256="꣦"
+d257="꣧"
+d258="꣨"
+d259="꣩"
+d260="꣪"
+d261="꣫"
+d262="꣬"
+d263="꣭"
+d264="꣮"
+d265="꣯"
+d266="꣰"
+d267="꣱"
+d268="ꪰ"
+d269="ꪲ"
+d270="ꪳ"
+d271="ꪷ"
+d272="ꪸ"
+d273="ꪾ"
+d274="꪿"
+d275="꫁"
+d276="︠"
+d277="︡"
+d278="︢"
+d279="︣"
+d280="︤"
+d281="︥"
+d282="︦"
+d283="𐨏"
+d284="𐨸"
+d285="𝆅"
+d286="𝆆"
+d287="𝆇"
+d288="𝆈"
+d289="𝆉"
+d290="𝆪"
+d291="𝆫"
+d292="𝆬"
+d293="𝆭"
+d294="𝉂"
+d295="𝉃"
+d296="𝉄"
+
+num_diacritics="297"
+
+placeholder="􎻮"
+
+#####################################################################
+# Upload the image and print the placeholder
+#####################################################################
+
+upload_image_and_print_placeholder
diff --git a/files/config/suckless/st/khash.h b/files/config/suckless/st/khash.h
new file mode 100644
index 0000000..f75f347
--- /dev/null
+++ b/files/config/suckless/st/khash.h
@@ -0,0 +1,627 @@
+/* The MIT License
+
+ Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+/*
+ An example:
+
+#include "khash.h"
+KHASH_MAP_INIT_INT(32, char)
+int main() {
+ int ret, is_missing;
+ khiter_t k;
+ khash_t(32) *h = kh_init(32);
+ k = kh_put(32, h, 5, &ret);
+ kh_value(h, k) = 10;
+ k = kh_get(32, h, 10);
+ is_missing = (k == kh_end(h));
+ k = kh_get(32, h, 5);
+ kh_del(32, h, k);
+ for (k = kh_begin(h); k != kh_end(h); ++k)
+ if (kh_exist(h, k)) kh_value(h, k) = 1;
+ kh_destroy(32, h);
+ return 0;
+}
+*/
+
+/*
+ 2013-05-02 (0.2.8):
+
+ * Use quadratic probing. When the capacity is power of 2, stepping function
+ i*(i+1)/2 guarantees to traverse each bucket. It is better than double
+ hashing on cache performance and is more robust than linear probing.
+
+ In theory, double hashing should be more robust than quadratic probing.
+ However, my implementation is probably not for large hash tables, because
+ the second hash function is closely tied to the first hash function,
+ which reduce the effectiveness of double hashing.
+
+ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
+
+ 2011-12-29 (0.2.7):
+
+ * Minor code clean up; no actual effect.
+
+ 2011-09-16 (0.2.6):
+
+ * The capacity is a power of 2. This seems to dramatically improve the
+ speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
+
+ - http://code.google.com/p/ulib/
+ - http://nothings.org/computer/judy/
+
+ * Allow to optionally use linear probing which usually has better
+ performance for random input. Double hashing is still the default as it
+ is more robust to certain non-random input.
+
+ * Added Wang's integer hash function (not used by default). This hash
+ function is more robust to certain non-random input.
+
+ 2011-02-14 (0.2.5):
+
+ * Allow to declare global functions.
+
+ 2009-09-26 (0.2.4):
+
+ * Improve portability
+
+ 2008-09-19 (0.2.3):
+
+ * Corrected the example
+ * Improved interfaces
+
+ 2008-09-11 (0.2.2):
+
+ * Improved speed a little in kh_put()
+
+ 2008-09-10 (0.2.1):
+
+ * Added kh_clear()
+ * Fixed a compiling error
+
+ 2008-09-02 (0.2.0):
+
+ * Changed to token concatenation which increases flexibility.
+
+ 2008-08-31 (0.1.2):
+
+ * Fixed a bug in kh_get(), which has not been tested previously.
+
+ 2008-08-31 (0.1.1):
+
+ * Added destructor
+*/
+
+
+#ifndef __AC_KHASH_H
+#define __AC_KHASH_H
+
+/*!
+ @header
+
+ Generic hash table library.
+ */
+
+#define AC_VERSION_KHASH_H "0.2.8"
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+/* compiler specific configuration */
+
+#if UINT_MAX == 0xffffffffu
+typedef unsigned int khint32_t;
+#elif ULONG_MAX == 0xffffffffu
+typedef unsigned long khint32_t;
+#endif
+
+#if ULONG_MAX == ULLONG_MAX
+typedef unsigned long khint64_t;
+#else
+typedef unsigned long long khint64_t;
+#endif
+
+#ifndef kh_inline
+#ifdef _MSC_VER
+#define kh_inline __inline
+#else
+#define kh_inline inline
+#endif
+#endif /* kh_inline */
+
+#ifndef klib_unused
+#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
+#define klib_unused __attribute__ ((__unused__))
+#else
+#define klib_unused
+#endif
+#endif /* klib_unused */
+
+typedef khint32_t khint_t;
+typedef khint_t khiter_t;
+
+#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
+#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
+#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
+#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
+#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
+#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
+#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
+
+#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
+
+#ifndef kroundup32
+#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
+#endif
+
+#ifndef kcalloc
+#define kcalloc(N,Z) calloc(N,Z)
+#endif
+#ifndef kmalloc
+#define kmalloc(Z) malloc(Z)
+#endif
+#ifndef krealloc
+#define krealloc(P,Z) realloc(P,Z)
+#endif
+#ifndef kfree
+#define kfree(P) free(P)
+#endif
+
+static const double __ac_HASH_UPPER = 0.77;
+
+#define __KHASH_TYPE(name, khkey_t, khval_t) \
+ typedef struct kh_##name##_s { \
+ khint_t n_buckets, size, n_occupied, upper_bound; \
+ khint32_t *flags; \
+ khkey_t *keys; \
+ khval_t *vals; \
+ } kh_##name##_t;
+
+#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \
+ extern kh_##name##_t *kh_init_##name(void); \
+ extern void kh_destroy_##name(kh_##name##_t *h); \
+ extern void kh_clear_##name(kh_##name##_t *h); \
+ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \
+ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
+ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
+ extern void kh_del_##name(kh_##name##_t *h, khint_t x);
+
+#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ SCOPE kh_##name##_t *kh_init_##name(void) { \
+ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \
+ } \
+ SCOPE void kh_destroy_##name(kh_##name##_t *h) \
+ { \
+ if (h) { \
+ kfree((void *)h->keys); kfree(h->flags); \
+ kfree((void *)h->vals); \
+ kfree(h); \
+ } \
+ } \
+ SCOPE void kh_clear_##name(kh_##name##_t *h) \
+ { \
+ if (h && h->flags) { \
+ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
+ h->size = h->n_occupied = 0; \
+ } \
+ } \
+ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \
+ { \
+ if (h->n_buckets) { \
+ khint_t k, i, last, mask, step = 0; \
+ mask = h->n_buckets - 1; \
+ k = __hash_func(key); i = k & mask; \
+ last = i; \
+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+ i = (i + (++step)) & mask; \
+ if (i == last) return h->n_buckets; \
+ } \
+ return __ac_iseither(h->flags, i)? h->n_buckets : i; \
+ } else return 0; \
+ } \
+ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
+ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
+ khint32_t *new_flags = 0; \
+ khint_t j = 1; \
+ { \
+ kroundup32(new_n_buckets); \
+ if (new_n_buckets < 4) new_n_buckets = 4; \
+ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \
+ else { /* hash table size to be changed (shrink or expand); rehash */ \
+ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
+ if (!new_flags) return -1; \
+ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
+ if (h->n_buckets < new_n_buckets) { /* expand */ \
+ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+ if (!new_keys) { kfree(new_flags); return -1; } \
+ h->keys = new_keys; \
+ if (kh_is_map) { \
+ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+ if (!new_vals) { kfree(new_flags); return -1; } \
+ h->vals = new_vals; \
+ } \
+ } /* otherwise shrink */ \
+ } \
+ } \
+ if (j) { /* rehashing is needed */ \
+ for (j = 0; j != h->n_buckets; ++j) { \
+ if (__ac_iseither(h->flags, j) == 0) { \
+ khkey_t key = h->keys[j]; \
+ khval_t val; \
+ khint_t new_mask; \
+ new_mask = new_n_buckets - 1; \
+ if (kh_is_map) val = h->vals[j]; \
+ __ac_set_isdel_true(h->flags, j); \
+ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
+ khint_t k, i, step = 0; \
+ k = __hash_func(key); \
+ i = k & new_mask; \
+ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
+ __ac_set_isempty_false(new_flags, i); \
+ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
+ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
+ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
+ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
+ } else { /* write the element and jump out of the loop */ \
+ h->keys[i] = key; \
+ if (kh_is_map) h->vals[i] = val; \
+ break; \
+ } \
+ } \
+ } \
+ } \
+ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
+ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+ } \
+ kfree(h->flags); /* free the working space */ \
+ h->flags = new_flags; \
+ h->n_buckets = new_n_buckets; \
+ h->n_occupied = h->size; \
+ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
+ } \
+ return 0; \
+ } \
+ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
+ { \
+ khint_t x; \
+ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
+ if (h->n_buckets > (h->size<<1)) { \
+ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
+ *ret = -1; return h->n_buckets; \
+ } \
+ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
+ *ret = -1; return h->n_buckets; \
+ } \
+ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
+ { \
+ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
+ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
+ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \
+ else { \
+ last = i; \
+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+ if (__ac_isdel(h->flags, i)) site = i; \
+ i = (i + (++step)) & mask; \
+ if (i == last) { x = site; break; } \
+ } \
+ if (x == h->n_buckets) { \
+ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
+ else x = i; \
+ } \
+ } \
+ } \
+ if (__ac_isempty(h->flags, x)) { /* not present at all */ \
+ h->keys[x] = key; \
+ __ac_set_isboth_false(h->flags, x); \
+ ++h->size; ++h->n_occupied; \
+ *ret = 1; \
+ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \
+ h->keys[x] = key; \
+ __ac_set_isboth_false(h->flags, x); \
+ ++h->size; \
+ *ret = 2; \
+ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
+ return x; \
+ } \
+ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \
+ { \
+ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
+ __ac_set_isdel_true(h->flags, x); \
+ --h->size; \
+ } \
+ }
+
+#define KHASH_DECLARE(name, khkey_t, khval_t) \
+ __KHASH_TYPE(name, khkey_t, khval_t) \
+ __KHASH_PROTOTYPES(name, khkey_t, khval_t)
+
+#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ __KHASH_TYPE(name, khkey_t, khval_t) \
+ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+/* --- BEGIN OF HASH FUNCTIONS --- */
+
+/*! @function
+ @abstract Integer hash function
+ @param key The integer [khint32_t]
+ @return The hash value [khint_t]
+ */
+#define kh_int_hash_func(key) (khint32_t)(key)
+/*! @function
+ @abstract Integer comparison function
+ */
+#define kh_int_hash_equal(a, b) ((a) == (b))
+/*! @function
+ @abstract 64-bit integer hash function
+ @param key The integer [khint64_t]
+ @return The hash value [khint_t]
+ */
+#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
+/*! @function
+ @abstract 64-bit integer comparison function
+ */
+#define kh_int64_hash_equal(a, b) ((a) == (b))
+/*! @function
+ @abstract const char* hash function
+ @param s Pointer to a null terminated string
+ @return The hash value
+ */
+static kh_inline khint_t __ac_X31_hash_string(const char *s)
+{
+ khint_t h = (khint_t)*s;
+ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
+ return h;
+}
+/*! @function
+ @abstract Another interface to const char* hash function
+ @param key Pointer to a null terminated string [const char*]
+ @return The hash value [khint_t]
+ */
+#define kh_str_hash_func(key) __ac_X31_hash_string(key)
+/*! @function
+ @abstract Const char* comparison function
+ */
+#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
+
+static kh_inline khint_t __ac_Wang_hash(khint_t key)
+{
+ key += ~(key << 15);
+ key ^= (key >> 10);
+ key += (key << 3);
+ key ^= (key >> 6);
+ key += ~(key << 11);
+ key ^= (key >> 16);
+ return key;
+}
+#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
+
+/* --- END OF HASH FUNCTIONS --- */
+
+/* Other convenient macros... */
+
+/*!
+ @abstract Type of the hash table.
+ @param name Name of the hash table [symbol]
+ */
+#define khash_t(name) kh_##name##_t
+
+/*! @function
+ @abstract Initiate a hash table.
+ @param name Name of the hash table [symbol]
+ @return Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_init(name) kh_init_##name()
+
+/*! @function
+ @abstract Destroy a hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_destroy(name, h) kh_destroy_##name(h)
+
+/*! @function
+ @abstract Reset a hash table without deallocating memory.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_clear(name, h) kh_clear_##name(h)
+
+/*! @function
+ @abstract Resize a hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param s New size [khint_t]
+ */
+#define kh_resize(name, h, s) kh_resize_##name(h, s)
+
+/*! @function
+ @abstract Insert a key to the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Key [type of keys]
+ @param r Extra return code: -1 if the operation failed;
+ 0 if the key is present in the hash table;
+ 1 if the bucket is empty (never used); 2 if the element in
+ the bucket has been deleted [int*]
+ @return Iterator to the inserted element [khint_t]
+ */
+#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
+
+/*! @function
+ @abstract Retrieve a key from the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Key [type of keys]
+ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
+ */
+#define kh_get(name, h, k) kh_get_##name(h, k)
+
+/*! @function
+ @abstract Remove a key from the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Iterator to the element to be deleted [khint_t]
+ */
+#define kh_del(name, h, k) kh_del_##name(h, k)
+
+/*! @function
+ @abstract Test whether a bucket contains data.
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return 1 if containing data; 0 otherwise [int]
+ */
+#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
+
+/*! @function
+ @abstract Get key given an iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return Key [type of keys]
+ */
+#define kh_key(h, x) ((h)->keys[x])
+
+/*! @function
+ @abstract Get value given an iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return Value [type of values]
+ @discussion For hash sets, calling this results in segfault.
+ */
+#define kh_val(h, x) ((h)->vals[x])
+
+/*! @function
+ @abstract Alias of kh_val()
+ */
+#define kh_value(h, x) ((h)->vals[x])
+
+/*! @function
+ @abstract Get the start iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return The start iterator [khint_t]
+ */
+#define kh_begin(h) (khint_t)(0)
+
+/*! @function
+ @abstract Get the end iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return The end iterator [khint_t]
+ */
+#define kh_end(h) ((h)->n_buckets)
+
+/*! @function
+ @abstract Get the number of elements in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return Number of elements in the hash table [khint_t]
+ */
+#define kh_size(h) ((h)->size)
+
+/*! @function
+ @abstract Get the number of buckets in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return Number of buckets in the hash table [khint_t]
+ */
+#define kh_n_buckets(h) ((h)->n_buckets)
+
+/*! @function
+ @abstract Iterate over the entries in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param kvar Variable to which key will be assigned
+ @param vvar Variable to which value will be assigned
+ @param code Block of code to execute
+ */
+#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \
+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
+ if (!kh_exist(h,__i)) continue; \
+ (kvar) = kh_key(h,__i); \
+ (vvar) = kh_val(h,__i); \
+ code; \
+ } }
+
+/*! @function
+ @abstract Iterate over the values in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param vvar Variable to which value will be assigned
+ @param code Block of code to execute
+ */
+#define kh_foreach_value(h, vvar, code) { khint_t __i; \
+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
+ if (!kh_exist(h,__i)) continue; \
+ (vvar) = kh_val(h,__i); \
+ code; \
+ } }
+
+/* More convenient interfaces */
+
+/*! @function
+ @abstract Instantiate a hash set containing integer keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT(name) \
+ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing integer keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT(name, khval_t) \
+ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash set containing 64-bit integer keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT64(name) \
+ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing 64-bit integer keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT64(name, khval_t) \
+ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
+
+typedef const char *kh_cstr_t;
+/*! @function
+ @abstract Instantiate a hash map containing const char* keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_STR(name) \
+ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing const char* keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_STR(name, khval_t) \
+ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
+
+#endif /* __AC_KHASH_H */
diff --git a/files/config/suckless/st/kvec.h b/files/config/suckless/st/kvec.h
new file mode 100644
index 0000000..10f1c5b
--- /dev/null
+++ b/files/config/suckless/st/kvec.h
@@ -0,0 +1,90 @@
+/* The MIT License
+
+ Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk>
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+/*
+ An example:
+
+#include "kvec.h"
+int main() {
+ kvec_t(int) array;
+ kv_init(array);
+ kv_push(int, array, 10); // append
+ kv_a(int, array, 20) = 5; // dynamic
+ kv_A(array, 20) = 4; // static
+ kv_destroy(array);
+ return 0;
+}
+*/
+
+/*
+ 2008-09-22 (0.1.0):
+
+ * The initial version.
+
+*/
+
+#ifndef AC_KVEC_H
+#define AC_KVEC_H
+
+#include <stdlib.h>
+
+#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
+
+#define kvec_t(type) struct { size_t n, m; type *a; }
+#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0)
+#define kv_destroy(v) free((v).a)
+#define kv_A(v, i) ((v).a[(i)])
+#define kv_pop(v) ((v).a[--(v).n])
+#define kv_size(v) ((v).n)
+#define kv_max(v) ((v).m)
+
+#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m))
+
+#define kv_copy(type, v1, v0) do { \
+ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \
+ (v1).n = (v0).n; \
+ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \
+ } while (0) \
+
+#define kv_push(type, v, x) do { \
+ if ((v).n == (v).m) { \
+ (v).m = (v).m? (v).m<<1 : 2; \
+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \
+ } \
+ (v).a[(v).n++] = (x); \
+ } while (0)
+
+#define kv_pushp(type, v) ((((v).n == (v).m)? \
+ ((v).m = ((v).m? (v).m<<1 : 2), \
+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
+ : 0), ((v).a + ((v).n++)))
+
+#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \
+ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \
+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
+ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \
+ : 0), (v).a[(i)])
+
+#endif
diff --git a/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff b/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
new file mode 100644
index 0000000..6049fe2
--- /dev/null
+++ b/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
@@ -0,0 +1,7324 @@
+From 25d9cca81ce48141de7f6a823b006dddaafd9de8 Mon Sep 17 00:00:00 2001
+From: Sergei Grechanik <sergei.grechanik@gmail.com>
+Date: Sun, 22 Sep 2024 08:36:05 -0700
+Subject: [PATCH] Kitty graphics protocol support 7b717e3 2024-09-22
+
+This patch implements the kitty graphics protocol in st.
+See https://github.com/sergei-grechanik/st-graphics
+Created by squashing the graphics branch, the most recent
+commit is 7b717e38b1739e11356b34df9fdfdfa339960864 (2024-09-22).
+
+Squashed on top of a0274bc20e11d8672bb2953fdd1d3010c0e708c5
+
+Note that the following files were excluded from the squash:
+ .clang-format
+ README.md
+ generate-rowcolumn-helpers.py
+ rowcolumn-diacritics.txt
+ rowcolumn_diacritics.sh
+---
+ Makefile | 4 +-
+ config.def.h | 46 +-
+ config.mk | 5 +-
+ graphics.c | 3812 ++++++++++++++++++++++++++++++++
+ graphics.h | 107 +
+ icat-mini.sh | 801 +++++++
+ khash.h | 627 ++++++
+ kvec.h | 90 +
+ rowcolumn_diacritics_helpers.c | 391 ++++
+ st.c | 279 ++-
+ st.h | 84 +-
+ st.info | 6 +
+ win.h | 3 +
+ x.c | 411 +++-
+ 14 files changed, 6615 insertions(+), 51 deletions(-)
+ create mode 100644 graphics.c
+ create mode 100644 graphics.h
+ create mode 100755 icat-mini.sh
+ create mode 100644 khash.h
+ create mode 100644 kvec.h
+ create mode 100644 rowcolumn_diacritics_helpers.c
+
+diff --git a/Makefile b/Makefile
+index 15db421..e79b89e 100644
+--- a/Makefile
++++ b/Makefile
+@@ -4,7 +4,7 @@
+
+ include config.mk
+
+-SRC = st.c x.c
++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c
+ OBJ = $(SRC:.c=.o)
+
+ all: st
+@@ -16,7 +16,7 @@ config.h:
+ $(CC) $(STCFLAGS) -c $<
+
+ st.o: config.h st.h win.h
+-x.o: arg.h config.h st.h win.h
++x.o: arg.h config.h st.h win.h graphics.h
+
+ $(OBJ): config.h config.mk
+
+diff --git a/config.def.h b/config.def.h
+index 2cd740a..4aadbbc 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -8,6 +8,13 @@
+ static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
+ static int borderpx = 2;
+
++/* How to align the content in the window when the size of the terminal
++ * doesn't perfectly match the size of the window. The values are percentages.
++ * 50 means center, 0 means flush left/top, 100 means flush right/bottom.
++ */
++static int anysize_halign = 50;
++static int anysize_valign = 50;
++
+ /*
+ * What program is execed by st depends of these precedence rules:
+ * 1: program passed with -e
+@@ -23,7 +30,8 @@ char *scroll = NULL;
+ char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
+
+ /* identification sequence returned in DA and DECID */
+-char *vtiden = "\033[?6c";
++/* By default, use the same one as kitty. */
++char *vtiden = "\033[?62c";
+
+ /* Kerning / character bounding-box multipliers */
+ static float cwscale = 1.0;
+@@ -163,6 +171,28 @@ static unsigned int mousebg = 0;
+ */
+ static unsigned int defaultattr = 11;
+
++/*
++ * Graphics configuration
++ */
++
++/// The template for the cache directory.
++const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX";
++/// The max size of a single image file, in bytes.
++unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024;
++/// The max size of the cache, in bytes.
++unsigned graphics_total_file_cache_size = 300 * 1024 * 1024;
++/// The max ram size of an image or placement, in bytes.
++unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024;
++/// The max total size of all images loaded into RAM.
++unsigned graphics_max_total_ram_size = 300 * 1024 * 1024;
++/// The max total number of image placements and images.
++unsigned graphics_max_total_placements = 4096;
++/// The ratio by which limits can be exceeded. This is to reduce the frequency
++/// of image removal.
++double graphics_excess_tolerance_ratio = 0.05;
++/// The minimum delay between redraws caused by animations, in milliseconds.
++unsigned graphics_animation_min_delay = 20;
++
+ /*
+ * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
+ * Note that if you want to use ShiftMask with selmasks, set this to an other
+@@ -170,12 +200,18 @@ static unsigned int defaultattr = 11;
+ */
+ static uint forcemousemod = ShiftMask;
+
++/* Internal keyboard shortcuts. */
++#define MODKEY Mod1Mask
++#define TERMMOD (ControlMask|ShiftMask)
++
+ /*
+ * Internal mouse shortcuts.
+ * Beware that overloading Button1 will disable the selection.
+ */
+ static MouseShortcut mshortcuts[] = {
+ /* mask button function argument release */
++ { TERMMOD, Button3, previewimage, {.s = "feh"} },
++ { TERMMOD, Button2, showimageinfo, {}, 1 },
+ { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
+ { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
+ { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
+@@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = {
+ { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
+ };
+
+-/* Internal keyboard shortcuts. */
+-#define MODKEY Mod1Mask
+-#define TERMMOD (ControlMask|ShiftMask)
+-
+ static Shortcut shortcuts[] = {
+ /* mask keysym function argument */
+ { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
+@@ -201,6 +233,10 @@ static Shortcut shortcuts[] = {
+ { TERMMOD, XK_Y, selpaste, {.i = 0} },
+ { ShiftMask, XK_Insert, selpaste, {.i = 0} },
+ { TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
++ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} },
++ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} },
++ { TERMMOD, XK_F7, unloadimages, {.i = 0} },
++ { TERMMOD, XK_F8, toggleimages, {.i = 0} },
+ };
+
+ /*
+diff --git a/config.mk b/config.mk
+index fdc29a7..cb2875c 100644
+--- a/config.mk
++++ b/config.mk
+@@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config
+
+ # includes and libs
+ INCS = -I$(X11INC) \
++ `$(PKG_CONFIG) --cflags imlib2` \
+ `$(PKG_CONFIG) --cflags fontconfig` \
+ `$(PKG_CONFIG) --cflags freetype2`
+-LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
++LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \
++ `$(PKG_CONFIG) --libs imlib2` \
++ `$(PKG_CONFIG) --libs zlib` \
+ `$(PKG_CONFIG) --libs fontconfig` \
+ `$(PKG_CONFIG) --libs freetype2`
+
+diff --git a/graphics.c b/graphics.c
+new file mode 100644
+index 0000000..64e6fe0
+--- /dev/null
++++ b/graphics.c
+@@ -0,0 +1,3812 @@
++/* The MIT License
++
++ Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com>
++
++ Permission is hereby granted, free of charge, to any person obtaining
++ a copy of this software and associated documentation files (the
++ "Software"), to deal in the Software without restriction, including
++ without limitation the rights to use, copy, modify, merge, publish,
++ distribute, sublicense, and/or sell copies of the Software, and to
++ permit persons to whom the Software is furnished to do so, subject to
++ the following conditions:
++
++ The above copyright notice and this permission notice shall be
++ included in all copies or substantial portions of the Software.
++
++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++ SOFTWARE.
++*/
++
++////////////////////////////////////////////////////////////////////////////////
++//
++// This file implements a subset of the kitty graphics protocol.
++//
++////////////////////////////////////////////////////////////////////////////////
++
++#define _POSIX_C_SOURCE 200809L
++
++#include <zlib.h>
++#include <Imlib2.h>
++#include <X11/Xlib.h>
++#include <X11/extensions/Xrender.h>
++#include <assert.h>
++#include <ctype.h>
++#include <spawn.h>
++#include <stdarg.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <string.h>
++#include <sys/stat.h>
++#include <time.h>
++#include <unistd.h>
++#include <errno.h>
++
++#include "graphics.h"
++#include "khash.h"
++#include "kvec.h"
++
++extern char **environ;
++
++#define MAX_FILENAME_SIZE 256
++#define MAX_INFO_LEN 256
++#define MAX_IMAGE_RECTS 20
++
++/// The type used in this file to represent time. Used both for time differences
++/// and absolute times (as milliseconds since an arbitrary point in time, see
++/// `initialization_time`).
++typedef int64_t Milliseconds;
++
++enum ScaleMode {
++ SCALE_MODE_UNSET = 0,
++ /// Stretch or shrink the image to fill the box, ignoring aspect ratio.
++ SCALE_MODE_FILL = 1,
++ /// Preserve aspect ratio and fit to width or to height so that the
++ /// whole image is visible.
++ SCALE_MODE_CONTAIN = 2,
++ /// Do not scale. The image may be cropped if the box is too small.
++ SCALE_MODE_NONE = 3,
++ /// Do not scale, unless the box is too small, in which case the image
++ /// will be shrunk like with `SCALE_MODE_CONTAIN`.
++ SCALE_MODE_NONE_OR_CONTAIN = 4,
++};
++
++enum AnimationState {
++ ANIMATION_STATE_UNSET = 0,
++ /// The animation is stopped. Display the current frame, but don't
++ /// advance to the next one.
++ ANIMATION_STATE_STOPPED = 1,
++ /// Run the animation to then end, then wait for the next frame.
++ ANIMATION_STATE_LOADING = 2,
++ /// Run the animation in a loop.
++ ANIMATION_STATE_LOOPING = 3,
++};
++
++/// The status of an image. Each image uploaded to the terminal is cached on
++/// disk, then it is loaded to ram when needed.
++enum ImageStatus {
++ STATUS_UNINITIALIZED = 0,
++ STATUS_UPLOADING = 1,
++ STATUS_UPLOADING_ERROR = 2,
++ STATUS_UPLOADING_SUCCESS = 3,
++ STATUS_RAM_LOADING_ERROR = 4,
++ STATUS_RAM_LOADING_IN_PROGRESS = 5,
++ STATUS_RAM_LOADING_SUCCESS = 6,
++};
++
++const char *image_status_strings[6] = {
++ "STATUS_UNINITIALIZED",
++ "STATUS_UPLOADING",
++ "STATUS_UPLOADING_ERROR",
++ "STATUS_UPLOADING_SUCCESS",
++ "STATUS_RAM_LOADING_ERROR",
++ "STATUS_RAM_LOADING_SUCCESS",
++};
++
++enum ImageUploadingFailure {
++ ERROR_OVER_SIZE_LIMIT = 1,
++ ERROR_CANNOT_OPEN_CACHED_FILE = 2,
++ ERROR_UNEXPECTED_SIZE = 3,
++ ERROR_CANNOT_COPY_FILE = 4,
++};
++
++const char *image_uploading_failure_strings[5] = {
++ "NO_ERROR",
++ "ERROR_OVER_SIZE_LIMIT",
++ "ERROR_CANNOT_OPEN_CACHED_FILE",
++ "ERROR_UNEXPECTED_SIZE",
++ "ERROR_CANNOT_COPY_FILE",
++};
++
++////////////////////////////////////////////////////////////////////////////////
++//
++// We use the following structures to represent images and placements:
++//
++// - Image: this is the main structure representing an image, usually created
++// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id,
++// specified by 'i='). An image may have multiple frames (ImageFrame) and
++// placements (ImagePlacement).
++//
++// - ImageFrame: represents a single frame of an image, usually created by
++// the action 'a=f' (and the first frame is created with the image itself).
++// Each frame has an index and also:
++// - a file containing the frame data (considered to be "on disk", although
++// it's probably in tmpfs),
++// - an imlib object containing the fully composed frame (i.e. the frame
++// data from the file composed onto the background frame or color). It is
++// not ready for display yet, because it needs to be scaled and uploaded
++// to the X server.
++//
++// - ImagePlacement: represents a placement of an image, created by 'a=p' and
++// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also
++// each placement has an array of pixmaps: one for each frame of the image.
++// Each pixmap is a scaled and uploaded image ready to be displayed.
++//
++// Images are store in the `images` hash table, mapping image ids to Image
++// objects (allocated on the heap).
++//
++// Placements are stored in the `placements` hash table of each Image object,
++// mapping placement ids to ImagePlacement objects (also allocated on the heap).
++//
++// ImageFrames are stored in the `first_frame` field and in the
++// `frames_beyond_the_first` array of each Image object. They are stored by
++// value, so ImageFrame pointer may be invalidated when frames are
++// added/deleted, be careful.
++//
++////////////////////////////////////////////////////////////////////////////////
++
++struct Image;
++struct ImageFrame;
++struct ImagePlacement;
++
++KHASH_MAP_INIT_INT(id2image, struct Image *)
++KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *)
++
++typedef struct ImageFrame {
++ /// The image this frame belongs to.
++ struct Image *image;
++ /// The 1-based index of the frame. Zero if the frame isn't initialized.
++ int index;
++ /// The last time when the frame was displayed or otherwise touched.
++ Milliseconds atime;
++ /// The background color of the frame in the 0xRRGGBBAA format.
++ uint32_t background_color;
++ /// The index of the background frame. Zero to use the color instead.
++ int background_frame_index;
++ /// The duration of the frame in milliseconds.
++ int gap;
++ /// The expected size of the frame image file (specified with 'S='),
++ /// used to check if uploading succeeded.
++ unsigned expected_size;
++ /// Format specification (see the `f=` key).
++ int format;
++ /// Pixel width and height of the non-composed (on-disk) frame data. May
++ /// differ from the image (i.e. first frame) dimensions.
++ int data_pix_width, data_pix_height;
++ /// The offset of the frame relative to the first frame.
++ int x, y;
++ /// Compression mode (see the `o=` key).
++ char compression;
++ /// The status (see `ImageStatus`).
++ char status;
++ /// The reason of uploading failure (see `ImageUploadingFailure`).
++ char uploading_failure;
++ /// Whether failures and successes should be reported ('q=').
++ char quiet;
++ /// Whether to blend the frame with the background or replace it.
++ char blend;
++ /// The file corresponding to the on-disk cache, used when uploading.
++ FILE *open_file;
++ /// The size of the corresponding file cached on disk.
++ unsigned disk_size;
++ /// The imlib object containing the fully composed frame. It's not
++ /// scaled for screen display yet.
++ Imlib_Image imlib_object;
++} ImageFrame;
++
++typedef struct Image {
++ /// The client id (the one specified with 'i='). Must be nonzero.
++ uint32_t image_id;
++ /// The client id specified in the query command (`a=q`). This one must
++ /// be used to create the response if it's non-zero.
++ uint32_t query_id;
++ /// The number specified in the transmission command (`I=`). If
++ /// non-zero, it may be used to identify the image instead of the
++ /// image_id, and it also should be mentioned in responses.
++ uint32_t image_number;
++ /// The last time when the image was displayed or otherwise touched.
++ Milliseconds atime;
++ /// The total duration of the animation in milliseconds.
++ int total_duration;
++ /// The total size of cached image files for all frames.
++ int total_disk_size;
++ /// The global index of the creation command. Used to decide which image
++ /// is newer if they have the same image number.
++ uint64_t global_command_index;
++ /// The 1-based index of the currently displayed frame.
++ int current_frame;
++ /// The state of the animation, see `AnimationState`.
++ char animation_state;
++ /// The absolute time that is assumed to be the start of the current
++ /// frame (in ms since initialization).
++ Milliseconds current_frame_time;
++ /// The absolute time of the last redraw (in ms since initialization).
++ /// Used to check whether it's the first time we draw the image in the
++ /// current redraw cycle.
++ Milliseconds last_redraw;
++ /// The absolute time of the next redraw (in ms since initialization).
++ /// 0 means no redraw is scheduled.
++ Milliseconds next_redraw;
++ /// The unscaled pixel width and height of the image. Usually inherited
++ /// from the first frame.
++ int pix_width, pix_height;
++ /// The first frame.
++ ImageFrame first_frame;
++ /// The array of frames beyond the first one.
++ kvec_t(ImageFrame) frames_beyond_the_first;
++ /// Image placements.
++ khash_t(id2placement) *placements;
++ /// The default placement.
++ uint32_t default_placement;
++ /// The initial placement id, specified with the transmission command,
++ /// used to report success or failure.
++ uint32_t initial_placement_id;
++} Image;
++
++typedef struct ImagePlacement {
++ /// The image this placement belongs to.
++ Image *image;
++ /// The id of the placement. Must be nonzero.
++ uint32_t placement_id;
++ /// The last time when the placement was displayed or otherwise touched.
++ Milliseconds atime;
++ /// The 1-based index of the protected pixmap. We protect a pixmap in
++ /// gr_load_pixmap to avoid unloading it right after it was loaded.
++ int protected_frame;
++ /// Whether the placement is used only for Unicode placeholders.
++ char virtual;
++ /// The scaling mode (see `ScaleMode`).
++ char scale_mode;
++ /// Height and width in cells.
++ uint16_t rows, cols;
++ /// Top-left corner of the source rectangle ('x=' and 'y=').
++ int src_pix_x, src_pix_y;
++ /// Height and width of the source rectangle (zero if full image).
++ int src_pix_width, src_pix_height;
++ /// The image appropriately scaled and uploaded to the X server. This
++ /// pixmap is premultiplied by alpha.
++ Pixmap first_pixmap;
++ /// The array of pixmaps beyond the first one.
++ kvec_t(Pixmap) pixmaps_beyond_the_first;
++ /// The dimensions of the cell used to scale the image. If cell
++ /// dimensions are changed (font change), the image will be rescaled.
++ uint16_t scaled_cw, scaled_ch;
++ /// If true, do not move the cursor when displaying this placement
++ /// (non-virtual placements only).
++ char do_not_move_cursor;
++} ImagePlacement;
++
++/// A rectangular piece of an image to be drawn.
++typedef struct {
++ uint32_t image_id;
++ uint32_t placement_id;
++ /// The position of the rectangle in pixels.
++ int screen_x_pix, screen_y_pix;
++ /// The starting row on the screen.
++ int screen_y_row;
++ /// The part of the whole image to be drawn, in cells. Starts are
++ /// zero-based, ends are exclusive.
++ int img_start_col, img_end_col, img_start_row, img_end_row;
++ /// The current cell width and height in pixels.
++ int cw, ch;
++ /// Whether colors should be inverted.
++ int reverse;
++} ImageRect;
++
++/// Executes `code` for each frame of an image. Example:
++///
++/// foreach_frame(image, frame, {
++/// printf("Frame %d\n", frame->index);
++/// });
++///
++#define foreach_frame(image, framevar, code) { size_t __i; \
++ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \
++ ImageFrame *framevar = \
++ __i == 0 ? &(image).first_frame \
++ : &kv_A((image).frames_beyond_the_first, __i - 1); \
++ code; \
++ } }
++
++/// Executes `code` for each pixmap of a placement. Example:
++///
++/// foreach_pixmap(placement, pixmap, {
++/// ...
++/// });
++///
++#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \
++ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \
++ Pixmap pixmapvar = \
++ __i == 0 ? (placement).first_pixmap \
++ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \
++ code; \
++ } }
++
++
++static Image *gr_find_image(uint32_t image_id);
++static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len);
++static void gr_delete_image(Image *img);
++static void gr_check_limits();
++static char *gr_base64dec(const char *src, size_t *size);
++static void sanitize_str(char *str, size_t max_len);
++static const char *sanitized_filename(const char *str);
++
++/// The array of image rectangles to draw. It is reset each frame.
++static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}};
++/// The known images (including the ones being uploaded).
++static khash_t(id2image) *images = NULL;
++/// The total number of placements in all images.
++static unsigned total_placement_count = 0;
++/// The total size of all image files stored in the on-disk cache.
++static int64_t images_disk_size = 0;
++/// The total size of all images and placements loaded into ram.
++static int64_t images_ram_size = 0;
++/// The id of the last loaded image.
++static uint32_t last_image_id = 0;
++/// Current cell width and heigh in pixels.
++static int current_cw = 0, current_ch = 0;
++/// The id of the currently uploaded image (when using direct uploading).
++static uint32_t current_upload_image_id = 0;
++/// The index of the frame currently being uploaded.
++static int current_upload_frame_index = 0;
++/// The time when the graphics module was initialized.
++static struct timespec initialization_time = {0};
++/// The time when the current frame drawing started, used for debugging fps and
++/// to calculate the current frame for animations.
++static Milliseconds drawing_start_time;
++/// The global index of the current command.
++static uint64_t global_command_counter = 0;
++/// The next redraw times for each row of the terminal. Used for animations.
++/// 0 means no redraw is scheduled.
++static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL};
++/// The number of files loaded in the current redraw cycle.
++static int this_redraw_cycle_loaded_files = 0;
++/// The number of pixmaps loaded in the current redraw cycle.
++static int this_redraw_cycle_loaded_pixmaps = 0;
++
++/// The directory where the cache files are stored.
++static char cache_dir[MAX_FILENAME_SIZE - 16];
++
++/// The table used for color inversion.
++static unsigned char reverse_table[256];
++
++// Declared in the header.
++GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE;
++char graphics_display_images = 1;
++GraphicsCommandResult graphics_command_result = {0};
++int graphics_next_redraw_delay = INT_MAX;
++
++// Defined in config.h
++extern const char graphics_cache_dir_template[];
++extern unsigned graphics_max_single_image_file_size;
++extern unsigned graphics_total_file_cache_size;
++extern unsigned graphics_max_single_image_ram_size;
++extern unsigned graphics_max_total_ram_size;
++extern unsigned graphics_max_total_placements;
++extern double graphics_excess_tolerance_ratio;
++extern unsigned graphics_animation_min_delay;
++
++
++////////////////////////////////////////////////////////////////////////////////
++// Basic helpers.
++////////////////////////////////////////////////////////////////////////////////
++
++#define MIN(a, b) ((a) < (b) ? (a) : (b))
++#define MAX(a, b) ((a) < (b) ? (b) : (a))
++
++/// Returns the difference between `end` and `start` in milliseconds.
++static int64_t gr_timediff_ms(const struct timespec *end,
++ const struct timespec *start) {
++ return (end->tv_sec - start->tv_sec) * 1000 +
++ (end->tv_nsec - start->tv_nsec) / 1000000;
++}
++
++/// Returns the current time in milliseconds since the initialization.
++static Milliseconds gr_now_ms() {
++ struct timespec now;
++ clock_gettime(CLOCK_MONOTONIC, &now);
++ return gr_timediff_ms(&now, &initialization_time);
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Logging.
++////////////////////////////////////////////////////////////////////////////////
++
++#define GR_LOG(...) \
++ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0)
++
++////////////////////////////////////////////////////////////////////////////////
++// Basic image management functions (create, delete, find, etc).
++////////////////////////////////////////////////////////////////////////////////
++
++/// Returns the 1-based index of the last frame. Note that you may want to use
++/// `gr_last_uploaded_frame_index` instead since the last frame may be not
++/// fully uploaded yet.
++static inline int gr_last_frame_index(Image *img) {
++ return kv_size(img->frames_beyond_the_first) + 1;
++}
++
++/// Returns the frame with the given index. Returns NULL if the index is out of
++/// bounds. The index is 1-based.
++static ImageFrame *gr_get_frame(Image *img, int index) {
++ if (!img)
++ return NULL;
++ if (index == 1)
++ return &img->first_frame;
++ if (2 <= index && index <= gr_last_frame_index(img))
++ return &kv_A(img->frames_beyond_the_first, index - 2);
++ return NULL;
++}
++
++/// Returns the last frame of the image. Returns NULL if `img` is NULL.
++static ImageFrame *gr_get_last_frame(Image *img) {
++ if (!img)
++ return NULL;
++ return gr_get_frame(img, gr_last_frame_index(img));
++}
++
++/// Returns the 1-based index of the last frame or the second-to-last frame if
++/// the last frame is not fully uploaded yet.
++static inline int gr_last_uploaded_frame_index(Image *img) {
++ int last_index = gr_last_frame_index(img);
++ if (last_index > 1 &&
++ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS)
++ return last_index - 1;
++ return last_index;
++}
++
++/// Returns the pixmap for the frame with the given index. Returns 0 if the
++/// index is out of bounds. The index is 1-based.
++static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) {
++ if (index == 1)
++ return placement->first_pixmap;
++ if (2 <= index &&
++ index <= kv_size(placement->pixmaps_beyond_the_first) + 1)
++ return kv_A(placement->pixmaps_beyond_the_first, index - 2);
++ return 0;
++}
++
++/// Sets the pixmap for the frame with the given index. The index is 1-based.
++/// The array of pixmaps is resized if needed.
++static void gr_set_frame_pixmap(ImagePlacement *placement, int index,
++ Pixmap pixmap) {
++ if (index == 1) {
++ placement->first_pixmap = pixmap;
++ return;
++ }
++ // Resize the array if needed.
++ size_t old_size = kv_size(placement->pixmaps_beyond_the_first);
++ if (old_size < index - 1) {
++ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2);
++ for (size_t i = old_size; i < index - 1; i++)
++ kv_A(placement->pixmaps_beyond_the_first, i) = 0;
++ }
++ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap;
++}
++
++/// Finds the image corresponding to the client id. Returns NULL if cannot find.
++static Image *gr_find_image(uint32_t image_id) {
++ khiter_t k = kh_get(id2image, images, image_id);
++ if (k == kh_end(images))
++ return NULL;
++ Image *res = kh_value(images, k);
++ return res;
++}
++
++/// Finds the newest image corresponding to the image number. Returns NULL if
++/// cannot find.
++static Image *gr_find_image_by_number(uint32_t image_number) {
++ if (image_number == 0)
++ return NULL;
++ Image *newest_img = NULL;
++ Image *img = NULL;
++ kh_foreach_value(images, img, {
++ if (img->image_number == image_number &&
++ (!newest_img || newest_img->global_command_index <
++ img->global_command_index))
++ newest_img = img;
++ });
++ if (!newest_img)
++ GR_LOG("Image number %u not found\n", image_number);
++ else
++ GR_LOG("Found image number %u, its id is %u\n", image_number,
++ img->image_id);
++ return newest_img;
++}
++
++/// Finds the placement corresponding to the id. If the placement id is 0,
++/// returns some default placement.
++static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) {
++ if (!img)
++ return NULL;
++ if (placement_id == 0) {
++ // Try to get the default placement.
++ ImagePlacement *dflt = NULL;
++ if (img->default_placement != 0)
++ dflt = gr_find_placement(img, img->default_placement);
++ if (dflt)
++ return dflt;
++ // If there is no default placement, return the first one and
++ // set it as the default.
++ kh_foreach_value(img->placements, dflt, {
++ img->default_placement = dflt->placement_id;
++ return dflt;
++ });
++ // If there are no placements, return NULL.
++ return NULL;
++ }
++ khiter_t k = kh_get(id2placement, img->placements, placement_id);
++ if (k == kh_end(img->placements))
++ return NULL;
++ ImagePlacement *res = kh_value(img->placements, k);
++ return res;
++}
++
++/// Finds the placement by image id and placement id.
++static ImagePlacement *gr_find_image_and_placement(uint32_t image_id,
++ uint32_t placement_id) {
++ return gr_find_placement(gr_find_image(image_id), placement_id);
++}
++
++/// Writes the name of the on-disk cache file to `out`. `max_len` should be the
++/// size of `out`. The name will be something like
++/// "/tmp/st-images-xxx/img-ID-FRAME".
++static void gr_get_frame_filename(ImageFrame *frame, char *out,
++ size_t max_len) {
++ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir,
++ frame->image->image_id, frame->index);
++}
++
++/// Returns the (estimation) of the RAM size used by the frame right now.
++static unsigned gr_frame_current_ram_size(ImageFrame *frame) {
++ if (!frame->imlib_object)
++ return 0;
++ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4;
++}
++
++/// Returns the (estimation) of the RAM size used by a single frame pixmap.
++static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) {
++ return (unsigned)placement->rows * placement->cols *
++ placement->scaled_ch * placement->scaled_cw * 4;
++}
++
++/// Returns the (estimation) of the RAM size used by the placemenet right now.
++static unsigned gr_placement_current_ram_size(ImagePlacement *placement) {
++ unsigned single_frame_size =
++ gr_placement_single_frame_ram_size(placement);
++ unsigned result = 0;
++ foreach_pixmap(*placement, pixmap, {
++ if (pixmap)
++ result += single_frame_size;
++ });
++ return result;
++}
++
++/// Unload the frame from RAM (i.e. delete the corresponding imlib object).
++/// If the on-disk file of the frame is preserved, it can be reloaded later.
++static void gr_unload_frame(ImageFrame *frame) {
++ if (!frame->imlib_object)
++ return;
++
++ unsigned frame_ram_size = gr_frame_current_ram_size(frame);
++ images_ram_size -= frame_ram_size;
++
++ imlib_context_set_image(frame->imlib_object);
++ imlib_free_image_and_decache();
++ frame->imlib_object = NULL;
++
++ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) "
++ "ram: %ld KiB (- %u KiB)\n",
++ frame->image->image_id, frame->index,
++ drawing_start_time - frame->atime, images_ram_size / 1024,
++ frame_ram_size / 1024);
++}
++
++/// Unload all frames of the image.
++static void gr_unload_all_frames(Image *img) {
++ foreach_frame(*img, frame, {
++ gr_unload_frame(frame);
++ });
++}
++
++/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps).
++/// If the on-disk files or imlib objects of the corresponding image are
++/// preserved, the placement can be reloaded later.
++static void gr_unload_placement(ImagePlacement *placement) {
++ unsigned placement_ram_size = gr_placement_current_ram_size(placement);
++ images_ram_size -= placement_ram_size;
++
++ Display *disp = imlib_context_get_display();
++ foreach_pixmap(*placement, pixmap, {
++ if (pixmap)
++ XFreePixmap(disp, pixmap);
++ });
++
++ placement->first_pixmap = 0;
++ placement->pixmaps_beyond_the_first.n = 0;
++ placement->scaled_ch = placement->scaled_cw = 0;
++
++ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) "
++ "ram: %ld KiB (- %u KiB)\n",
++ placement->image->image_id, placement->placement_id,
++ drawing_start_time - placement->atime, images_ram_size / 1024,
++ placement_ram_size / 1024);
++}
++
++/// Unload a single pixmap of the placement from RAM.
++static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) {
++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
++ if (!pixmap)
++ return;
++
++ Display *disp = imlib_context_get_display();
++ XFreePixmap(disp, pixmap);
++ gr_set_frame_pixmap(placement, frameidx, 0);
++ images_ram_size -= gr_placement_single_frame_ram_size(placement);
++
++ GR_LOG("After unloading pixmap %ld of "
++ "placement %u/%u (atime %ld ms ago) "
++ "frame %u (atime %ld ms ago) "
++ "ram: %ld KiB (- %u KiB)\n",
++ pixmap, placement->image->image_id, placement->placement_id,
++ drawing_start_time - placement->atime, frameidx,
++ drawing_start_time -
++ gr_get_frame(placement->image, frameidx)->atime,
++ images_ram_size / 1024,
++ gr_placement_single_frame_ram_size(placement) / 1024);
++}
++
++/// Deletes the on-disk cache file corresponding to the frame. The in-ram image
++/// object (if it exists) is not deleted, placements are not unloaded either.
++static void gr_delete_imagefile(ImageFrame *frame) {
++ // It may still be being loaded. Close the file in this case.
++ if (frame->open_file) {
++ fclose(frame->open_file);
++ frame->open_file = NULL;
++ }
++
++ if (frame->disk_size == 0)
++ return;
++
++ char filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
++ remove(filename);
++
++ unsigned disk_size = frame->disk_size;
++ images_disk_size -= disk_size;
++ frame->image->total_disk_size -= disk_size;
++ frame->disk_size = 0;
++
++ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) "
++ "disk: %ld KiB (- %u KiB)\n",
++ frame->image->image_id, frame->index,
++ drawing_start_time - frame->atime, images_disk_size / 1024,
++ disk_size / 1024);
++}
++
++/// Deletes all on-disk cache files of the image (for each frame).
++static void gr_delete_imagefiles(Image *img) {
++ foreach_frame(*img, frame, {
++ gr_delete_imagefile(frame);
++ });
++}
++
++/// Deletes the given placement: unloads, frees the object, but doesn't change
++/// the `placements` hash table.
++static void gr_delete_placement_keep_id(ImagePlacement *placement) {
++ if (!placement)
++ return;
++ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id,
++ placement->placement_id);
++ gr_unload_placement(placement);
++ kv_destroy(placement->pixmaps_beyond_the_first);
++ free(placement);
++ total_placement_count--;
++}
++
++/// Deletes all placements of `img`.
++static void gr_delete_all_placements(Image *img) {
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(img->placements, placement, {
++ gr_delete_placement_keep_id(placement);
++ });
++ kh_clear(id2placement, img->placements);
++}
++
++/// Deletes the given image: unloads, deletes the file, frees the Image object,
++/// but doesn't change the `images` hash table.
++static void gr_delete_image_keep_id(Image *img) {
++ if (!img)
++ return;
++ GR_LOG("Deleting image %u\n", img->image_id);
++ foreach_frame(*img, frame, {
++ gr_delete_imagefile(frame);
++ gr_unload_frame(frame);
++ });
++ kv_destroy(img->frames_beyond_the_first);
++ gr_delete_all_placements(img);
++ kh_destroy(id2placement, img->placements);
++ free(img);
++}
++
++/// Deletes the given image: unloads, deletes the file, frees the Image object,
++/// and also removes it from `images`.
++static void gr_delete_image(Image *img) {
++ if (!img)
++ return;
++ uint32_t id = img->image_id;
++ gr_delete_image_keep_id(img);
++ khiter_t k = kh_get(id2image, images, id);
++ kh_del(id2image, images, k);
++}
++
++/// Deletes the given placement: unloads, frees the object, and also removes it
++/// from `placements`.
++static void gr_delete_placement(ImagePlacement *placement) {
++ if (!placement)
++ return;
++ uint32_t id = placement->placement_id;
++ Image *img = placement->image;
++ gr_delete_placement_keep_id(placement);
++ khiter_t k = kh_get(id2placement, img->placements, id);
++ kh_del(id2placement, img->placements, k);
++}
++
++/// Deletes all images and clears `images`.
++static void gr_delete_all_images() {
++ Image *img = NULL;
++ kh_foreach_value(images, img, {
++ gr_delete_image_keep_id(img);
++ });
++ kh_clear(id2image, images);
++}
++
++/// Update the atime of the image.
++static void gr_touch_image(Image *img) {
++ img->atime = gr_now_ms();
++}
++
++/// Update the atime of the frame.
++static void gr_touch_frame(ImageFrame *frame) {
++ frame->image->atime = frame->atime = gr_now_ms();
++}
++
++/// Update the atime of the placement. Touches the images too.
++static void gr_touch_placement(ImagePlacement *placement) {
++ placement->image->atime = placement->atime = gr_now_ms();
++}
++
++/// Creates a new image with the given id. If an image with that id already
++/// exists, it is deleted first. If the provided id is 0, generates a
++/// random id.
++static Image *gr_new_image(uint32_t id) {
++ if (id == 0) {
++ do {
++ id = rand();
++ // Avoid IDs that don't need full 32 bits.
++ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 ||
++ gr_find_image(id));
++ GR_LOG("Generated random image id %u\n", id);
++ }
++ Image *img = gr_find_image(id);
++ gr_delete_image_keep_id(img);
++ GR_LOG("Creating image %u\n", id);
++ img = malloc(sizeof(Image));
++ memset(img, 0, sizeof(Image));
++ img->placements = kh_init(id2placement);
++ int ret;
++ khiter_t k = kh_put(id2image, images, id, &ret);
++ kh_value(images, k) = img;
++ img->image_id = id;
++ gr_touch_image(img);
++ img->global_command_index = global_command_counter;
++ return img;
++}
++
++/// Creates a new frame at the end of the frame array. It may be the first frame
++/// if there are no frames yet.
++static ImageFrame *gr_append_new_frame(Image *img) {
++ ImageFrame *frame = NULL;
++ if (img->first_frame.index == 0 &&
++ kv_size(img->frames_beyond_the_first) == 0) {
++ frame = &img->first_frame;
++ frame->index = 1;
++ } else {
++ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first);
++ memset(frame, 0, sizeof(ImageFrame));
++ frame->index = kv_size(img->frames_beyond_the_first) + 1;
++ }
++ frame->image = img;
++ gr_touch_frame(frame);
++ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id);
++ return frame;
++}
++
++/// Creates a new placement with the given id. If a placement with that id
++/// already exists, it is deleted first. If the provided id is 0, generates a
++/// random id.
++static ImagePlacement *gr_new_placement(Image *img, uint32_t id) {
++ if (id == 0) {
++ do {
++ // Currently we support only 24-bit IDs.
++ id = rand() & 0xFFFFFF;
++ // Avoid IDs that need only one byte.
++ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id));
++ }
++ ImagePlacement *placement = gr_find_placement(img, id);
++ gr_delete_placement_keep_id(placement);
++ GR_LOG("Creating placement %u/%u\n", img->image_id, id);
++ placement = malloc(sizeof(ImagePlacement));
++ memset(placement, 0, sizeof(ImagePlacement));
++ total_placement_count++;
++ int ret;
++ khiter_t k = kh_put(id2placement, img->placements, id, &ret);
++ kh_value(img->placements, k) = placement;
++ placement->image = img;
++ placement->placement_id = id;
++ gr_touch_placement(placement);
++ if (img->default_placement == 0)
++ img->default_placement = id;
++ return placement;
++}
++
++static int64_t ceil_div(int64_t a, int64_t b) {
++ return (a + b - 1) / b;
++}
++
++/// Computes the best number of rows and columns for a placement if it's not
++/// specified, and also adjusts the source rectangle size.
++static void gr_infer_placement_size_maybe(ImagePlacement *placement) {
++ // The size of the image.
++ int image_pix_width = placement->image->pix_width;
++ int image_pix_height = placement->image->pix_height;
++ // Negative values are not allowed. Quietly set them to 0.
++ if (placement->src_pix_x < 0)
++ placement->src_pix_x = 0;
++ if (placement->src_pix_y < 0)
++ placement->src_pix_y = 0;
++ if (placement->src_pix_width < 0)
++ placement->src_pix_width = 0;
++ if (placement->src_pix_height < 0)
++ placement->src_pix_height = 0;
++ // If the source rectangle is outside the image, truncate it.
++ if (placement->src_pix_x > image_pix_width)
++ placement->src_pix_x = image_pix_width;
++ if (placement->src_pix_y > image_pix_height)
++ placement->src_pix_y = image_pix_height;
++ // If the source rectangle is not specified, use the whole image. If
++ // it's partially outside the image, truncate it.
++ if (placement->src_pix_width == 0 ||
++ placement->src_pix_x + placement->src_pix_width > image_pix_width)
++ placement->src_pix_width =
++ image_pix_width - placement->src_pix_x;
++ if (placement->src_pix_height == 0 ||
++ placement->src_pix_y + placement->src_pix_height > image_pix_height)
++ placement->src_pix_height =
++ image_pix_height - placement->src_pix_y;
++
++ if (placement->cols != 0 && placement->rows != 0)
++ return;
++ if (placement->src_pix_width == 0 || placement->src_pix_height == 0)
++ return;
++ if (current_cw == 0 || current_ch == 0)
++ return;
++
++ // If no size is specified, use the image size.
++ if (placement->cols == 0 && placement->rows == 0) {
++ placement->cols =
++ ceil_div(placement->src_pix_width, current_cw);
++ placement->rows =
++ ceil_div(placement->src_pix_height, current_ch);
++ return;
++ }
++
++ // Some applications specify only one of the dimensions.
++ if (placement->scale_mode == SCALE_MODE_CONTAIN) {
++ // If we preserve aspect ratio and fit to width/height, the most
++ // logical thing is to find the minimum size of the
++ // non-specified dimension that allows the image to fit the
++ // specified dimension.
++ if (placement->cols == 0) {
++ placement->cols = ceil_div(
++ placement->src_pix_width * placement->rows *
++ current_ch,
++ placement->src_pix_height * current_cw);
++ return;
++ }
++ if (placement->rows == 0) {
++ placement->rows =
++ ceil_div(placement->src_pix_height *
++ placement->cols * current_cw,
++ placement->src_pix_width * current_ch);
++ return;
++ }
++ } else {
++ // Otherwise we stretch the image or preserve the original size.
++ // In both cases we compute the best number of columns from the
++ // pixel size and cell size.
++ // TODO: In the case of stretching it's not the most logical
++ // thing to do, may need to revisit in the future.
++ // Currently we switch to SCALE_MODE_CONTAIN when only one
++ // of the dimensions is specified, so this case shouldn't
++ // happen in practice.
++ if (!placement->cols)
++ placement->cols =
++ ceil_div(placement->src_pix_width, current_cw);
++ if (!placement->rows)
++ placement->rows =
++ ceil_div(placement->src_pix_height, current_ch);
++ }
++}
++
++/// Adjusts the current frame index if enough time has passed since the display
++/// of the current frame. Also computes the time of the next redraw of this
++/// image (`img->next_redraw`). The current time is passed as an argument so
++/// that all animations are in sync.
++static void gr_update_frame_index(Image *img, Milliseconds now) {
++ if (img->current_frame == 0) {
++ img->current_frame_time = now;
++ img->current_frame = 1;
++ img->next_redraw = now + MAX(1, img->first_frame.gap);
++ return;
++ }
++ // If the animation is stopped, show the current frame.
++ if (!img->animation_state ||
++ img->animation_state == ANIMATION_STATE_STOPPED ||
++ img->animation_state == ANIMATION_STATE_UNSET) {
++ // The next redraw is never (unless the state is changed).
++ img->next_redraw = 0;
++ return;
++ }
++ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img);
++ // If we are loading and we reached the last frame, show the last frame.
++ if (img->animation_state == ANIMATION_STATE_LOADING &&
++ img->current_frame == last_uploaded_frame_index) {
++ // The next redraw is never (unless the state is changed or
++ // frames are added).
++ img->next_redraw = 0;
++ return;
++ }
++
++ // Check how many milliseconds passed since the current frame was shown.
++ int passed_ms = now - img->current_frame_time;
++ // If the animation is looping and too much time has passes, we can
++ // make a shortcut.
++ if (img->animation_state == ANIMATION_STATE_LOOPING &&
++ img->total_duration > 0 && passed_ms >= img->total_duration) {
++ passed_ms %= img->total_duration;
++ img->current_frame_time = now - passed_ms;
++ }
++ // Find the next frame.
++ int original_frame_index = img->current_frame;
++ while (1) {
++ ImageFrame *frame = gr_get_frame(img, img->current_frame);
++ if (!frame) {
++ // The frame doesn't exist, go to the first frame.
++ img->current_frame = 1;
++ img->current_frame_time = now;
++ img->next_redraw = now + MAX(1, img->first_frame.gap);
++ return;
++ }
++ if (frame->gap >= 0 && passed_ms < frame->gap) {
++ // Not enough time has passed, we are still in the same
++ // frame, and it's not a gapless frame.
++ img->next_redraw =
++ img->current_frame_time + MAX(1, frame->gap);
++ return;
++ }
++ // Otherwise go to the next frame.
++ passed_ms -= MAX(0, frame->gap);
++ if (img->current_frame >= last_uploaded_frame_index) {
++ // It's the last frame, if the animation is loading,
++ // remain on it.
++ if (img->animation_state == ANIMATION_STATE_LOADING) {
++ img->next_redraw = 0;
++ return;
++ }
++ // Otherwise the animation is looping.
++ img->current_frame = 1;
++ // TODO: Support finite number of loops.
++ } else {
++ img->current_frame++;
++ }
++ // Make sure we don't get stuck in an infinite loop.
++ if (img->current_frame == original_frame_index) {
++ // We looped through all frames, but haven't reached the
++ // next frame yet. This may happen if too much time has
++ // passed since the last redraw or all the frames are
++ // gapless. Just move on to the next frame.
++ img->current_frame++;
++ if (img->current_frame >
++ last_uploaded_frame_index)
++ img->current_frame = 1;
++ img->current_frame_time = now;
++ img->next_redraw = now + MAX(
++ 1, gr_get_frame(img, img->current_frame)->gap);
++ return;
++ }
++ // Adjust the start time of the frame. The next redraw time will
++ // be set in the next iteration.
++ img->current_frame_time += MAX(0, frame->gap);
++ }
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Unloading and deleting images to save resources.
++////////////////////////////////////////////////////////////////////////////////
++
++/// A helper to compare frames by atime for qsort.
++static int gr_cmp_frames_by_atime(const void *a, const void *b) {
++ ImageFrame *frame_a = *(ImageFrame *const *)a;
++ ImageFrame *frame_b = *(ImageFrame *const *)b;
++ if (frame_a->atime == frame_b->atime)
++ return frame_a->image->global_command_index -
++ frame_b->image->global_command_index;
++ return frame_a->atime - frame_b->atime;
++}
++
++/// A helper to compare images by atime for qsort.
++static int gr_cmp_images_by_atime(const void *a, const void *b) {
++ Image *img_a = *(Image *const *)a;
++ Image *img_b = *(Image *const *)b;
++ if (img_a->atime == img_b->atime)
++ return img_a->global_command_index -
++ img_b->global_command_index;
++ return img_a->atime - img_b->atime;
++}
++
++/// A helper to compare placements by atime for qsort.
++static int gr_cmp_placements_by_atime(const void *a, const void *b) {
++ ImagePlacement *p_a = *(ImagePlacement **)a;
++ ImagePlacement *p_b = *(ImagePlacement **)b;
++ if (p_a->atime == p_b->atime)
++ return p_a->image->global_command_index -
++ p_b->image->global_command_index;
++ return p_a->atime - p_b->atime;
++}
++
++typedef kvec_t(Image *) ImageVec;
++typedef kvec_t(ImagePlacement *) ImagePlacementVec;
++typedef kvec_t(ImageFrame *) ImageFrameVec;
++
++/// Returns an array of pointers to all images sorted by atime.
++static ImageVec gr_get_images_sorted_by_atime() {
++ ImageVec vec;
++ kv_init(vec);
++ if (kh_size(images) == 0)
++ return vec;
++ kv_resize(Image *, vec, kh_size(images));
++ Image *img = NULL;
++ kh_foreach_value(images, img, { kv_push(Image *, vec, img); });
++ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime);
++ return vec;
++}
++
++/// Returns an array of pointers to all placements sorted by atime.
++static ImagePlacementVec gr_get_placements_sorted_by_atime() {
++ ImagePlacementVec vec;
++ kv_init(vec);
++ if (total_placement_count == 0)
++ return vec;
++ kv_resize(ImagePlacement *, vec, total_placement_count);
++ Image *img = NULL;
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(images, img, {
++ kh_foreach_value(img->placements, placement, {
++ kv_push(ImagePlacement *, vec, placement);
++ });
++ });
++ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *),
++ gr_cmp_placements_by_atime);
++ return vec;
++}
++
++/// Returns an array of pointers to all frames sorted by atime.
++static ImageFrameVec gr_get_frames_sorted_by_atime() {
++ ImageFrameVec frames;
++ kv_init(frames);
++ Image *img = NULL;
++ kh_foreach_value(images, img, {
++ foreach_frame(*img, frame, {
++ kv_push(ImageFrame *, frames, frame);
++ });
++ });
++ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *),
++ gr_cmp_frames_by_atime);
++ return frames;
++}
++
++/// An object that can be unloaded from RAM.
++typedef struct {
++ /// Some score, probably based on access time. The lower the score, the
++ /// more likely that the object should be unloaded.
++ int64_t score;
++ union {
++ ImagePlacement *placement;
++ ImageFrame *frame;
++ };
++ /// If zero, the object is the imlib object of `frame`, if non-zero,
++ /// the object is a pixmap of `frameidx`-th frame of `placement`.
++ int frameidx;
++} UnloadableObject;
++
++typedef kvec_t(UnloadableObject) UnloadableObjectVec;
++
++/// A helper to compare unloadable objects by score for qsort.
++static int gr_cmp_unloadable_objects(const void *a, const void *b) {
++ UnloadableObject *obj_a = (UnloadableObject *)a;
++ UnloadableObject *obj_b = (UnloadableObject *)b;
++ return obj_a->score - obj_b->score;
++}
++
++/// Unloads an unloadable object from RAM.
++static void gr_unload_object(UnloadableObject *obj) {
++ if (obj->frameidx) {
++ if (obj->placement->protected_frame == obj->frameidx)
++ return;
++ gr_unload_pixmap(obj->placement, obj->frameidx);
++ } else {
++ gr_unload_frame(obj->frame);
++ }
++}
++
++/// Returns the recency threshold for an image. Frames that were accessed within
++/// this threshold from now are considered recent and may be handled
++/// differently because we may need them again very soon.
++static Milliseconds gr_recency_threshold(Image *img) {
++ return img->total_duration * 2 + 1000;
++}
++
++/// Creates an unloadable object for the imlib object of a frame.
++static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now,
++ ImageFrame *frame) {
++ UnloadableObject obj = {0};
++ obj.frameidx = 0;
++ obj.frame = frame;
++ Milliseconds atime = frame->atime;
++ obj.score = atime;
++ if (atime >= now - gr_recency_threshold(frame->image)) {
++ // This is a recent frame, probably from an active animation.
++ // Score it above `now` to prefer unloading non-active frames.
++ // Randomize the score because it's not very clear in which
++ // order we want to unload them: reloading a frame may require
++ // reloading other frames.
++ obj.score = now + 1000 + rand() % 1000;
++ }
++ return obj;
++}
++
++/// Creates an unloadable object for a pixmap.
++static UnloadableObject
++gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame,
++ ImagePlacement *placement) {
++ UnloadableObject obj = {0};
++ obj.frameidx = frame->index;
++ obj.placement = placement;
++ obj.score = placement->atime;
++ // Since we don't store pixmap atimes, use the
++ // oldest atime of the frame and the placement.
++ Milliseconds atime = MIN(placement->atime, frame->atime);
++ obj.score = atime;
++ if (atime >= now - gr_recency_threshold(frame->image)) {
++ // This is a recent pixmap, probably from an active animation.
++ // Score it above `now` to prefer unloading non-active frames.
++ // Also assign higher scores to frames that are closer to the
++ // current frame (more likely to be used soon).
++ int num_frames = gr_last_frame_index(frame->image);
++ int dist = frame->index - frame->image->current_frame;
++ if (dist < 0)
++ dist += num_frames;
++ obj.score =
++ now + 1000 + (num_frames - dist) * 1000 / num_frames;
++ // If the pixmap is much larger than the imlib image, prefer to
++ // unload the pixmap by adding up to -1000 to the score. If the
++ // imlib image is larger, add up to +1000.
++ float imlib_size = gr_frame_current_ram_size(frame);
++ float pixmap_size =
++ gr_placement_single_frame_ram_size(placement);
++ obj.score +=
++ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5);
++ }
++ return obj;
++}
++
++/// Returns an array of unloadable objects sorted by score.
++static UnloadableObjectVec
++gr_get_unloadable_objects_sorted_by_score(Milliseconds now) {
++ UnloadableObjectVec objects;
++ kv_init(objects);
++ Image *img = NULL;
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(images, img, {
++ foreach_frame(*img, frame, {
++ if (!frame->imlib_object)
++ continue;
++ kv_push(UnloadableObject, objects,
++ gr_unloadable_object_for_frame(now, frame));
++ int frameidx = frame->index;
++ kh_foreach_value(img->placements, placement, {
++ if (!gr_get_frame_pixmap(placement, frameidx))
++ continue;
++ kv_push(UnloadableObject, objects,
++ gr_unloadable_object_for_pixmap(
++ now, frame, placement));
++ });
++ });
++ });
++ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject),
++ gr_cmp_unloadable_objects);
++ return objects;
++}
++
++/// Returns the limit adjusted by the excess tolerance ratio.
++static inline unsigned apply_tolerance(unsigned limit) {
++ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio);
++}
++
++/// Checks RAM and disk cache limits and deletes/unloads some images.
++static void gr_check_limits() {
++ Milliseconds now = gr_now_ms();
++ ImageVec images_sorted = {0};
++ ImagePlacementVec placements_sorted = {0};
++ ImageFrameVec frames_sorted = {0};
++ UnloadableObjectVec objects_sorted = {0};
++ int images_begin = 0;
++ int placements_begin = 0;
++ char changed = 0;
++ // First reduce the number of images if there are too many.
++ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) {
++ GR_LOG("Too many images: %d\n", kh_size(images));
++ changed = 1;
++ images_sorted = gr_get_images_sorted_by_atime();
++ int to_delete = kv_size(images_sorted) -
++ graphics_max_total_placements;
++ for (; images_begin < to_delete; images_begin++)
++ gr_delete_image(images_sorted.a[images_begin]);
++ }
++ // Then reduce the number of placements if there are too many.
++ if (total_placement_count >
++ apply_tolerance(graphics_max_total_placements)) {
++ GR_LOG("Too many placements: %d\n", total_placement_count);
++ changed = 1;
++ placements_sorted = gr_get_placements_sorted_by_atime();
++ int to_delete = kv_size(placements_sorted) -
++ graphics_max_total_placements;
++ for (; placements_begin < to_delete; placements_begin++) {
++ ImagePlacement *placement =
++ placements_sorted.a[placements_begin];
++ if (placement->protected_frame)
++ break;
++ gr_delete_placement(placement);
++ }
++ }
++ // Then reduce the size of the image file cache. The files correspond to
++ // image frames.
++ if (images_disk_size >
++ apply_tolerance(graphics_total_file_cache_size)) {
++ GR_LOG("Too big disk cache: %ld KiB\n",
++ images_disk_size / 1024);
++ changed = 1;
++ frames_sorted = gr_get_frames_sorted_by_atime();
++ for (int i = 0; i < kv_size(frames_sorted); i++) {
++ if (images_disk_size <= graphics_total_file_cache_size)
++ break;
++ gr_delete_imagefile(kv_A(frames_sorted, i));
++ }
++ }
++ // Then unload images from RAM.
++ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) {
++ changed = 1;
++ int frames_begin = 0;
++ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024);
++ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now);
++ for (int i = 0; i < kv_size(objects_sorted); i++) {
++ if (images_ram_size <= graphics_max_total_ram_size)
++ break;
++ gr_unload_object(&kv_A(objects_sorted, i));
++ }
++ }
++ if (changed) {
++ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
++ "img count: %d placement count: %d\n",
++ images_ram_size / 1024, images_disk_size / 1024,
++ kh_size(images), total_placement_count);
++ }
++ kv_destroy(images_sorted);
++ kv_destroy(placements_sorted);
++ kv_destroy(frames_sorted);
++ kv_destroy(objects_sorted);
++}
++
++/// Unloads all images by user request.
++void gr_unload_images_to_reduce_ram() {
++ Image *img = NULL;
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(images, img, {
++ kh_foreach_value(img->placements, placement, {
++ if (placement->protected_frame)
++ continue;
++ gr_unload_placement(placement);
++ });
++ gr_unload_all_frames(img);
++ });
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Image loading.
++////////////////////////////////////////////////////////////////////////////////
++
++/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2
++/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted
++/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout
++/// on little-endian architectures).
++static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format,
++ size_t num_pixels) {
++ size_t pixel_size = format == 24 ? 3 : 4;
++ if (format == 32) {
++ for (unsigned i = 0; i < num_pixels; ++i) {
++ unsigned byte_i = i * pixel_size;
++ to[i] = ((DATA32)from[byte_i + 2]) |
++ ((DATA32)from[byte_i + 1]) << 8 |
++ ((DATA32)from[byte_i]) << 16 |
++ ((DATA32)from[byte_i + 3]) << 24;
++ }
++ } else {
++ for (unsigned i = 0; i < num_pixels; ++i) {
++ unsigned byte_i = i * pixel_size;
++ to[i] = ((DATA32)from[byte_i + 2]) |
++ ((DATA32)from[byte_i + 1]) << 8 |
++ ((DATA32)from[byte_i]) << 16 | 0xFF000000;
++ }
++ }
++}
++
++/// Loads uncompressed RGB or RGBA image data from a file.
++static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file,
++ int format,
++ size_t total_pixels) {
++ unsigned char chunk[BUFSIZ];
++ size_t pixel_size = format == 24 ? 3 : 4;
++ size_t chunk_size_pix = BUFSIZ / 4;
++ size_t chunk_size_bytes = chunk_size_pix * pixel_size;
++ size_t bytes = total_pixels * pixel_size;
++ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels;
++ chunk_start_pix += chunk_size_pix) {
++ size_t read_size = fread(chunk, 1, chunk_size_bytes, file);
++ size_t read_pixels = read_size / pixel_size;
++ if (chunk_start_pix + read_pixels > total_pixels)
++ read_pixels = total_pixels - chunk_start_pix;
++ gr_copy_pixels(data + chunk_start_pix, chunk, format,
++ read_pixels);
++ }
++}
++
++#define COMPRESSED_CHUNK_SIZE BUFSIZ
++#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4)
++
++/// Loads compressed RGB or RGBA image data from a file.
++static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file,
++ int format, size_t total_pixels) {
++ size_t pixel_size = format == 24 ? 3 : 4;
++ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE];
++ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE];
++
++ z_stream strm;
++ strm.zalloc = Z_NULL;
++ strm.zfree = Z_NULL;
++ strm.opaque = Z_NULL;
++ strm.next_out = decompressed_chunk;
++ strm.avail_out = DECOMPRESSED_CHUNK_SIZE;
++ strm.avail_in = 0;
++ strm.next_in = Z_NULL;
++ int ret = inflateInit(&strm);
++ if (ret != Z_OK)
++ return 1;
++
++ int error = 0;
++ int progress = 0;
++ size_t total_copied_pixels = 0;
++ while (1) {
++ // If we don't have enough data in the input buffer, try to read
++ // from the file.
++ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) {
++ // Move the existing data to the beginning.
++ memmove(compressed_chunk, strm.next_in, strm.avail_in);
++ strm.next_in = compressed_chunk;
++ // Read more data.
++ size_t bytes_read = fread(
++ compressed_chunk + strm.avail_in, 1,
++ COMPRESSED_CHUNK_SIZE - strm.avail_in, file);
++ strm.avail_in += bytes_read;
++ if (bytes_read != 0)
++ progress = 1;
++ }
++
++ // Try to inflate the data.
++ int ret = inflate(&strm, Z_SYNC_FLUSH);
++ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
++ error = 1;
++ fprintf(stderr,
++ "error: could not decompress the image, error "
++ "%s\n",
++ ret == Z_MEM_ERROR ? "Z_MEM_ERROR"
++ : "Z_DATA_ERROR");
++ break;
++ }
++
++ // Copy the data from the output buffer to the image.
++ size_t full_pixels =
++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size;
++ // Make sure we don't overflow the image.
++ if (full_pixels > total_pixels - total_copied_pixels)
++ full_pixels = total_pixels - total_copied_pixels;
++ if (full_pixels > 0) {
++ // Copy pixels.
++ gr_copy_pixels(data, decompressed_chunk, format,
++ full_pixels);
++ data += full_pixels;
++ total_copied_pixels += full_pixels;
++ if (total_copied_pixels >= total_pixels) {
++ // We filled the whole image, there may be some
++ // data left, but we just truncate it.
++ break;
++ }
++ // Move the remaining data to the beginning.
++ size_t copied_bytes = full_pixels * pixel_size;
++ size_t leftover =
++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) -
++ copied_bytes;
++ memmove(decompressed_chunk,
++ decompressed_chunk + copied_bytes, leftover);
++ strm.next_out -= copied_bytes;
++ strm.avail_out += copied_bytes;
++ progress = 1;
++ }
++
++ // If we haven't made any progress, then we have reached the end
++ // of both the file and the inflated data.
++ if (!progress)
++ break;
++ progress = 0;
++ }
++
++ inflateEnd(&strm);
++ return error;
++}
++
++#undef COMPRESSED_CHUNK_SIZE
++#undef DECOMPRESSED_CHUNK_SIZE
++
++/// Load the image from a file containing raw pixel data (RGB or RGBA), the data
++/// may be compressed.
++static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame,
++ const char *filename) {
++ size_t total_pixels = frame->data_pix_width * frame->data_pix_height;
++ if (total_pixels * 4 > graphics_max_single_image_ram_size) {
++ fprintf(stderr,
++ "error: image %u frame %u is too big too load: %zu > %u\n",
++ frame->image->image_id, frame->index, total_pixels * 4,
++ graphics_max_single_image_ram_size);
++ return NULL;
++ }
++
++ FILE* file = fopen(filename, "rb");
++ if (!file) {
++ fprintf(stderr,
++ "error: could not open image file: %s\n",
++ sanitized_filename(filename));
++ return NULL;
++ }
++
++ Imlib_Image image = imlib_create_image(frame->data_pix_width,
++ frame->data_pix_height);
++ if (!image) {
++ fprintf(stderr,
++ "error: could not create an image of size %d x %d\n",
++ frame->data_pix_width, frame->data_pix_height);
++ fclose(file);
++ return NULL;
++ }
++
++ imlib_context_set_image(image);
++ imlib_image_set_has_alpha(1);
++ DATA32* data = imlib_image_get_data();
++
++ // The default format is 32.
++ int format = frame->format ? frame->format : 32;
++
++ if (frame->compression == 0) {
++ gr_load_raw_pixel_data_uncompressed(data, file, format,
++ total_pixels);
++ } else {
++ int ret = gr_load_raw_pixel_data_compressed(data, file, format,
++ total_pixels);
++ if (ret != 0) {
++ imlib_image_put_back_data(data);
++ imlib_free_image();
++ fclose(file);
++ return NULL;
++ }
++ }
++
++ fclose(file);
++ imlib_image_put_back_data(data);
++ return image;
++}
++
++/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object
++/// is fully composed on top of the background frame. If the frame is already
++/// loaded, does nothing. Loading may fail, in which case the status of the
++/// frame will be set to STATUS_RAM_LOADING_ERROR.
++static void gr_load_imlib_object(ImageFrame *frame) {
++ if (frame->imlib_object)
++ return;
++
++ // If the image is uninitialized or uploading has failed, or the file
++ // has been deleted, we cannot load the image.
++ if (frame->status < STATUS_UPLOADING_SUCCESS)
++ return;
++ if (frame->disk_size == 0) {
++ if (frame->status != STATUS_RAM_LOADING_ERROR) {
++ fprintf(stderr,
++ "error: cached image was deleted: %u frame %u\n",
++ frame->image->image_id, frame->index);
++ }
++ frame->status = STATUS_RAM_LOADING_ERROR;
++ return;
++ }
++
++ // Prevent recursive dependences between frames.
++ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) {
++ fprintf(stderr,
++ "error: recursive loading of image %u frame %u\n",
++ frame->image->image_id, frame->index);
++ frame->status = STATUS_RAM_LOADING_ERROR;
++ return;
++ }
++ frame->status = STATUS_RAM_LOADING_IN_PROGRESS;
++
++ // Load the background frame if needed. Hopefully it's not recursive.
++ ImageFrame *bg_frame = NULL;
++ if (frame->background_frame_index) {
++ bg_frame = gr_get_frame(frame->image,
++ frame->background_frame_index);
++ if (!bg_frame) {
++ fprintf(stderr,
++ "error: could not find background "
++ "frame %d for image %u frame %d\n",
++ frame->background_frame_index,
++ frame->image->image_id, frame->index);
++ frame->status = STATUS_RAM_LOADING_ERROR;
++ return;
++ }
++ gr_load_imlib_object(bg_frame);
++ if (!bg_frame->imlib_object) {
++ fprintf(stderr,
++ "error: could not load background frame %d for "
++ "image %u frame %d\n",
++ frame->background_frame_index,
++ frame->image->image_id, frame->index);
++ frame->status = STATUS_RAM_LOADING_ERROR;
++ return;
++ }
++ }
++
++ // Load the frame data image.
++ Imlib_Image frame_data_image = NULL;
++ char filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
++ GR_LOG("Loading image: %s\n", sanitized_filename(filename));
++ if (frame->format == 100 || frame->format == 0)
++ frame_data_image = imlib_load_image(filename);
++ if (frame->format == 32 || frame->format == 24 ||
++ (!frame_data_image && frame->format == 0))
++ frame_data_image = gr_load_raw_pixel_data(frame, filename);
++ this_redraw_cycle_loaded_files++;
++
++ if (!frame_data_image) {
++ if (frame->status != STATUS_RAM_LOADING_ERROR) {
++ fprintf(stderr, "error: could not load image: %s\n",
++ sanitized_filename(filename));
++ }
++ frame->status = STATUS_RAM_LOADING_ERROR;
++ return;
++ }
++
++ imlib_context_set_image(frame_data_image);
++ int frame_data_width = imlib_image_get_width();
++ int frame_data_height = imlib_image_get_height();
++ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width,
++ frame_data_height);
++ // If imlib loading succeeded, and it is the first frame, set the
++ // information about the original image size, unless it's already set.
++ if (frame->index == 1 && frame->image->pix_width == 0 &&
++ frame->image->pix_height == 0) {
++ frame->image->pix_width = frame_data_width;
++ frame->image->pix_height = frame_data_height;
++ }
++
++ int image_width = frame->image->pix_width;
++ int image_height = frame->image->pix_height;
++
++ // Compose the image with the background color or frame.
++ if (frame->background_color != 0 || bg_frame ||
++ image_width != frame_data_width ||
++ image_height != frame_data_height) {
++ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n",
++ frame->background_color, frame->background_frame_index);
++ Imlib_Image composed_image = imlib_create_image(
++ image_width, image_height);
++ imlib_context_set_image(composed_image);
++ imlib_image_set_has_alpha(1);
++ imlib_context_set_anti_alias(0);
++
++ // Start with the background frame or color.
++ imlib_context_set_blend(0);
++ if (bg_frame && bg_frame->imlib_object) {
++ imlib_blend_image_onto_image(
++ bg_frame->imlib_object, 1, 0, 0,
++ image_width, image_height, 0, 0,
++ image_width, image_height);
++ } else {
++ int r = (frame->background_color >> 24) & 0xFF;
++ int g = (frame->background_color >> 16) & 0xFF;
++ int b = (frame->background_color >> 8) & 0xFF;
++ int a = frame->background_color & 0xFF;
++ imlib_context_set_color(r, g, b, a);
++ imlib_image_fill_rectangle(0, 0, image_width,
++ image_height);
++ }
++
++ // Blend the frame data image onto the background.
++ imlib_context_set_blend(1);
++ imlib_blend_image_onto_image(
++ frame_data_image, 1, 0, 0, frame->data_pix_width,
++ frame->data_pix_height, frame->x, frame->y,
++ frame->data_pix_width, frame->data_pix_height);
++
++ // Free the frame data image.
++ imlib_context_set_image(frame_data_image);
++ imlib_free_image();
++
++ frame_data_image = composed_image;
++ }
++
++ frame->imlib_object = frame_data_image;
++
++ images_ram_size += gr_frame_current_ram_size(frame);
++ frame->status = STATUS_RAM_LOADING_SUCCESS;
++
++ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n",
++ frame->image->image_id, frame->index,
++ images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024);
++}
++
++/// Premultiplies the alpha channel of the image data. The data is an array of
++/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB.
++static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) {
++ for (size_t i = 0; i < num_pixels; ++i) {
++ DATA32 pixel = data[i];
++ unsigned char a = pixel >> 24;
++ if (a == 0) {
++ data[i] = 0;
++ } else if (a != 255) {
++ unsigned char b = (pixel & 0xFF) * a / 255;
++ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255;
++ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255;
++ data[i] = (a << 24) | (r << 16) | (g << 8) | b;
++ }
++ }
++}
++
++/// Creates a pixmap for the frame of an image placement. The pixmap contain the
++/// image data correctly scaled and fit to the box defined by the number of
++/// rows/columns of the image placement and the provided cell dimensions in
++/// pixels. If the placement is already loaded, it will be reloaded only if the
++/// cell dimensions have changed.
++Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) {
++ Image *img = placement->image;
++ ImageFrame *frame = gr_get_frame(img, frameidx);
++
++ // Update the atime uncoditionally.
++ gr_touch_placement(placement);
++ if (frame)
++ gr_touch_frame(frame);
++
++ // If cw or ch are different, unload all the pixmaps.
++ if (placement->scaled_cw != cw || placement->scaled_ch != ch) {
++ gr_unload_placement(placement);
++ placement->scaled_cw = cw;
++ placement->scaled_ch = ch;
++ }
++
++ // If it's already loaded, do nothing.
++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
++ if (pixmap)
++ return pixmap;
++
++ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id,
++ placement->placement_id, frameidx);
++
++ // Load the imlib object for the frame.
++ if (!frame) {
++ fprintf(stderr,
++ "error: could not find frame %u for image %u\n",
++ frameidx, img->image_id);
++ return 0;
++ }
++ gr_load_imlib_object(frame);
++ if (!frame->imlib_object)
++ return 0;
++
++ // Infer the placement size if needed.
++ gr_infer_placement_size_maybe(placement);
++
++ // Create the scaled image. This is temporary, we will scale it
++ // appropriately, upload to the X server, and then delete immediately.
++ int scaled_w = (int)placement->cols * cw;
++ int scaled_h = (int)placement->rows * ch;
++ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) {
++ fprintf(stderr,
++ "error: placement %u/%u would be too big to load: %d x "
++ "%d x 4 > %u\n",
++ img->image_id, placement->placement_id, scaled_w,
++ scaled_h, graphics_max_single_image_ram_size);
++ return 0;
++ }
++ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h);
++ if (!scaled_image) {
++ fprintf(stderr,
++ "error: imlib_create_image(%d, %d) returned "
++ "null\n",
++ scaled_w, scaled_h);
++ return 0;
++ }
++ imlib_context_set_image(scaled_image);
++ imlib_image_set_has_alpha(1);
++
++ // First fill the scaled image with the transparent color.
++ imlib_context_set_blend(0);
++ imlib_context_set_color(0, 0, 0, 0);
++ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h);
++ imlib_context_set_anti_alias(1);
++ imlib_context_set_blend(1);
++
++ // The source rectangle.
++ int src_x = placement->src_pix_x;
++ int src_y = placement->src_pix_y;
++ int src_w = placement->src_pix_width;
++ int src_h = placement->src_pix_height;
++ // Whether the box is too small to use the true size of the image.
++ char box_too_small = scaled_w < src_w || scaled_h < src_h;
++ char mode = placement->scale_mode;
++
++ // Then blend the original image onto the transparent background.
++ if (src_w <= 0 || src_h <= 0) {
++ fprintf(stderr, "warning: image of zero size\n");
++ } else if (mode == SCALE_MODE_FILL) {
++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
++ src_y, src_w, src_h, 0, 0,
++ scaled_w, scaled_h);
++ } else if (mode == SCALE_MODE_NONE ||
++ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) {
++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
++ src_y, src_w, src_h, 0, 0, src_w,
++ src_h);
++ } else {
++ if (mode != SCALE_MODE_CONTAIN &&
++ mode != SCALE_MODE_NONE_OR_CONTAIN) {
++ fprintf(stderr,
++ "warning: unknown scale mode %u, using "
++ "'contain' instead\n",
++ mode);
++ }
++ int dest_x, dest_y;
++ int dest_w, dest_h;
++ if (scaled_w * src_h > src_w * scaled_h) {
++ // If the box is wider than the original image, fit to
++ // height.
++ dest_h = scaled_h;
++ dest_y = 0;
++ dest_w = src_w * scaled_h / src_h;
++ dest_x = (scaled_w - dest_w) / 2;
++ } else {
++ // Otherwise, fit to width.
++ dest_w = scaled_w;
++ dest_x = 0;
++ dest_h = src_h * scaled_w / src_w;
++ dest_y = (scaled_h - dest_h) / 2;
++ }
++ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
++ src_y, src_w, src_h, dest_x,
++ dest_y, dest_w, dest_h);
++ }
++
++ // XRender needs the alpha channel premultiplied.
++ DATA32 *data = imlib_image_get_data();
++ gr_premultiply_alpha(data, scaled_w * scaled_h);
++
++ // Upload the image to the X server.
++ Display *disp = imlib_context_get_display();
++ Visual *vis = imlib_context_get_visual();
++ Colormap cmap = imlib_context_get_colormap();
++ Drawable drawable = imlib_context_get_drawable();
++ if (!drawable)
++ drawable = DefaultRootWindow(disp);
++ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32);
++ XVisualInfo visinfo;
++ XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo);
++ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
++ (char *)data, scaled_w, scaled_h, 32, 0);
++ GC gc = XCreateGC(disp, pixmap, 0, NULL);
++ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w,
++ scaled_h);
++ XFreeGC(disp, gc);
++ // XDestroyImage will free the data as well, but it is managed by imlib,
++ // so set it to NULL.
++ ximage->data = NULL;
++ XDestroyImage(ximage);
++ imlib_image_put_back_data(data);
++ imlib_free_image();
++
++ // Assign the pixmap to the frame and increase the ram size.
++ gr_set_frame_pixmap(placement, frameidx, pixmap);
++ images_ram_size += gr_placement_single_frame_ram_size(placement);
++ this_redraw_cycle_loaded_pixmaps++;
++
++ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
++ "KiB)\n",
++ frame->image->image_id, placement->placement_id, frame->index,
++ images_ram_size / 1024,
++ gr_placement_single_frame_ram_size(placement) / 1024);
++
++ // Free up ram if needed, but keep the pixmap we've loaded no matter
++ // what.
++ placement->protected_frame = frameidx;
++ gr_check_limits();
++ placement->protected_frame = 0;
++
++ return pixmap;
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Initialization and deinitialization.
++////////////////////////////////////////////////////////////////////////////////
++
++/// Creates a temporary directory.
++static int gr_create_cache_dir() {
++ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir));
++ if (!mkdtemp(cache_dir)) {
++ fprintf(stderr,
++ "error: could not create temporary dir from template "
++ "%s\n",
++ sanitized_filename(cache_dir));
++ return 0;
++ }
++ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir);
++ return 1;
++}
++
++/// Checks whether `tmp_dir` exists and recreates it if it doesn't.
++static void gr_make_sure_tmpdir_exists() {
++ struct stat st;
++ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode))
++ return;
++ fprintf(stderr,
++ "error: %s is not a directory, will need to create a new "
++ "graphics cache directory\n",
++ sanitized_filename(cache_dir));
++ gr_create_cache_dir();
++}
++
++/// Initialize the graphics module.
++void gr_init(Display *disp, Visual *vis, Colormap cm) {
++ // Set the initialization time.
++ clock_gettime(CLOCK_MONOTONIC, &initialization_time);
++
++ // Create the temporary dir.
++ if (!gr_create_cache_dir())
++ abort();
++
++ // Initialize imlib.
++ imlib_context_set_display(disp);
++ imlib_context_set_visual(vis);
++ imlib_context_set_colormap(cm);
++ imlib_context_set_anti_alias(1);
++ imlib_context_set_blend(1);
++ // Imlib2 checks only the file name when caching, which is not enough
++ // for us since we reuse file names. Disable caching.
++ imlib_set_cache_size(0);
++
++ // Prepare for color inversion.
++ for (size_t i = 0; i < 256; ++i)
++ reverse_table[i] = 255 - i;
++
++ // Create data structures.
++ images = kh_init(id2image);
++ kv_init(next_redraw_times);
++
++ atexit(gr_deinit);
++}
++
++/// Deinitialize the graphics module.
++void gr_deinit() {
++ // Remove the cache dir.
++ remove(cache_dir);
++ kv_destroy(next_redraw_times);
++ if (images) {
++ // Delete all images.
++ gr_delete_all_images();
++ // Destroy the data structures.
++ kh_destroy(id2image, images);
++ images = NULL;
++ }
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Dumping, debugging, and image preview.
++////////////////////////////////////////////////////////////////////////////////
++
++/// Returns a string containing a time difference in a human-readable format.
++/// Uses a static buffer, so be careful.
++static const char *gr_ago(Milliseconds diff) {
++ static char result[32];
++ double seconds = (double)diff / 1000.0;
++ if (seconds < 1)
++ snprintf(result, sizeof(result), "%.2f sec ago", seconds);
++ else if (seconds < 60)
++ snprintf(result, sizeof(result), "%d sec ago", (int)seconds);
++ else if (seconds < 3600)
++ snprintf(result, sizeof(result), "%d min %d sec ago",
++ (int)(seconds / 60), (int)(seconds) % 60);
++ else {
++ snprintf(result, sizeof(result), "%d hr %d min %d sec ago",
++ (int)(seconds / 3600), (int)(seconds) % 3600 / 60,
++ (int)(seconds) % 60);
++ }
++ return result;
++}
++
++/// Prints to `file` with an indentation of `ind` spaces.
++static void fprintf_ind(FILE *file, int ind, const char *format, ...) {
++ fprintf(file, "%*s", ind, "");
++ va_list args;
++ va_start(args, format);
++ vfprintf(file, format, args);
++ va_end(args);
++}
++
++/// Dumps the image info to `file` with an indentation of `ind` spaces.
++static void gr_dump_image_info(FILE *file, Image *img, int ind) {
++ if (!img) {
++ fprintf_ind(file, ind, "Image is NULL\n");
++ return;
++ }
++ Milliseconds now = gr_now_ms();
++ fprintf_ind(file, ind, "Image %u\n", img->image_id);
++ ind += 4;
++ fprintf_ind(file, ind, "number: %u\n", img->image_number);
++ fprintf_ind(file, ind, "global command index: %lu\n",
++ img->global_command_index);
++ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime,
++ gr_ago(now - img->atime));
++ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width,
++ img->pix_height);
++ fprintf_ind(file, ind, "cur frame start time: %ld %s\n",
++ img->current_frame_time,
++ gr_ago(now - img->current_frame_time));
++ if (img->next_redraw)
++ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n",
++ img->next_redraw, img->next_redraw - now);
++ fprintf_ind(file, ind, "total disk size: %u KiB\n",
++ img->total_disk_size / 1024);
++ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration);
++ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img));
++ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame);
++ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state);
++ fprintf_ind(file, ind, "default_placement: %u\n",
++ img->default_placement);
++}
++
++/// Dumps the frame info to `file` with an indentation of `ind` spaces.
++static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) {
++ if (!frame) {
++ fprintf_ind(file, ind, "Frame is NULL\n");
++ return;
++ }
++ Milliseconds now = gr_now_ms();
++ fprintf_ind(file, ind, "Frame %d\n", frame->index);
++ ind += 4;
++ if (frame->index == 0) {
++ fprintf_ind(file, ind, "NOT INITIALIZED\n");
++ return;
++ }
++ if (frame->uploading_failure)
++ fprintf_ind(file, ind, "uploading failure: %s\n",
++ image_uploading_failure_strings
++ [frame->uploading_failure]);
++ fprintf_ind(file, ind, "gap: %d\n", frame->gap);
++ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime,
++ gr_ago(now - frame->atime));
++ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width,
++ frame->data_pix_height);
++ char filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
++ if (access(filename, F_OK) != -1)
++ fprintf_ind(file, ind, "file: %s\n",
++ sanitized_filename(filename));
++ else
++ fprintf_ind(file, ind, "not on disk\n");
++ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024);
++ if (frame->imlib_object) {
++ unsigned ram_size = gr_frame_current_ram_size(frame);
++ fprintf_ind(file, ind,
++ "loaded into ram, size: %d "
++ "KiB\n",
++ ram_size / 1024);
++ } else {
++ fprintf_ind(file, ind, "not loaded into ram\n");
++ }
++}
++
++/// Dumps the placement info to `file` with an indentation of `ind` spaces.
++static void gr_dump_placement_info(FILE *file, ImagePlacement *placement,
++ int ind) {
++ if (!placement) {
++ fprintf_ind(file, ind, "Placement is NULL\n");
++ return;
++ }
++ Milliseconds now = gr_now_ms();
++ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id);
++ ind += 4;
++ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime,
++ gr_ago(now - placement->atime));
++ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode);
++ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols,
++ placement->rows);
++ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw,
++ placement->scaled_ch);
++ fprintf_ind(file, ind, "ram per frame: %u KiB\n",
++ gr_placement_single_frame_ram_size(placement) / 1024);
++ unsigned ram_size = gr_placement_current_ram_size(placement);
++ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024);
++}
++
++/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces.
++static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement,
++ int ind) {
++ if (!placement)
++ return;
++ int frameidx = 1;
++ foreach_pixmap(*placement, pixmap, {
++ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx,
++ pixmap);
++ ++frameidx;
++ });
++}
++
++/// Dumps the internal state (images and placements) to stderr.
++void gr_dump_state() {
++ FILE *file = stderr;
++ int ind = 0;
++ fprintf_ind(file, ind, "======= Graphics module state dump =======\n");
++ fprintf_ind(file, ind,
++ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu "
++ "sizeof(ImagePlacement) = %lu\n",
++ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement));
++ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images));
++ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count);
++ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n",
++ images_ram_size / 1024);
++ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n",
++ images_disk_size / 1024);
++
++ Milliseconds now = gr_now_ms();
++
++ int64_t images_ram_size_computed = 0;
++ int64_t images_disk_size_computed = 0;
++
++ Image *img = NULL;
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(images, img, {
++ fprintf_ind(file, ind, "----------------\n");
++ gr_dump_image_info(file, img, 0);
++ int64_t total_disk_size_computed = 0;
++ int total_duration_computed = 0;
++ foreach_frame(*img, frame, {
++ gr_dump_frame_info(file, frame, 4);
++ if (frame->image != img)
++ fprintf_ind(file, 8,
++ "ERROR: WRONG IMAGE POINTER\n");
++ total_duration_computed += frame->gap;
++ images_disk_size_computed += frame->disk_size;
++ total_disk_size_computed += frame->disk_size;
++ if (frame->imlib_object)
++ images_ram_size_computed +=
++ gr_frame_current_ram_size(frame);
++ });
++ if (img->total_disk_size != total_disk_size_computed) {
++ fprintf_ind(file, ind,
++ " ERROR: total_disk_size is %u, but "
++ "computed value is %ld\n",
++ img->total_disk_size, total_disk_size_computed);
++ }
++ if (img->total_duration != total_duration_computed) {
++ fprintf_ind(file, ind,
++ " ERROR: total_duration is %d, but computed "
++ "value is %d\n",
++ img->total_duration, total_duration_computed);
++ }
++ kh_foreach_value(img->placements, placement, {
++ gr_dump_placement_info(file, placement, 4);
++ if (placement->image != img)
++ fprintf_ind(file, 8,
++ "ERROR: WRONG IMAGE POINTER\n");
++ fprintf_ind(file, 8,
++ "Pixmaps:\n");
++ gr_dump_placement_pixmaps(file, placement, 12);
++ unsigned ram_size =
++ gr_placement_current_ram_size(placement);
++ images_ram_size_computed += ram_size;
++ });
++ });
++ if (images_ram_size != images_ram_size_computed) {
++ fprintf_ind(file, ind,
++ "ERROR: images_ram_size is %ld, but computed value "
++ "is %ld\n",
++ images_ram_size, images_ram_size_computed);
++ }
++ if (images_disk_size != images_disk_size_computed) {
++ fprintf_ind(file, ind,
++ "ERROR: images_disk_size is %ld, but computed value "
++ "is %ld\n",
++ images_disk_size, images_disk_size_computed);
++ }
++ fprintf_ind(file, ind, "===========================================\n");
++}
++
++/// Executes `command` with the name of the file corresponding to `image_id` as
++/// the argument. Executes xmessage with an error message on failure.
++// TODO: Currently we do this for the first frame only. Not sure what to do with
++// animations.
++void gr_preview_image(uint32_t image_id, const char *exec) {
++ char command[256];
++ size_t len;
++ Image *img = gr_find_image(image_id);
++ if (img) {
++ ImageFrame *frame = &img->first_frame;
++ char filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
++ if (frame->disk_size == 0) {
++ len = snprintf(command, 255,
++ "xmessage 'Image with id=%u is not "
++ "fully copied to %s'",
++ image_id, sanitized_filename(filename));
++ } else {
++ len = snprintf(command, 255, "%s %s &", exec,
++ sanitized_filename(filename));
++ }
++ } else {
++ len = snprintf(command, 255,
++ "xmessage 'Cannot find image with id=%u'",
++ image_id);
++ }
++ if (len > 255) {
++ fprintf(stderr, "error: command too long: %s\n", command);
++ snprintf(command, 255, "xmessage 'error: command too long'");
++ }
++ if (system(command) != 0) {
++ fprintf(stderr, "error: could not execute command %s\n",
++ command);
++ }
++}
++
++/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
++/// containing the information about an image and placement, and <st> is
++/// specified with `st_executable`.
++void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
++ uint32_t imgcol, uint32_t imgrow,
++ char is_classic_placeholder, int32_t diacritic_count,
++ char *st_executable) {
++ char filename[MAX_FILENAME_SIZE];
++ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id);
++ FILE *file = fopen(filename, "w");
++ if (!file) {
++ perror("fopen");
++ return;
++ }
++ // Basic information about the cell.
++ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id);
++ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id);
++ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow);
++ fprintf(file, "classic/unicode placeholder = %s\n",
++ is_classic_placeholder ? "classic" : "unicode");
++ fprintf(file, "original diacritic count = %d\n", diacritic_count);
++ // Information about the image and the placement.
++ Image *img = gr_find_image(image_id);
++ ImagePlacement *placement = gr_find_placement(img, placement_id);
++ gr_dump_image_info(file, img, 0);
++ gr_dump_placement_info(file, placement, 0);
++ if (img) {
++ fprintf(file, "Frames:\n");
++ foreach_frame(*img, frame, {
++ gr_dump_frame_info(file, frame, 4);
++ });
++ }
++ if (placement) {
++ fprintf(file, "Placement pixmaps:\n");
++ gr_dump_placement_pixmaps(file, placement, 4);
++ }
++ fclose(file);
++ char *argv[] = {st_executable, "-e", "less", filename, NULL};
++ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) {
++ perror("posix_spawnp");
++ return;
++ }
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Appending and displaying image rectangles.
++////////////////////////////////////////////////////////////////////////////////
++
++/// Displays debug information in the rectangle using colors col1 and col2.
++static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2,
++ const char *message) {
++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
++ Display *disp = imlib_context_get_display();
++ GC gc = XCreateGC(disp, buf, 0, NULL);
++ char info[MAX_INFO_LEN];
++ if (rect->placement_id)
++ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message,
++ rect->image_id, rect->placement_id,
++ rect->img_start_col, rect->img_end_col,
++ rect->img_start_row, rect->img_end_row);
++ else
++ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message,
++ rect->image_id, rect->img_start_col, rect->img_end_col,
++ rect->img_start_row, rect->img_end_row);
++ XSetForeground(disp, gc, col1);
++ XDrawString(disp, buf, gc, rect->screen_x_pix + 4,
++ rect->screen_y_pix + h_pix - 3, info, strlen(info));
++ XSetForeground(disp, gc, col2);
++ XDrawString(disp, buf, gc, rect->screen_x_pix + 2,
++ rect->screen_y_pix + h_pix - 5, info, strlen(info));
++ XFreeGC(disp, gc);
++}
++
++/// Draws a rectangle (bounding box) for debugging.
++static void gr_showrect(Drawable buf, ImageRect *rect) {
++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
++ Display *disp = imlib_context_get_display();
++ GC gc = XCreateGC(disp, buf, 0, NULL);
++ XSetForeground(disp, gc, 0xFF00FF00);
++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix,
++ w_pix - 1, h_pix - 1);
++ XSetForeground(disp, gc, 0xFFFF0000);
++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1,
++ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3);
++ XFreeGC(disp, gc);
++}
++
++/// Updates the next redraw time for the given row. Resizes the
++/// next_redraw_times array if needed.
++static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) {
++ if (next_redraw == 0)
++ return;
++ if (row >= kv_size(next_redraw_times)) {
++ size_t old_size = kv_size(next_redraw_times);
++ kv_a(Milliseconds, next_redraw_times, row);
++ for (size_t i = old_size; i <= row; ++i)
++ kv_A(next_redraw_times, i) = 0;
++ }
++ Milliseconds old_value = kv_A(next_redraw_times, row);
++ if (old_value == 0 || old_value > next_redraw)
++ kv_A(next_redraw_times, row) = next_redraw;
++}
++
++/// Draws the given part of an image.
++static void gr_drawimagerect(Drawable buf, ImageRect *rect) {
++ ImagePlacement *placement =
++ gr_find_image_and_placement(rect->image_id, rect->placement_id);
++ // If the image does not exist or image display is switched off, draw
++ // the bounding box.
++ if (!placement || !graphics_display_images) {
++ gr_showrect(buf, rect);
++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
++ return;
++ }
++
++ Image *img = placement->image;
++
++ if (img->last_redraw < drawing_start_time) {
++ // This is the first time we draw this image in this redraw
++ // cycle. Update the frame index we are going to display. Note
++ // that currently all image placements are synchronized.
++ int old_frame = img->current_frame;
++ gr_update_frame_index(img, drawing_start_time);
++ img->last_redraw = drawing_start_time;
++ }
++
++ // Adjust next redraw times for the rows of this image rect.
++ if (img->next_redraw) {
++ for (int row = rect->screen_y_row;
++ row <= rect->screen_y_row + rect->img_end_row -
++ rect->img_start_row - 1; ++row) {
++ gr_update_next_redraw_time(
++ row, img->next_redraw);
++ }
++ }
++
++ // Load the frame.
++ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw,
++ rect->ch);
++
++ // If the image couldn't be loaded, display the bounding box.
++ if (!pixmap) {
++ gr_showrect(buf, rect);
++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
++ return;
++ }
++
++ int src_x = rect->img_start_col * rect->cw;
++ int src_y = rect->img_start_row * rect->ch;
++ int width = (rect->img_end_col - rect->img_start_col) * rect->cw;
++ int height = (rect->img_end_row - rect->img_start_row) * rect->ch;
++ int dst_x = rect->screen_x_pix;
++ int dst_y = rect->screen_y_pix;
++
++ // Display the image.
++ Display *disp = imlib_context_get_display();
++ Visual *vis = imlib_context_get_visual();
++
++ // Create an xrender picture for the window.
++ XRenderPictFormat *win_format =
++ XRenderFindVisualFormat(disp, vis);
++ Picture window_pic =
++ XRenderCreatePicture(disp, buf, win_format, 0, NULL);
++
++ // If needed, invert the image pixmap. Note that this naive approach of
++ // inverting the pixmap is not entirely correct, because the pixmap is
++ // premultiplied. But the result is good enough to visually indicate
++ // selection.
++ if (rect->reverse) {
++ unsigned pixmap_w =
++ (unsigned)placement->cols * placement->scaled_cw;
++ unsigned pixmap_h =
++ (unsigned)placement->rows * placement->scaled_ch;
++ Pixmap invpixmap =
++ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32);
++ XGCValues gcv = {.function = GXcopyInverted};
++ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv);
++ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w,
++ pixmap_h, 0, 0);
++ XFreeGC(disp, gc);
++ pixmap = invpixmap;
++ }
++
++ // Create a picture for the image pixmap.
++ XRenderPictFormat *pic_format =
++ XRenderFindStandardFormat(disp, PictStandardARGB32);
++ Picture pixmap_pic =
++ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL);
++
++ // Composite the image onto the window. In the reverse mode we ignore
++ // the alpha channel of the image because the naive inversion above
++ // seems to invert the alpha channel as well.
++ int pictop = rect->reverse ? PictOpSrc : PictOpOver;
++ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic,
++ src_x, src_y, src_x, src_y, dst_x, dst_y, width,
++ height);
++
++ // Free resources
++ XRenderFreePicture(disp, pixmap_pic);
++ XRenderFreePicture(disp, window_pic);
++ if (rect->reverse)
++ XFreePixmap(disp, pixmap);
++
++ // In debug mode always draw bounding boxes and print info.
++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) {
++ gr_showrect(buf, rect);
++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
++ }
++}
++
++/// Removes the given image rectangle.
++static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); }
++
++/// Returns the bottom coordinate of the rect.
++static int gr_getrectbottom(ImageRect *rect) {
++ return rect->screen_y_pix +
++ (rect->img_end_row - rect->img_start_row) * rect->ch;
++}
++
++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
++void gr_start_drawing(Drawable buf, int cw, int ch) {
++ current_cw = cw;
++ current_ch = ch;
++ this_redraw_cycle_loaded_files = 0;
++ this_redraw_cycle_loaded_pixmaps = 0;
++ drawing_start_time = gr_now_ms();
++ imlib_context_set_drawable(buf);
++}
++
++/// Finish image drawing. This functions will draw all the rectangles left to
++/// draw.
++void gr_finish_drawing(Drawable buf) {
++ // Draw and then delete all known image rectangles.
++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
++ ImageRect *rect = &image_rects[i];
++ if (!rect->image_id)
++ continue;
++ gr_drawimagerect(buf, rect);
++ gr_freerect(rect);
++ }
++
++ // Compute the delay until the next redraw as the minimum of the next
++ // redraw delays for all rows.
++ Milliseconds drawing_end_time = gr_now_ms();
++ graphics_next_redraw_delay = INT_MAX;
++ for (int row = 0; row < kv_size(next_redraw_times); ++row) {
++ Milliseconds row_next_redraw = kv_A(next_redraw_times, row);
++ if (row_next_redraw > 0) {
++ int delay = MAX(graphics_animation_min_delay,
++ row_next_redraw - drawing_end_time);
++ graphics_next_redraw_delay =
++ MIN(graphics_next_redraw_delay, delay);
++ }
++ }
++
++ // In debug mode display additional info.
++ if (graphics_debug_mode) {
++ int milliseconds = drawing_end_time - drawing_start_time;
++
++ Display *disp = imlib_context_get_display();
++ GC gc = XCreateGC(disp, buf, 0, NULL);
++ const char *debug_mode_str =
++ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES
++ ? "(boxes shown) "
++ : "";
++ int redraw_delay = graphics_next_redraw_delay == INT_MAX
++ ? -1
++ : graphics_next_redraw_delay;
++ char info[MAX_INFO_LEN];
++ snprintf(info, MAX_INFO_LEN,
++ "%sRender time: %d ms ram %ld K disk %ld K count "
++ "%d cell %dx%d delay %d",
++ debug_mode_str, milliseconds, images_ram_size / 1024,
++ images_disk_size / 1024, kh_size(images), current_cw,
++ current_ch, redraw_delay);
++ XSetForeground(disp, gc, 0xFF000000);
++ XFillRectangle(disp, buf, gc, 0, 0, 600, 16);
++ XSetForeground(disp, gc, 0xFFFFFFFF);
++ XDrawString(disp, buf, gc, 0, 14, info, strlen(info));
++ XFreeGC(disp, gc);
++
++ if (milliseconds > 0) {
++ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n",
++ info, this_redraw_cycle_loaded_files,
++ this_redraw_cycle_loaded_pixmaps);
++ }
++ }
++
++ // Check the limits in case we have used too much ram for placements.
++ gr_check_limits();
++}
++
++// Add an image rectangle to the list of rectangles to draw.
++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
++ int img_start_col, int img_end_col, int img_start_row,
++ int img_end_row, int x_col, int y_row, int x_pix,
++ int y_pix, int cw, int ch, int reverse) {
++ current_cw = cw;
++ current_ch = ch;
++
++ ImageRect new_rect;
++ new_rect.image_id = image_id;
++ new_rect.placement_id = placement_id;
++ new_rect.img_start_col = img_start_col;
++ new_rect.img_end_col = img_end_col;
++ new_rect.img_start_row = img_start_row;
++ new_rect.img_end_row = img_end_row;
++ new_rect.screen_y_row = y_row;
++ new_rect.screen_x_pix = x_pix;
++ new_rect.screen_y_pix = y_pix;
++ new_rect.ch = ch;
++ new_rect.cw = cw;
++ new_rect.reverse = reverse;
++
++ // Display some red text in debug mode.
++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
++ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? ");
++
++ // If it's the empty image (image_id=0) or an empty rectangle, do
++ // nothing.
++ if (image_id == 0 || img_end_col - img_start_col <= 0 ||
++ img_end_row - img_start_row <= 0)
++ return;
++ // Try to find a rect to merge with.
++ ImageRect *free_rect = NULL;
++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
++ ImageRect *rect = &image_rects[i];
++ if (rect->image_id == 0) {
++ if (!free_rect)
++ free_rect = rect;
++ continue;
++ }
++ if (rect->image_id != image_id ||
++ rect->placement_id != placement_id || rect->cw != cw ||
++ rect->ch != ch || rect->reverse != reverse)
++ continue;
++ // We only support the case when the new stripe is added to the
++ // bottom of an existing rectangle and they are perfectly
++ // aligned.
++ if (rect->img_end_row == img_start_row &&
++ gr_getrectbottom(rect) == y_pix) {
++ if (rect->img_start_col == img_start_col &&
++ rect->img_end_col == img_end_col &&
++ rect->screen_x_pix == x_pix) {
++ rect->img_end_row = img_end_row;
++ return;
++ }
++ }
++ }
++ // If we haven't merged the new rect with any existing rect, and there
++ // is no free rect, we have to render one of the existing rects.
++ if (!free_rect) {
++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
++ ImageRect *rect = &image_rects[i];
++ if (!free_rect || gr_getrectbottom(free_rect) >
++ gr_getrectbottom(rect))
++ free_rect = rect;
++ }
++ gr_drawimagerect(buf, free_rect);
++ gr_freerect(free_rect);
++ }
++ // Start a new rectangle in `free_rect`.
++ *free_rect = new_rect;
++}
++
++/// Mark rows containing animations as dirty if it's time to redraw them. Must
++/// be called right after `gr_start_drawing`.
++void gr_mark_dirty_animations(int *dirty, int rows) {
++ if (rows < kv_size(next_redraw_times))
++ kv_size(next_redraw_times) = rows;
++ if (rows * 2 < kv_max(next_redraw_times))
++ kv_resize(Milliseconds, next_redraw_times, rows);
++ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) {
++ if (dirty[i]) {
++ kv_A(next_redraw_times, i) = 0;
++ continue;
++ }
++ Milliseconds next_update = kv_A(next_redraw_times, i);
++ if (next_update > 0 && next_update <= drawing_start_time) {
++ dirty[i] = 1;
++ kv_A(next_redraw_times, i) = 0;
++ }
++ }
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// Command parsing and handling.
++////////////////////////////////////////////////////////////////////////////////
++
++/// A parsed kitty graphics protocol command.
++typedef struct {
++ /// The command itself, without the 'G'.
++ char *command;
++ /// The payload (after ';').
++ char *payload;
++ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'.
++ char action;
++ /// 'q=', 1 to suppress OK response, 2 to suppress errors too.
++ int quiet;
++ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with
++ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to
++ /// 32-bit pixel data.
++ int format;
++ /// 'o=', may be 'z' for RFC 1950 ZLIB.
++ int compression;
++ /// 't=', may be 'f', 't' or 'd'.
++ char transmission_medium;
++ /// 'd='
++ char delete_specifier;
++ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'.
++ /// When 'a=f', this is the size of the frame rectangle when composed on
++ /// top of another frame.
++ int frame_pix_width, frame_pix_height;
++ /// 'x=', 'y=' - top-left corner of the source rectangle.
++ int src_pix_x, src_pix_y;
++ /// 'w=', 'h=' - width and height of the source rectangle.
++ int src_pix_width, src_pix_height;
++ /// 'r=', 'c='
++ int rows, columns;
++ /// 'i='
++ uint32_t image_id;
++ /// 'I='
++ uint32_t image_number;
++ /// 'p='
++ uint32_t placement_id;
++ /// 'm=', may be 0 or 1.
++ int more;
++ /// True if either 'm=0' or 'm=1' is specified.
++ char is_data_transmission;
++ /// True if turns out that this command is a continuation of a data
++ /// transmission and not the first one for this image. Populated by
++ /// `gr_handle_transmit_command`.
++ char is_direct_transmission_continuation;
++ /// 'S=', used to check the size of uploaded data.
++ int size;
++ /// 'U=', whether it's a virtual placement for Unicode placeholders.
++ int virtual;
++ /// 'C=', if true, do not move the cursor when displaying this placement
++ /// (non-virtual placements only).
++ char do_not_move_cursor;
++ // ---------------------------------------------------------------------
++ // Animation-related fields. Their keys often overlap with keys of other
++ // commands, so these make sense only if the action is 'a=f' (frame
++ // transmission) or 'a=a' (animation control).
++ //
++ // 'x=' and 'y=', the relative position of the frame image when it's
++ // composed on top of another frame.
++ int frame_dst_pix_x, frame_dst_pix_y;
++ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of
++ /// the background color or frame.
++ char replace_instead_of_blending;
++ /// 'Y=', the background color in the 0xRRGGBBAA format (still
++ /// transmitted as a decimal number).
++ uint32_t background_color;
++ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame.
++ int background_frame;
++ /// (Only for 'a=a'). 'c=', sets the index of the current frame.
++ int current_frame;
++ /// 'r=', the 1-based index of the frame to edit.
++ int edit_frame;
++ /// 'z=', the duration of the frame. Zero if not specified, negative if
++ /// the frame is gapless (i.e. skipped).
++ int gap;
++ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the
++ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop.
++ int animation_state;
++ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the
++ /// animation will loop. 1 to loop infinitely, N to loop N-1 times.
++ int loops;
++} GraphicsCommand;
++
++/// Replaces all non-printed characters in `str` with '?' and truncates the
++/// string to `max_size`, maybe inserting ellipsis at the end.
++static void sanitize_str(char *str, size_t max_size) {
++ assert(max_size >= 4);
++ for (size_t i = 0; i < max_size; ++i) {
++ unsigned c = str[i];
++ if (c == '\0')
++ return;
++ if (c >= 128 || !isprint(c))
++ str[i] = '?';
++ }
++ str[max_size - 1] = '\0';
++ str[max_size - 2] = '.';
++ str[max_size - 3] = '.';
++ str[max_size - 4] = '.';
++}
++
++/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be
++/// careful.
++static const char *sanitized_filename(const char *str) {
++ static char buf[MAX_FILENAME_SIZE];
++ strncpy(buf, str, sizeof(buf));
++ sanitize_str(buf, sizeof(buf));
++ return buf;
++}
++
++/// Creates a response to the current command in `graphics_command_result`.
++static void gr_createresponse(uint32_t image_id, uint32_t image_number,
++ uint32_t placement_id, const char *msg) {
++ if (!image_id && !image_number && !placement_id) {
++ // Nobody expects the response in this case, so just print it to
++ // stderr.
++ fprintf(stderr,
++ "error: No image id or image number or placement_id, "
++ "but still there is a response: %s\n",
++ msg);
++ return;
++ }
++ char *buf = graphics_command_result.response;
++ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN;
++ size_t written;
++ written = snprintf(buf, maxlen, "\033_G");
++ buf += written;
++ maxlen -= written;
++ if (image_id) {
++ written = snprintf(buf, maxlen, "i=%u,", image_id);
++ buf += written;
++ maxlen -= written;
++ }
++ if (image_number) {
++ written = snprintf(buf, maxlen, "I=%u,", image_number);
++ buf += written;
++ maxlen -= written;
++ }
++ if (placement_id) {
++ written = snprintf(buf, maxlen, "p=%u,", placement_id);
++ buf += written;
++ maxlen -= written;
++ }
++ buf[-1] = ';';
++ written = snprintf(buf, maxlen, "%s\033\\", msg);
++ buf += written;
++ maxlen -= written;
++ buf[-2] = '\033';
++ buf[-1] = '\\';
++}
++
++/// Creates the 'OK' response to the current command, unless suppressed or a
++/// non-final data transmission.
++static void gr_reportsuccess_cmd(GraphicsCommand *cmd) {
++ if (cmd->quiet < 1 && !cmd->more)
++ gr_createresponse(cmd->image_id, cmd->image_number,
++ cmd->placement_id, "OK");
++}
++
++/// Creates the 'OK' response to the current command (unless suppressed).
++static void gr_reportsuccess_frame(ImageFrame *frame) {
++ uint32_t id = frame->image->query_id ? frame->image->query_id
++ : frame->image->image_id;
++ if (frame->quiet < 1)
++ gr_createresponse(id, frame->image->image_number,
++ frame->image->initial_placement_id, "OK");
++}
++
++/// Creates an error response to the current command (unless suppressed).
++static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) {
++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
++ graphics_command_result.error = 1;
++ va_list args;
++ va_start(args, format);
++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
++ va_end(args);
++
++ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command);
++ if (cmd->quiet < 2)
++ gr_createresponse(cmd->image_id, cmd->image_number,
++ cmd->placement_id, errmsg);
++}
++
++/// Creates an error response to the current command (unless suppressed).
++static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) {
++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
++ graphics_command_result.error = 1;
++ va_list args;
++ va_start(args, format);
++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
++ va_end(args);
++
++ if (!frame) {
++ fprintf(stderr, "%s\n", errmsg);
++ gr_createresponse(0, 0, 0, errmsg);
++ } else {
++ uint32_t id = frame->image->query_id ? frame->image->query_id
++ : frame->image->image_id;
++ fprintf(stderr, "%s id=%u\n", errmsg, id);
++ if (frame->quiet < 2)
++ gr_createresponse(id, frame->image->image_number,
++ frame->image->initial_placement_id,
++ errmsg);
++ }
++}
++
++/// Loads an image and creates a success/failure response. Returns `frame`, or
++/// NULL if it's a query action and the image was deleted.
++static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) {
++ gr_load_imlib_object(frame);
++ if (!frame->imlib_object) {
++ gr_reporterror_frame(frame, "EBADF: could not load image");
++ } else {
++ gr_reportsuccess_frame(frame);
++ }
++ // If it was a query action, discard the image.
++ if (frame->image->query_id) {
++ gr_delete_image(frame->image);
++ return NULL;
++ }
++ return frame;
++}
++
++/// Creates an appropriate uploading failure response to the current command.
++static void gr_reportuploaderror(ImageFrame *frame) {
++ switch (frame->uploading_failure) {
++ case 0:
++ return;
++ case ERROR_CANNOT_OPEN_CACHED_FILE:
++ gr_reporterror_frame(frame,
++ "EIO: could not create a file for image");
++ break;
++ case ERROR_OVER_SIZE_LIMIT:
++ gr_reporterror_frame(
++ frame,
++ "EFBIG: the size of the uploaded image exceeded "
++ "the image size limit %u",
++ graphics_max_single_image_file_size);
++ break;
++ case ERROR_UNEXPECTED_SIZE:
++ gr_reporterror_frame(frame,
++ "EINVAL: the size of the uploaded image %u "
++ "doesn't match the expected size %u",
++ frame->disk_size, frame->expected_size);
++ break;
++ };
++}
++
++/// Displays a non-virtual placement. This functions records the information in
++/// `graphics_command_result`, the placeholder itself is created by the terminal
++/// after handling the current command in the graphics module.
++static void gr_display_nonvirtual_placement(ImagePlacement *placement) {
++ if (placement->virtual)
++ return;
++ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS)
++ return;
++ // Infer the placement size if needed.
++ gr_infer_placement_size_maybe(placement);
++ // Populate the information about the placeholder which will be created
++ // by the terminal.
++ graphics_command_result.create_placeholder = 1;
++ graphics_command_result.placeholder.image_id = placement->image->image_id;
++ graphics_command_result.placeholder.placement_id = placement->placement_id;
++ graphics_command_result.placeholder.columns = placement->cols;
++ graphics_command_result.placeholder.rows = placement->rows;
++ graphics_command_result.placeholder.do_not_move_cursor =
++ placement->do_not_move_cursor;
++ GR_LOG("Creating a placeholder for %u/%u %d x %d\n",
++ placement->image->image_id, placement->placement_id,
++ placement->cols, placement->rows);
++}
++
++/// Marks the rows that are occupied by the image as dirty.
++static void gr_schedule_image_redraw(Image *img) {
++ if (!img)
++ return;
++ gr_schedule_image_redraw_by_id(img->image_id);
++}
++
++/// Appends data from `payload` to the frame `frame` when using direct
++/// transmission. Note that we report errors only for the final command
++/// (`!more`) to avoid spamming the client. If the frame is not specified, use
++/// the image id and frame index we are currently uploading.
++static void gr_append_data(ImageFrame *frame, const char *payload, int more) {
++ if (!frame) {
++ Image *img = gr_find_image(current_upload_image_id);
++ frame = gr_get_frame(img, current_upload_frame_index);
++ GR_LOG("Appending data to image %u frame %d\n",
++ current_upload_image_id, current_upload_frame_index);
++ if (!img)
++ GR_LOG("ERROR: this image doesn't exist\n");
++ if (!frame)
++ GR_LOG("ERROR: this frame doesn't exist\n");
++ }
++ if (!more) {
++ current_upload_image_id = 0;
++ current_upload_frame_index = 0;
++ }
++ if (!frame) {
++ if (!more)
++ gr_reporterror_frame(NULL, "ENOENT: could not find the "
++ "image to append data to");
++ return;
++ }
++ if (frame->status != STATUS_UPLOADING) {
++ if (!more)
++ gr_reportuploaderror(frame);
++ return;
++ }
++
++ // Decode the data.
++ size_t data_size = 0;
++ char *data = gr_base64dec(payload, &data_size);
++
++ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size,
++ frame->disk_size + data_size);
++
++ // Do not append this data if the image exceeds the size limit.
++ if (frame->disk_size + data_size >
++ graphics_max_single_image_file_size ||
++ frame->expected_size > graphics_max_single_image_file_size) {
++ free(data);
++ gr_delete_imagefile(frame);
++ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
++ if (!more)
++ gr_reportuploaderror(frame);
++ return;
++ }
++
++ // If there is no open file corresponding to the image, create it.
++ if (!frame->open_file) {
++ gr_make_sure_tmpdir_exists();
++ char filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
++ FILE *file = fopen(filename, frame->disk_size ? "a" : "w");
++ if (!file) {
++ frame->status = STATUS_UPLOADING_ERROR;
++ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
++ if (!more)
++ gr_reportuploaderror(frame);
++ return;
++ }
++ frame->open_file = file;
++ }
++
++ // Write data to the file and update disk size variables.
++ fwrite(data, 1, data_size, frame->open_file);
++ free(data);
++ frame->disk_size += data_size;
++ frame->image->total_disk_size += data_size;
++ images_disk_size += data_size;
++ gr_touch_frame(frame);
++
++ if (more) {
++ current_upload_image_id = frame->image->image_id;
++ current_upload_frame_index = frame->index;
++ } else {
++ current_upload_image_id = 0;
++ current_upload_frame_index = 0;
++ // Close the file.
++ if (frame->open_file) {
++ fclose(frame->open_file);
++ frame->open_file = NULL;
++ }
++ frame->status = STATUS_UPLOADING_SUCCESS;
++ uint32_t placement_id = frame->image->default_placement;
++ if (frame->expected_size &&
++ frame->expected_size != frame->disk_size) {
++ // Report failure if the uploaded image size doesn't
++ // match the expected size.
++ frame->status = STATUS_UPLOADING_ERROR;
++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
++ gr_reportuploaderror(frame);
++ } else {
++ // Make sure to redraw all existing image instances.
++ gr_schedule_image_redraw(frame->image);
++ // Try to load the image into ram and report the result.
++ frame = gr_loadimage_and_report(frame);
++ // If there is a non-virtual image placement, we may
++ // need to display it.
++ if (frame && frame->index == 1) {
++ Image *img = frame->image;
++ ImagePlacement *placement = NULL;
++ kh_foreach_value(img->placements, placement, {
++ gr_display_nonvirtual_placement(placement);
++ });
++ }
++ }
++ }
++
++ // Check whether we need to delete old images.
++ gr_check_limits();
++}
++
++/// Finds the image either by id or by number specified in the command and sets
++/// the image_id of `cmd` if the image was found.
++static Image *gr_find_image_for_command(GraphicsCommand *cmd) {
++ if (cmd->image_id)
++ return gr_find_image(cmd->image_id);
++ Image *img = NULL;
++ // If the image number is not specified, we can't find the image, unless
++ // it's a put command, in which case we will try the last image.
++ if (cmd->image_number == 0 && cmd->action == 'p')
++ img = gr_find_image(last_image_id);
++ else
++ img = gr_find_image_by_number(cmd->image_number);
++ if (img)
++ cmd->image_id = img->image_id;
++ return img;
++}
++
++/// Creates a new image or a new frame in an existing image (depending on the
++/// command's action) and initializes its parameters from the command.
++static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) {
++ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 &&
++ cmd->compression != 0) {
++ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only "
++ "for raw pixel data (f=32 or f=24)");
++ // Even though we report an error, we still create an image.
++ }
++
++ Image *img = NULL;
++ if (cmd->action == 'f') {
++ // If it's a frame transmission action, there must be an
++ // existing image.
++ img = gr_find_image_for_command(cmd);
++ if (!img) {
++ gr_reporterror_cmd(cmd, "ENOENT: image not found");
++ return NULL;
++ }
++ } else {
++ // Otherwise create a new image object. If the action is `q`,
++ // we'll use random id instead of the one specified in the
++ // command.
++ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id;
++ img = gr_new_image(image_id);
++ if (!img)
++ return NULL;
++ if (cmd->action == 'q')
++ img->query_id = cmd->image_id;
++ else if (!cmd->image_id)
++ cmd->image_id = img->image_id;
++ // Set the image number.
++ img->image_number = cmd->image_number;
++ }
++
++ ImageFrame *frame = gr_append_new_frame(img);
++ // Initialize the frame.
++ frame->expected_size = cmd->size;
++ frame->format = cmd->format;
++ frame->compression = cmd->compression;
++ frame->background_color = cmd->background_color;
++ frame->background_frame_index = cmd->background_frame;
++ frame->gap = cmd->gap;
++ img->total_duration += frame->gap;
++ frame->blend = !cmd->replace_instead_of_blending;
++ frame->data_pix_width = cmd->frame_pix_width;
++ frame->data_pix_height = cmd->frame_pix_height;
++ if (cmd->action == 'f') {
++ frame->x = cmd->frame_dst_pix_x;
++ frame->y = cmd->frame_dst_pix_y;
++ }
++ // We save the quietness information in the frame because for direct
++ // transmission subsequent transmission command won't contain this info.
++ frame->quiet = cmd->quiet;
++ return frame;
++}
++
++/// Removes a file if it actually looks like a temporary file.
++static void gr_delete_tmp_file(const char *filename) {
++ if (strstr(filename, "tty-graphics-protocol") == NULL)
++ return;
++ if (strstr(filename, "/tmp/") != filename) {
++ const char *tmpdir = getenv("TMPDIR");
++ if (!tmpdir || !tmpdir[0] ||
++ strstr(filename, tmpdir) != filename)
++ return;
++ }
++ unlink(filename);
++}
++
++/// Handles a data transmission command.
++static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) {
++ // The default is direct transmission.
++ if (!cmd->transmission_medium)
++ cmd->transmission_medium = 'd';
++
++ // If neither id, nor image number is specified, and the transmission
++ // medium is 'd' (or unspecified), and there is an active direct upload,
++ // this is a continuation of the upload.
++ if (current_upload_image_id != 0 && cmd->image_id == 0 &&
++ cmd->image_number == 0 && cmd->transmission_medium == 'd') {
++ cmd->image_id = current_upload_image_id;
++ GR_LOG("No images id is specified, continuing uploading %u\n",
++ cmd->image_id);
++ }
++
++ ImageFrame *frame = NULL;
++ if (cmd->transmission_medium == 'f' ||
++ cmd->transmission_medium == 't') {
++ // File transmission.
++ // Create a new image or a new frame of an existing image.
++ frame = gr_new_image_or_frame_from_command(cmd);
++ if (!frame)
++ return NULL;
++ last_image_id = frame->image->image_id;
++ // Decode the filename.
++ char *original_filename = gr_base64dec(cmd->payload, NULL);
++ GR_LOG("Copying image %s\n",
++ sanitized_filename(original_filename));
++ // Stat the file and check that it's a regular file and not too
++ // big.
++ struct stat st;
++ int stat_res = stat(original_filename, &st);
++ const char *stat_error = NULL;
++ if (stat_res)
++ stat_error = strerror(errno);
++ else if (!S_ISREG(st.st_mode))
++ stat_error = "Not a regular file";
++ else if (st.st_size == 0)
++ stat_error = "The size of the file is zero";
++ else if (st.st_size > graphics_max_single_image_file_size)
++ stat_error = "The file is too large";
++ if (stat_error) {
++ gr_reporterror_cmd(cmd,
++ "EBADF: %s", stat_error);
++ fprintf(stderr, "Could not load the file %s\n",
++ sanitized_filename(original_filename));
++ frame->status = STATUS_UPLOADING_ERROR;
++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
++ } else {
++ gr_make_sure_tmpdir_exists();
++ // Build the filename for the cached copy of the file.
++ char cache_filename[MAX_FILENAME_SIZE];
++ gr_get_frame_filename(frame, cache_filename,
++ MAX_FILENAME_SIZE);
++ // We will create a symlink to the original file, and
++ // then copy the file to the temporary cache dir. We do
++ // this symlink trick mostly to be able to use cp for
++ // copying, and avoid escaping file name characters when
++ // calling system at the same time.
++ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0};
++ strcat(tmp_filename_symlink, cache_filename);
++ strcat(tmp_filename_symlink, ".sym");
++ char command[MAX_FILENAME_SIZE + 256];
++ size_t len =
++ snprintf(command, MAX_FILENAME_SIZE + 255,
++ "cp '%s' '%s'", tmp_filename_symlink,
++ cache_filename);
++ if (len > MAX_FILENAME_SIZE + 255 ||
++ symlink(original_filename, tmp_filename_symlink) ||
++ system(command) != 0) {
++ gr_reporterror_cmd(cmd,
++ "EBADF: could not copy the "
++ "image to the cache dir");
++ fprintf(stderr,
++ "Could not copy the image "
++ "%s (symlink %s) to %s",
++ sanitized_filename(original_filename),
++ tmp_filename_symlink, cache_filename);
++ frame->status = STATUS_UPLOADING_ERROR;
++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
++ } else {
++ // Get the file size of the copied file.
++ frame->status = STATUS_UPLOADING_SUCCESS;
++ frame->disk_size = st.st_size;
++ frame->image->total_disk_size += st.st_size;
++ images_disk_size += frame->disk_size;
++ if (frame->expected_size &&
++ frame->expected_size != frame->disk_size) {
++ // The file has unexpected size.
++ frame->status = STATUS_UPLOADING_ERROR;
++ frame->uploading_failure =
++ ERROR_UNEXPECTED_SIZE;
++ gr_reportuploaderror(frame);
++ } else {
++ // Everything seems fine, try to load
++ // and redraw existing instances.
++ gr_schedule_image_redraw(frame->image);
++ frame = gr_loadimage_and_report(frame);
++ }
++ }
++ // Delete the symlink.
++ unlink(tmp_filename_symlink);
++ // Delete the original file if it's temporary.
++ if (cmd->transmission_medium == 't')
++ gr_delete_tmp_file(original_filename);
++ }
++ free(original_filename);
++ gr_check_limits();
++ } else if (cmd->transmission_medium == 'd') {
++ // Direct transmission (default if 't' is not specified).
++ frame = gr_get_last_frame(gr_find_image_for_command(cmd));
++ if (frame && frame->status == STATUS_UPLOADING) {
++ // This is a continuation of the previous transmission.
++ cmd->is_direct_transmission_continuation = 1;
++ gr_append_data(frame, cmd->payload, cmd->more);
++ return frame;
++ }
++ // If no action is specified, it's not the first transmission
++ // command. If we couldn't find the image, something went wrong
++ // and we should just drop this command.
++ if (cmd->action == 0)
++ return NULL;
++ // Otherwise create a new image or frame structure.
++ frame = gr_new_image_or_frame_from_command(cmd);
++ if (!frame)
++ return NULL;
++ last_image_id = frame->image->image_id;
++ frame->status = STATUS_UPLOADING;
++ // Start appending data.
++ gr_append_data(frame, cmd->payload, cmd->more);
++ } else {
++ gr_reporterror_cmd(
++ cmd,
++ "EINVAL: transmission medium '%c' is not supported",
++ cmd->transmission_medium);
++ return NULL;
++ }
++
++ return frame;
++}
++
++/// Handles the 'put' command by creating a placement.
++static void gr_handle_put_command(GraphicsCommand *cmd) {
++ if (cmd->image_id == 0 && cmd->image_number == 0) {
++ gr_reporterror_cmd(cmd,
++ "EINVAL: neither image id nor image number "
++ "are specified or both are zero");
++ return;
++ }
++
++ // Find the image with the id or number.
++ Image *img = gr_find_image_for_command(cmd);
++ if (!img) {
++ gr_reporterror_cmd(cmd, "ENOENT: image not found");
++ return;
++ }
++
++ // Create a placement. If a placement with the same id already exists,
++ // it will be deleted. If the id is zero, a random id will be generated.
++ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id);
++ placement->virtual = cmd->virtual;
++ placement->src_pix_x = cmd->src_pix_x;
++ placement->src_pix_y = cmd->src_pix_y;
++ placement->src_pix_width = cmd->src_pix_width;
++ placement->src_pix_height = cmd->src_pix_height;
++ placement->cols = cmd->columns;
++ placement->rows = cmd->rows;
++ placement->do_not_move_cursor = cmd->do_not_move_cursor;
++
++ if (placement->virtual) {
++ placement->scale_mode = SCALE_MODE_CONTAIN;
++ } else if (placement->cols && placement->rows) {
++ // For classic placements the default is to stretch the image if
++ // both cols and rows are specified.
++ placement->scale_mode = SCALE_MODE_FILL;
++ } else if (placement->cols || placement->rows) {
++ // But if only one of them is specified, the default is to
++ // contain.
++ placement->scale_mode = SCALE_MODE_CONTAIN;
++ } else {
++ // If none of them are specified, the default is to use the
++ // original size.
++ placement->scale_mode = SCALE_MODE_NONE;
++ }
++
++ // Display the placement unless it's virtual.
++ gr_display_nonvirtual_placement(placement);
++
++ // Report success.
++ gr_reportsuccess_cmd(cmd);
++}
++
++/// Information about what to delete.
++typedef struct DeletionData {
++ uint32_t image_id;
++ uint32_t placement_id;
++ /// If true, delete the image object if there are no more placements.
++ char delete_image_if_no_ref;
++} DeletionData;
++
++/// The callback called for each cell to perform deletion.
++static int gr_deletion_callback(void *data, uint32_t image_id,
++ uint32_t placement_id, int col,
++ int row, char is_classic) {
++ DeletionData *del_data = data;
++ // Leave unicode placeholders alone.
++ if (!is_classic)
++ return 0;
++ if (del_data->image_id && del_data->image_id != image_id)
++ return 0;
++ if (del_data->placement_id && del_data->placement_id != placement_id)
++ return 0;
++ Image *img = gr_find_image(image_id);
++ // If the image is already deleted, just erase the placeholder.
++ if (!img)
++ return 1;
++ // Delete the placement.
++ if (placement_id)
++ gr_delete_placement(gr_find_placement(img, placement_id));
++ // Delete the image if image deletion is requested (uppercase delete
++ // specifier) and there are no more placements.
++ if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0)
++ gr_delete_image(img);
++ return 1;
++}
++
++/// Handles the delete command.
++static void gr_handle_delete_command(GraphicsCommand *cmd) {
++ DeletionData del_data = {0};
++ del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0;
++ char d = tolower(cmd->delete_specifier);
++
++ if (d == 'n') {
++ d = 'i';
++ Image *img = gr_find_image_by_number(cmd->image_number);
++ if (!img)
++ return;
++ del_data.image_id = img->image_id;
++ }
++
++ if (!d || d == 'a') {
++ // Delete all visible placements.
++ gr_for_each_image_cell(gr_deletion_callback, &del_data);
++ } else if (d == 'i') {
++ // Delete the specified image by image id and maybe placement
++ // id.
++ if (!del_data.image_id)
++ del_data.image_id = cmd->image_id;
++ if (!del_data.image_id) {
++ fprintf(stderr,
++ "ERROR: image id is not specified in the "
++ "delete command\n");
++ return;
++ }
++ del_data.placement_id = cmd->placement_id;
++ // NOTE: It's not very clear whether we should delete the image
++ // even if there are no _visible_ placements to delete. We do
++ // this because otherwise there is no way to delete an image
++ // with virtual placements in one command.
++ if (!del_data.placement_id && del_data.delete_image_if_no_ref)
++ gr_delete_image(gr_find_image(cmd->image_id));
++ gr_for_each_image_cell(gr_deletion_callback, &del_data);
++ } else {
++ fprintf(stderr,
++ "WARNING: unsupported value of the d key: '%c'. The "
++ "command is ignored.\n",
++ cmd->delete_specifier);
++ }
++}
++
++static void gr_handle_animation_control_command(GraphicsCommand *cmd) {
++ if (cmd->image_id == 0 && cmd->image_number == 0) {
++ gr_reporterror_cmd(cmd,
++ "EINVAL: neither image id nor image number "
++ "are specified or both are zero");
++ return;
++ }
++
++ // Find the image with the id or number.
++ Image *img = gr_find_image_for_command(cmd);
++ if (!img) {
++ gr_reporterror_cmd(cmd, "ENOENT: image not found");
++ return;
++ }
++
++ // Find the frame to edit, if requested.
++ ImageFrame *frame = NULL;
++ if (cmd->edit_frame)
++ frame = gr_get_frame(img, cmd->edit_frame);
++ if (cmd->edit_frame || cmd->gap) {
++ if (!frame) {
++ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found",
++ cmd->edit_frame);
++ return;
++ }
++ if (cmd->gap) {
++ img->total_duration -= frame->gap;
++ frame->gap = cmd->gap;
++ img->total_duration += frame->gap;
++ }
++ }
++
++ // Set animation-related parameters of the image.
++ if (cmd->current_frame)
++ img->current_frame = cmd->current_frame;
++ if (cmd->animation_state) {
++ if (cmd->animation_state == 1) {
++ img->animation_state = ANIMATION_STATE_STOPPED;
++ } else if (cmd->animation_state == 2) {
++ img->animation_state = ANIMATION_STATE_LOADING;
++ } else if (cmd->animation_state == 3) {
++ img->animation_state = ANIMATION_STATE_LOOPING;
++ } else {
++ gr_reporterror_cmd(
++ cmd, "EINVAL: invalid animation state: %d",
++ cmd->animation_state);
++ }
++ }
++ // TODO: Set the number of loops to cmd->loops
++
++ // Make sure we redraw all instances of the image.
++ gr_schedule_image_redraw(img);
++}
++
++/// Handles a command.
++static void gr_handle_command(GraphicsCommand *cmd) {
++ if (!cmd->image_id && !cmd->image_number) {
++ // If there is no image id or image number, nobody expects a
++ // response, so set quiet to 2.
++ cmd->quiet = 2;
++ }
++ ImageFrame *frame = NULL;
++ switch (cmd->action) {
++ case 0:
++ // If no action is specified, it may be a data transmission
++ // command if 'm=' is specified.
++ if (cmd->is_data_transmission) {
++ gr_handle_transmit_command(cmd);
++ break;
++ }
++ gr_reporterror_cmd(cmd, "EINVAL: no action specified");
++ break;
++ case 't':
++ case 'q':
++ case 'f':
++ // Transmit data. 'q' means query, which is basically the same
++ // as transmit, but the image is discarded, and the id is fake.
++ // 'f' appends a frame to an existing image.
++ gr_handle_transmit_command(cmd);
++ break;
++ case 'p':
++ // Display (put) the image.
++ gr_handle_put_command(cmd);
++ break;
++ case 'T':
++ // Transmit and display.
++ frame = gr_handle_transmit_command(cmd);
++ if (frame && !cmd->is_direct_transmission_continuation) {
++ gr_handle_put_command(cmd);
++ if (cmd->placement_id)
++ frame->image->initial_placement_id =
++ cmd->placement_id;
++ }
++ break;
++ case 'd':
++ gr_handle_delete_command(cmd);
++ break;
++ case 'a':
++ gr_handle_animation_control_command(cmd);
++ break;
++ default:
++ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c",
++ cmd->action);
++ return;
++ }
++}
++
++/// A partially parsed key-value pair.
++typedef struct KeyAndValue {
++ char *key_start;
++ char *val_start;
++ unsigned key_len, val_len;
++} KeyAndValue;
++
++/// Parses the value of a key and assigns it to the appropriate field of `cmd`.
++static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) {
++ char *key_start = kv->key_start;
++ char *key_end = key_start + kv->key_len;
++ char *value_start = kv->val_start;
++ char *value_end = value_start + kv->val_len;
++ // Currently all keys are one-character.
++ if (key_end - key_start != 1) {
++ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s",
++ key_end - key_start, key_start);
++ return;
++ }
++ long num = 0;
++ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' ||
++ *key_start == 'o') {
++ // Some keys have one-character values.
++ if (value_end - value_start != 1) {
++ gr_reporterror_cmd(
++ cmd,
++ "EINVAL: value of 'a', 't' or 'd' must be a "
++ "single char: %s",
++ key_start);
++ return;
++ }
++ } else {
++ // All the other keys have integer values.
++ char *num_end = NULL;
++ num = strtol(value_start, &num_end, 10);
++ if (num_end != value_end) {
++ gr_reporterror_cmd(
++ cmd, "EINVAL: could not parse number value: %s",
++ key_start);
++ return;
++ }
++ }
++ switch (*key_start) {
++ case 'a':
++ cmd->action = *value_start;
++ break;
++ case 't':
++ cmd->transmission_medium = *value_start;
++ break;
++ case 'd':
++ cmd->delete_specifier = *value_start;
++ break;
++ case 'q':
++ cmd->quiet = num;
++ break;
++ case 'f':
++ cmd->format = num;
++ if (num != 0 && num != 24 && num != 32 && num != 100) {
++ gr_reporterror_cmd(
++ cmd,
++ "EINVAL: unsupported format specification: %s",
++ key_start);
++ }
++ break;
++ case 'o':
++ cmd->compression = *value_start;
++ if (cmd->compression != 'z') {
++ gr_reporterror_cmd(cmd,
++ "EINVAL: unsupported compression "
++ "specification: %s",
++ key_start);
++ }
++ break;
++ case 's':
++ if (cmd->action == 'a')
++ cmd->animation_state = num;
++ else
++ cmd->frame_pix_width = num;
++ break;
++ case 'v':
++ if (cmd->action == 'a')
++ cmd->loops = num;
++ else
++ cmd->frame_pix_height = num;
++ break;
++ case 'i':
++ cmd->image_id = num;
++ break;
++ case 'I':
++ cmd->image_number = num;
++ break;
++ case 'p':
++ cmd->placement_id = num;
++ break;
++ case 'x':
++ cmd->src_pix_x = num;
++ cmd->frame_dst_pix_x = num;
++ break;
++ case 'y':
++ if (cmd->action == 'f')
++ cmd->frame_dst_pix_y = num;
++ else
++ cmd->src_pix_y = num;
++ break;
++ case 'w':
++ cmd->src_pix_width = num;
++ break;
++ case 'h':
++ cmd->src_pix_height = num;
++ break;
++ case 'c':
++ if (cmd->action == 'f')
++ cmd->background_frame = num;
++ else if (cmd->action == 'a')
++ cmd->current_frame = num;
++ else
++ cmd->columns = num;
++ break;
++ case 'r':
++ if (cmd->action == 'f' || cmd->action == 'a')
++ cmd->edit_frame = num;
++ else
++ cmd->rows = num;
++ break;
++ case 'm':
++ cmd->is_data_transmission = 1;
++ cmd->more = num;
++ break;
++ case 'S':
++ cmd->size = num;
++ break;
++ case 'U':
++ cmd->virtual = num;
++ break;
++ case 'X':
++ if (cmd->action == 'f')
++ cmd->replace_instead_of_blending = num;
++ else
++ break; /*ignore*/
++ break;
++ case 'Y':
++ if (cmd->action == 'f')
++ cmd->background_color = num;
++ else
++ break; /*ignore*/
++ break;
++ case 'z':
++ if (cmd->action == 'f' || cmd->action == 'a')
++ cmd->gap = num;
++ else
++ break; /*ignore*/
++ break;
++ case 'C':
++ cmd->do_not_move_cursor = num;
++ break;
++ default:
++ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s",
++ key_start);
++ return;
++ }
++}
++
++/// Parse and execute a graphics command. `buf` must start with 'G' and contain
++/// at least `len + 1` characters. Returns 1 on success.
++int gr_parse_command(char *buf, size_t len) {
++ if (buf[0] != 'G')
++ return 0;
++
++ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
++
++ global_command_counter++;
++ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
++
++ // Eat the 'G'.
++ ++buf;
++ --len;
++
++ GraphicsCommand cmd = {.command = buf};
++ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to
++ // parse the payload.
++ char state = 'k';
++ // An array of partially parsed key-value pairs.
++ KeyAndValue key_vals[32];
++ unsigned key_vals_count = 0;
++ char *key_start = buf;
++ char *key_end = NULL;
++ char *val_start = NULL;
++ char *val_end = NULL;
++ char *c = buf;
++ while (c - buf < len + 1) {
++ if (state == 'k') {
++ switch (*c) {
++ case ',':
++ case ';':
++ case '\0':
++ state = *c == ',' ? 'k' : 'p';
++ key_end = c;
++ gr_reporterror_cmd(
++ &cmd, "EINVAL: key without value: %s ",
++ key_start);
++ break;
++ case '=':
++ key_end = c;
++ state = 'v';
++ val_start = c + 1;
++ break;
++ default:
++ break;
++ }
++ } else if (state == 'v') {
++ switch (*c) {
++ case ',':
++ case ';':
++ case '\0':
++ state = *c == ',' ? 'k' : 'p';
++ val_end = c;
++ if (key_vals_count >=
++ sizeof(key_vals) / sizeof(*key_vals)) {
++ gr_reporterror_cmd(&cmd,
++ "EINVAL: too many "
++ "key-value pairs");
++ break;
++ }
++ key_vals[key_vals_count].key_start = key_start;
++ key_vals[key_vals_count].val_start = val_start;
++ key_vals[key_vals_count].key_len =
++ key_end - key_start;
++ key_vals[key_vals_count].val_len =
++ val_end - val_start;
++ ++key_vals_count;
++ key_start = c + 1;
++ break;
++ default:
++ break;
++ }
++ } else if (state == 'p') {
++ cmd.payload = c;
++ // break out of the loop, we don't check the payload
++ break;
++ }
++ ++c;
++ }
++
++ // Set the action key ('a=') first because we need it to disambiguate
++ // some keys. Also set 'i=' and 'I=' for better error reporting.
++ for (unsigned i = 0; i < key_vals_count; ++i) {
++ if (key_vals[i].key_len == 1) {
++ char *start = key_vals[i].key_start;
++ if (*start == 'a' || *start == 'i' || *start == 'I') {
++ gr_set_keyvalue(&cmd, &key_vals[i]);
++ break;
++ }
++ }
++ }
++ // Set the rest of the keys.
++ for (unsigned i = 0; i < key_vals_count; ++i)
++ gr_set_keyvalue(&cmd, &key_vals[i]);
++
++ if (!cmd.payload)
++ cmd.payload = buf + len;
++
++ if (cmd.payload && cmd.payload[0])
++ GR_LOG(" payload size: %ld\n", strlen(cmd.payload));
++
++ if (!graphics_command_result.error)
++ gr_handle_command(&cmd);
++
++ if (graphics_debug_mode) {
++ fprintf(stderr, "Response: ");
++ for (const char *resp = graphics_command_result.response;
++ *resp != '\0'; ++resp) {
++ if (isprint(*resp))
++ fprintf(stderr, "%c", *resp);
++ else
++ fprintf(stderr, "(0x%x)", *resp);
++ }
++ fprintf(stderr, "\n");
++ }
++
++ // Make sure that we suppress response if needed. Usually cmd.quiet is
++ // taken into account when creating the response, but it's not very
++ // reliable in the current implementation.
++ if (cmd.quiet) {
++ if (!graphics_command_result.error || cmd.quiet >= 2)
++ graphics_command_result.response[0] = '\0';
++ }
++
++ return 1;
++}
++
++////////////////////////////////////////////////////////////////////////////////
++// base64 decoding part is basically copied from st.c
++////////////////////////////////////////////////////////////////////////////////
++
++static const char gr_base64_digits[] = {
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
++ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2,
++ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
++ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
++ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
++ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
++
++static char gr_base64_getc(const char **src) {
++ while (**src && !isprint(**src))
++ (*src)++;
++ return **src ? *((*src)++) : '='; /* emulate padding if string ends */
++}
++
++char *gr_base64dec(const char *src, size_t *size) {
++ size_t in_len = strlen(src);
++ char *result, *dst;
++
++ result = dst = malloc((in_len + 3) / 4 * 3 + 1);
++ while (*src) {
++ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
++ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
++ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
++ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
++
++ if (a == -1 || b == -1)
++ break;
++
++ *dst++ = (a << 2) | ((b & 0x30) >> 4);
++ if (c == -1)
++ break;
++ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
++ if (d == -1)
++ break;
++ *dst++ = ((c & 0x03) << 6) | d;
++ }
++ *dst = '\0';
++ if (size) {
++ *size = dst - result;
++ }
++ return result;
++}
+diff --git a/graphics.h b/graphics.h
+new file mode 100644
+index 0000000..2e75dea
+--- /dev/null
++++ b/graphics.h
+@@ -0,0 +1,107 @@
++
++#include <stdint.h>
++#include <sys/types.h>
++#include <X11/Xlib.h>
++
++/// Initialize the graphics module.
++void gr_init(Display *disp, Visual *vis, Colormap cm);
++/// Deinitialize the graphics module.
++void gr_deinit();
++
++/// Add an image rectangle to a list if rectangles to draw. This function may
++/// actually draw some rectangles, or it may wait till more rectangles are
++/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`.
++/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the
++/// part of the image to draw (row/col indices are zero-based, ends are
++/// excluded).
++/// - `x_col` and `y_row` are the coordinates of the top-left corner of the
++/// image in the terminal grid.
++/// - `x_pix` and `y_pix` are the same but in pixels.
++/// - `reverse` indicates whether colors should be inverted.
++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
++ int img_start_col, int img_end_col, int img_start_row,
++ int img_end_row, int x_col, int y_row, int x_pix,
++ int y_pix, int cw, int ch, int reverse);
++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
++void gr_start_drawing(Drawable buf, int cw, int ch);
++/// Finish image drawing. This functions will draw all the rectangles left to
++/// draw.
++void gr_finish_drawing(Drawable buf);
++/// Mark rows containing animations as dirty if it's time to redraw them. Must
++/// be called right after `gr_start_drawing`.
++void gr_mark_dirty_animations(int *dirty, int rows);
++
++/// Parse and execute a graphics command. `buf` must start with 'G' and contain
++/// at least `len + 1` characters (including '\0'). Returns 1 on success.
++/// Additional informations is returned through `graphics_command_result`.
++int gr_parse_command(char *buf, size_t len);
++
++/// Executes `command` with the name of the file corresponding to `image_id` as
++/// the argument. Executes xmessage with an error message on failure.
++void gr_preview_image(uint32_t image_id, const char *command);
++
++/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
++/// containing the information about an image and placement, and <st> is
++/// specified with `st_executable`.
++void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
++ uint32_t imgcol, uint32_t imgrow,
++ char is_classic_placeholder, int32_t diacritic_count,
++ char *st_executable);
++
++/// Dumps the internal state (images and placements) to stderr.
++void gr_dump_state();
++
++/// Unloads images to reduce RAM usage.
++void gr_unload_images_to_reduce_ram();
++
++/// Executes `callback` for each image cell. `callback` may return 1 to erase
++/// the cell or 0 to keep it. This function is implemented in `st.c`.
++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
++ uint32_t placement_id, int col,
++ int row, char is_classic),
++ void *data);
++
++/// Marks all the rows containing the image with `image_id` as dirty.
++void gr_schedule_image_redraw_by_id(uint32_t image_id);
++
++typedef enum {
++ GRAPHICS_DEBUG_NONE = 0,
++ GRAPHICS_DEBUG_LOG = 1,
++ GRAPHICS_DEBUG_LOG_AND_BOXES = 2,
++} GraphicsDebugMode;
++
++/// Print additional information, draw bounding bounding boxes, etc.
++extern GraphicsDebugMode graphics_debug_mode;
++
++/// Whether to display images or just draw bounding boxes.
++extern char graphics_display_images;
++
++/// The time in milliseconds until the next redraw to update animations.
++/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`.
++extern int graphics_next_redraw_delay;
++
++#define MAX_GRAPHICS_RESPONSE_LEN 256
++
++/// A structure representing the result of a graphics command.
++typedef struct {
++ /// Indicates if the terminal needs to be redrawn.
++ char redraw;
++ /// The response of the command that should be sent back to the client
++ /// (may be empty if the quiet flag is set).
++ char response[MAX_GRAPHICS_RESPONSE_LEN];
++ /// Whether there was an error executing this command (not very useful,
++ /// the response must be sent back anyway).
++ char error;
++ /// Whether the terminal has to create a placeholder for a non-virtual
++ /// placement.
++ char create_placeholder;
++ /// The placeholder that needs to be created.
++ struct {
++ uint32_t rows, columns;
++ uint32_t image_id, placement_id;
++ char do_not_move_cursor;
++ } placeholder;
++} GraphicsCommandResult;
++
++/// The result of a graphics command.
++extern GraphicsCommandResult graphics_command_result;
+diff --git a/icat-mini.sh b/icat-mini.sh
+new file mode 100755
+index 0000000..0a8ebab
+--- /dev/null
++++ b/icat-mini.sh
+@@ -0,0 +1,801 @@
++#!/bin/sh
++
++# vim: shiftwidth=4
++
++script_name="$(basename "$0")"
++
++short_help="Usage: $script_name [OPTIONS] <image_file>
++
++This is a script to display images in the terminal using the kitty graphics
++protocol with Unicode placeholders. It is very basic, please use something else
++if you have alternatives.
++
++Options:
++ -h Show this help.
++ -s SCALE The scale of the image, may be floating point.
++ -c N, --cols N The number of columns.
++ -r N, --rows N The number of rows.
++ --max-cols N The maximum number of columns.
++ --max-rows N The maximum number of rows.
++ --cell-size WxH The cell size in pixels.
++ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'.
++ --speed SPEED The multiplier for the animation speed (float).
++"
++
++# Exit the script on keyboard interrupt
++trap "echo 'icat-mini was interrupted' >&2; exit 1" INT
++
++cols=""
++rows=""
++file=""
++tty="/dev/tty"
++uploading_method="auto"
++cell_size=""
++scale=1
++max_cols=""
++max_rows=""
++speed=""
++
++# Parse the command line.
++while [ $# -gt 0 ]; do
++ case "$1" in
++ -c|--columns|--cols)
++ cols="$2"
++ shift 2
++ ;;
++ -r|--rows|-l|--lines)
++ rows="$2"
++ shift 2
++ ;;
++ -s|--scale)
++ scale="$2"
++ shift 2
++ ;;
++ -h|--help)
++ echo "$short_help"
++ exit 0
++ ;;
++ -m|--upload-method|--uploading-method)
++ uploading_method="$2"
++ shift 2
++ ;;
++ --cell-size)
++ cell_size="$2"
++ shift 2
++ ;;
++ --max-cols)
++ max_cols="$2"
++ shift 2
++ ;;
++ --max-rows)
++ max_rows="$2"
++ shift 2
++ ;;
++ --speed)
++ speed="$2"
++ shift 2
++ ;;
++ --)
++ file="$2"
++ shift 2
++ ;;
++ -*)
++ echo "Unknown option: $1" >&2
++ exit 1
++ ;;
++ *)
++ if [ -n "$file" ]; then
++ echo "Multiple image files are not supported: $file and $1" >&2
++ exit 1
++ fi
++ file="$1"
++ shift
++ ;;
++ esac
++done
++
++file="$(realpath "$file")"
++
++#####################################################################
++# Adjust the terminal state
++#####################################################################
++
++stty_orig="$(stty -g < "$tty")"
++stty -echo < "$tty"
++# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some
++# horrible issues otherwise.
++stty susp undef < "$tty"
++stty -icanon < "$tty"
++
++restore_echo() {
++ [ -n "$stty_orig" ] || return
++ stty $stty_orig < "$tty"
++}
++
++trap restore_echo EXIT TERM
++
++#####################################################################
++# Detect imagemagick
++#####################################################################
++
++# If there is the 'magick' command, use it instead of separate 'convert' and
++# 'identify' commands.
++if command -v magick > /dev/null; then
++ identify="magick identify"
++ convert="magick"
++else
++ identify="identify"
++ convert="convert"
++fi
++
++#####################################################################
++# Detect tmux
++#####################################################################
++
++# Check if we are inside tmux.
++inside_tmux=""
++if [ -n "$TMUX" ]; then
++ case "$TERM" in
++ *tmux*|*screen*)
++ inside_tmux=1
++ ;;
++ esac
++fi
++
++#####################################################################
++# Compute the number of rows and columns
++#####################################################################
++
++is_pos_int() {
++ if [ -z "$1" ]; then
++ return 1 # false
++ fi
++ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then
++ if [ "$1" -gt 0 ]; then
++ return 0 # true
++ fi
++ fi
++ return 1 # false
++}
++
++if [ -n "$cols" ] || [ -n "$rows" ]; then
++ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then
++ echo "You can't specify both max-cols/rows and cols/rows" >&2
++ exit 1
++ fi
++fi
++
++# Get the max number of cols and rows.
++[ -n "$max_cols" ] || max_cols="$(tput cols)"
++[ -n "$max_rows" ] || max_rows="$(tput lines)"
++if [ "$max_rows" -gt 255 ]; then
++ max_rows=255
++fi
++
++python_ioctl_command="import array, fcntl, termios
++buf = array.array('H', [0, 0, 0, 0])
++fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
++print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))"
++
++# Get the cell size in pixels if either cols or rows are not specified.
++if [ -z "$cols" ] || [ -z "$rows" ]; then
++ cell_width=""
++ cell_height=""
++ # If the cell size is specified, use it.
++ if [ -n "$cell_size" ]; then
++ cell_width="${cell_size%x*}"
++ cell_height="${cell_size#*x}"
++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
++ echo "Invalid cell size: $cell_size" >&2
++ exit 1
++ fi
++ fi
++ # Otherwise try to use TIOCGWINSZ ioctl via python.
++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
++ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)"
++ cell_width="${cell_size_ioctl% *}"
++ cell_height="${cell_size_ioctl#* }"
++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
++ cell_width=""
++ cell_height=""
++ fi
++ fi
++ # If it didn't work, try to use csi XTWINOPS.
++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
++ if [ -n "$inside_tmux" ]; then
++ printf '\ePtmux;\e\e[16t\e\\' >> "$tty"
++ else
++ printf '\e[16t' >> "$tty"
++ fi
++ # The expected response will look like ^[[6;<height>;<width>t
++ term_response=""
++ while true; do
++ char=$(dd bs=1 count=1 <"$tty" 2>/dev/null)
++ if [ "$char" = "t" ]; then
++ break
++ fi
++ term_response="$term_response$char"
++ done
++ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)"
++ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)"
++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
++ cell_width=8
++ cell_height=16
++ fi
++ fi
++fi
++
++# Compute a formula with bc and round to the nearest integer.
++bc_round() {
++ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)"
++}
++
++# Compute the number of rows and columns of the image.
++if [ -z "$cols" ] || [ -z "$rows" ]; then
++ # Get the size of the image and its resolution. If it's an animation, use
++ # the first frame.
++ format_output="$($identify -format '%w %h\n' "$file" | head -1)"
++ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)"
++ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)"
++ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then
++ echo "Couldn't get image size from identify: $format_output" >&2
++ echo >&2
++ exit 1
++ fi
++ opt_cols_expr="(${scale}*${img_width}/${cell_width})"
++ opt_rows_expr="(${scale}*${img_height}/${cell_height})"
++ if [ -z "$cols" ] && [ -z "$rows" ]; then
++ # If columns and rows are not specified, compute the optimal values
++ # using the information about rows and columns per inch.
++ cols="$(bc_round "$opt_cols_expr")"
++ rows="$(bc_round "$opt_rows_expr")"
++ # Make sure that automatically computed rows and columns are within some
++ # sane limits
++ if [ "$cols" -gt "$max_cols" ]; then
++ rows="$(bc_round "$rows * $max_cols / $cols")"
++ cols="$max_cols"
++ fi
++ if [ "$rows" -gt "$max_rows" ]; then
++ cols="$(bc_round "$cols * $max_rows / $rows")"
++ rows="$max_rows"
++ fi
++ elif [ -z "$cols" ]; then
++ # If only one dimension is specified, compute the other one to match the
++ # aspect ratio as close as possible.
++ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")"
++ elif [ -z "$rows" ]; then
++ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")"
++ fi
++
++ if [ "$cols" -lt 1 ]; then
++ cols=1
++ fi
++ if [ "$rows" -lt 1 ]; then
++ rows=1
++ fi
++fi
++
++#####################################################################
++# Generate an image id
++#####################################################################
++
++image_id=""
++while [ -z "$image_id" ]; do
++ image_id="$(shuf -i 16777217-4294967295 -n 1)"
++ # Check that the id requires 24-bit fg colors.
++ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then
++ image_id=""
++ fi
++done
++
++#####################################################################
++# Uploading the image
++#####################################################################
++
++# Choose the uploading method
++if [ "$uploading_method" = "auto" ]; then
++ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then
++ uploading_method="direct"
++ else
++ uploading_method="file"
++ fi
++fi
++
++# Functions to emit the start and the end of a graphics command.
++if [ -n "$inside_tmux" ]; then
++ # If we are in tmux we have to wrap the command in Ptmux.
++ graphics_command_start='\ePtmux;\e\e_G'
++ graphics_command_end='\e\e\\\e\\'
++else
++ graphics_command_start='\e_G'
++ graphics_command_end='\e\\'
++fi
++
++start_gr_command() {
++ printf "$graphics_command_start" >> "$tty"
++}
++end_gr_command() {
++ printf "$graphics_command_end" >> "$tty"
++}
++
++# Send a graphics command with the correct start and end
++gr_command() {
++ start_gr_command
++ printf '%s' "$1" >> "$tty"
++ end_gr_command
++}
++
++# Send an uploading command. Usage: gr_upload <action> <command> <file>
++# Where <action> is a part of command that specifies the action, it will be
++# repeated for every chunk (if the method is direct), and <command> is the rest
++# of the command that specifies the image parameters. <action> and <command>
++# must not include the transmission method or ';'.
++# Example:
++# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
++gr_upload() {
++ arg_action="$1"
++ arg_command="$2"
++ arg_file="$3"
++ if [ "$uploading_method" = "file" ]; then
++ # base64-encode the filename
++ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0)
++ gr_command "${arg_action},${arg_command},t=f;${encoded_filename}"
++ fi
++ if [ "$uploading_method" = "direct" ]; then
++ # Create a temporary directory to store the chunked image.
++ chunkdir="$(mktemp -d)"
++ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then
++ echo "Can't create a temp dir" >&2
++ exit 1
++ fi
++ # base64-encode the file and split it into chunks. The size of each
++ # graphics command shouldn't be more than 4096, so we set the size of an
++ # encoded chunk to be 3968, slightly less than that.
++ chunk_size=3968
++ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_"
++
++ # Issue a command indicating that we want to start data transmission for
++ # a new image.
++ gr_command "${arg_action},${arg_command},t=d,m=1"
++
++ # Transmit chunks.
++ for chunk in "$chunkdir/chunk_"*; do
++ start_gr_command
++ printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty"
++ cat "$chunk" >> "$tty"
++ end_gr_command
++ rm "$chunk"
++ done
++
++ # Tell the terminal that we are done.
++ gr_command "${arg_action},i=$image_id,m=0"
++
++ # Remove the temporary directory.
++ rmdir "$chunkdir"
++ fi
++}
++
++delayed_frame_dir_cleanup() {
++ arg_frame_dir="$1"
++ sleep 2
++ if [ -n "$arg_frame_dir" ]; then
++ for frame in "$arg_frame_dir"/frame_*.png; do
++ rm "$frame"
++ done
++ rmdir "$arg_frame_dir"
++ fi
++}
++
++upload_image_and_print_placeholder() {
++ # Check if the file is an animation.
++ frame_count=$($identify -format '%n\n' "$file" | head -n 1)
++ if [ "$frame_count" -gt 1 ]; then
++ # The file is an animation, decompose into frames and upload each frame.
++ frame_dir="$(mktemp -d)"
++ frame_dir="$HOME/temp/frames${frame_dir}"
++ mkdir -p "$frame_dir"
++ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then
++ echo "Can't create a temp dir for frames" >&2
++ exit 1
++ fi
++
++ # Decompose the animation into separate frames.
++ $convert "$file" -coalesce "$frame_dir/frame_%06d.png"
++
++ # Get all frame delays at once, in centiseconds, as a space-separated
++ # string.
++ delays=$($identify -format "%T " "$file")
++
++ frame_number=1
++ for frame in "$frame_dir"/frame_*.png; do
++ # Read the delay for the current frame and convert it from
++ # centiseconds to milliseconds.
++ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number")
++ delay=$((delay * 10))
++ # If the delay is 0, set it to 100ms.
++ if [ "$delay" -eq 0 ]; then
++ delay=100
++ fi
++
++ if [ -n "$speed" ]; then
++ delay=$(bc_round "$delay / $speed")
++ fi
++
++ if [ "$frame_number" -eq 1 ]; then
++ # Upload the first frame with a=T
++ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame"
++ # Set the delay for the first frame and also play the animation
++ # in loading mode (s=2).
++ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}"
++ # Print the placeholder after the first frame to reduce the wait
++ # time.
++ print_placeholder
++ else
++ # Upload subsequent frames with a=f
++ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame"
++ fi
++
++ frame_number=$((frame_number + 1))
++ done
++
++ # Play the animation in loop mode (s=3).
++ gr_command "a=a,v=1,s=3,i=${image_id}"
++
++ # Remove the temporary directory, but do it in the background with a
++ # delay to avoid removing files before they are loaded by the terminal.
++ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null &
++ else
++ # The file is not an animation, upload it directly
++ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
++ # Print the placeholder
++ print_placeholder
++ fi
++}
++
++#####################################################################
++# Printing the image placeholder
++#####################################################################
++
++print_placeholder() {
++ # Each line starts with the escape sequence to set the foreground color to
++ # the image id.
++ blue="$(expr "$image_id" % 256 )"
++ green="$(expr \( "$image_id" / 256 \) % 256 )"
++ red="$(expr \( "$image_id" / 65536 \) % 256 )"
++ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")"
++ line_end="$(printf "\e[39;m")"
++
++ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )"
++ eval "id_diacritic=\$d${id4th}"
++
++ # Reset the brush state, mostly to reset the underline color.
++ printf "\e[0m"
++
++ # Fill the output with characters representing the image
++ for y in $(seq 0 "$(expr "$rows" - 1)"); do
++ eval "row_diacritic=\$d${y}"
++ printf '%s' "$line_start"
++ for x in $(seq 0 "$(expr "$cols" - 1)"); do
++ eval "col_diacritic=\$d${x}"
++ # Note that when $x is out of bounds, the column diacritic will
++ # be empty, meaning that the column should be guessed by the
++ # terminal.
++ if [ "$x" -ge "$num_diacritics" ]; then
++ printf '%s' "${placeholder}${row_diacritic}"
++ else
++ printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}"
++ fi
++ done
++ printf '%s\n' "$line_end"
++ done
++
++ printf "\e[0m"
++}
++
++d0="̅"
++d1="̍"
++d2="̎"
++d3="̐"
++d4="̒"
++d5="̽"
++d6="̾"
++d7="̿"
++d8="͆"
++d9="͊"
++d10="͋"
++d11="͌"
++d12="͐"
++d13="͑"
++d14="͒"
++d15="͗"
++d16="͛"
++d17="ͣ"
++d18="ͤ"
++d19="ͥ"
++d20="ͦ"
++d21="ͧ"
++d22="ͨ"
++d23="ͩ"
++d24="ͪ"
++d25="ͫ"
++d26="ͬ"
++d27="ͭ"
++d28="ͮ"
++d29="ͯ"
++d30="҃"
++d31="҄"
++d32="҅"
++d33="҆"
++d34="҇"
++d35="֒"
++d36="֓"
++d37="֔"
++d38="֕"
++d39="֗"
++d40="֘"
++d41="֙"
++d42="֜"
++d43="֝"
++d44="֞"
++d45="֟"
++d46="֠"
++d47="֡"
++d48="֨"
++d49="֩"
++d50="֫"
++d51="֬"
++d52="֯"
++d53="ׄ"
++d54="ؐ"
++d55="ؑ"
++d56="ؒ"
++d57="ؓ"
++d58="ؔ"
++d59="ؕ"
++d60="ؖ"
++d61="ؗ"
++d62="ٗ"
++d63="٘"
++d64="ٙ"
++d65="ٚ"
++d66="ٛ"
++d67="ٝ"
++d68="ٞ"
++d69="ۖ"
++d70="ۗ"
++d71="ۘ"
++d72="ۙ"
++d73="ۚ"
++d74="ۛ"
++d75="ۜ"
++d76="۟"
++d77="۠"
++d78="ۡ"
++d79="ۢ"
++d80="ۤ"
++d81="ۧ"
++d82="ۨ"
++d83="۫"
++d84="۬"
++d85="ܰ"
++d86="ܲ"
++d87="ܳ"
++d88="ܵ"
++d89="ܶ"
++d90="ܺ"
++d91="ܽ"
++d92="ܿ"
++d93="݀"
++d94="݁"
++d95="݃"
++d96="݅"
++d97="݇"
++d98="݉"
++d99="݊"
++d100="߫"
++d101="߬"
++d102="߭"
++d103="߮"
++d104="߯"
++d105="߰"
++d106="߱"
++d107="߳"
++d108="ࠖ"
++d109="ࠗ"
++d110="࠘"
++d111="࠙"
++d112="ࠛ"
++d113="ࠜ"
++d114="ࠝ"
++d115="ࠞ"
++d116="ࠟ"
++d117="ࠠ"
++d118="ࠡ"
++d119="ࠢ"
++d120="ࠣ"
++d121="ࠥ"
++d122="ࠦ"
++d123="ࠧ"
++d124="ࠩ"
++d125="ࠪ"
++d126="ࠫ"
++d127="ࠬ"
++d128="࠭"
++d129="॑"
++d130="॓"
++d131="॔"
++d132="ྂ"
++d133="ྃ"
++d134="྆"
++d135="྇"
++d136="፝"
++d137="፞"
++d138="፟"
++d139="៝"
++d140="᤺"
++d141="ᨗ"
++d142="᩵"
++d143="᩶"
++d144="᩷"
++d145="᩸"
++d146="᩹"
++d147="᩺"
++d148="᩻"
++d149="᩼"
++d150="᭫"
++d151="᭭"
++d152="᭮"
++d153="᭯"
++d154="᭰"
++d155="᭱"
++d156="᭲"
++d157="᭳"
++d158="᳐"
++d159="᳑"
++d160="᳒"
++d161="᳚"
++d162="᳛"
++d163="᳠"
++d164="᷀"
++d165="᷁"
++d166="᷃"
++d167="᷄"
++d168="᷅"
++d169="᷆"
++d170="᷇"
++d171="᷈"
++d172="᷉"
++d173="᷋"
++d174="᷌"
++d175="᷑"
++d176="᷒"
++d177="ᷓ"
++d178="ᷔ"
++d179="ᷕ"
++d180="ᷖ"
++d181="ᷗ"
++d182="ᷘ"
++d183="ᷙ"
++d184="ᷚ"
++d185="ᷛ"
++d186="ᷜ"
++d187="ᷝ"
++d188="ᷞ"
++d189="ᷟ"
++d190="ᷠ"
++d191="ᷡ"
++d192="ᷢ"
++d193="ᷣ"
++d194="ᷤ"
++d195="ᷥ"
++d196="ᷦ"
++d197="᷾"
++d198="⃐"
++d199="⃑"
++d200="⃔"
++d201="⃕"
++d202="⃖"
++d203="⃗"
++d204="⃛"
++d205="⃜"
++d206="⃡"
++d207="⃧"
++d208="⃩"
++d209="⃰"
++d210="⳯"
++d211="⳰"
++d212="⳱"
++d213="ⷠ"
++d214="ⷡ"
++d215="ⷢ"
++d216="ⷣ"
++d217="ⷤ"
++d218="ⷥ"
++d219="ⷦ"
++d220="ⷧ"
++d221="ⷨ"
++d222="ⷩ"
++d223="ⷪ"
++d224="ⷫ"
++d225="ⷬ"
++d226="ⷭ"
++d227="ⷮ"
++d228="ⷯ"
++d229="ⷰ"
++d230="ⷱ"
++d231="ⷲ"
++d232="ⷳ"
++d233="ⷴ"
++d234="ⷵ"
++d235="ⷶ"
++d236="ⷷ"
++d237="ⷸ"
++d238="ⷹ"
++d239="ⷺ"
++d240="ⷻ"
++d241="ⷼ"
++d242="ⷽ"
++d243="ⷾ"
++d244="ⷿ"
++d245="꙯"
++d246="꙼"
++d247="꙽"
++d248="꛰"
++d249="꛱"
++d250="꣠"
++d251="꣡"
++d252="꣢"
++d253="꣣"
++d254="꣤"
++d255="꣥"
++d256="꣦"
++d257="꣧"
++d258="꣨"
++d259="꣩"
++d260="꣪"
++d261="꣫"
++d262="꣬"
++d263="꣭"
++d264="꣮"
++d265="꣯"
++d266="꣰"
++d267="꣱"
++d268="ꪰ"
++d269="ꪲ"
++d270="ꪳ"
++d271="ꪷ"
++d272="ꪸ"
++d273="ꪾ"
++d274="꪿"
++d275="꫁"
++d276="︠"
++d277="︡"
++d278="︢"
++d279="︣"
++d280="︤"
++d281="︥"
++d282="︦"
++d283="𐨏"
++d284="𐨸"
++d285="𝆅"
++d286="𝆆"
++d287="𝆇"
++d288="𝆈"
++d289="𝆉"
++d290="𝆪"
++d291="𝆫"
++d292="𝆬"
++d293="𝆭"
++d294="𝉂"
++d295="𝉃"
++d296="𝉄"
++
++num_diacritics="297"
++
++placeholder="􎻮"
++
++#####################################################################
++# Upload the image and print the placeholder
++#####################################################################
++
++upload_image_and_print_placeholder
+diff --git a/khash.h b/khash.h
+new file mode 100644
+index 0000000..f75f347
+--- /dev/null
++++ b/khash.h
+@@ -0,0 +1,627 @@
++/* The MIT License
++
++ Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
++
++ Permission is hereby granted, free of charge, to any person obtaining
++ a copy of this software and associated documentation files (the
++ "Software"), to deal in the Software without restriction, including
++ without limitation the rights to use, copy, modify, merge, publish,
++ distribute, sublicense, and/or sell copies of the Software, and to
++ permit persons to whom the Software is furnished to do so, subject to
++ the following conditions:
++
++ The above copyright notice and this permission notice shall be
++ included in all copies or substantial portions of the Software.
++
++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++ SOFTWARE.
++*/
++
++/*
++ An example:
++
++#include "khash.h"
++KHASH_MAP_INIT_INT(32, char)
++int main() {
++ int ret, is_missing;
++ khiter_t k;
++ khash_t(32) *h = kh_init(32);
++ k = kh_put(32, h, 5, &ret);
++ kh_value(h, k) = 10;
++ k = kh_get(32, h, 10);
++ is_missing = (k == kh_end(h));
++ k = kh_get(32, h, 5);
++ kh_del(32, h, k);
++ for (k = kh_begin(h); k != kh_end(h); ++k)
++ if (kh_exist(h, k)) kh_value(h, k) = 1;
++ kh_destroy(32, h);
++ return 0;
++}
++*/
++
++/*
++ 2013-05-02 (0.2.8):
++
++ * Use quadratic probing. When the capacity is power of 2, stepping function
++ i*(i+1)/2 guarantees to traverse each bucket. It is better than double
++ hashing on cache performance and is more robust than linear probing.
++
++ In theory, double hashing should be more robust than quadratic probing.
++ However, my implementation is probably not for large hash tables, because
++ the second hash function is closely tied to the first hash function,
++ which reduce the effectiveness of double hashing.
++
++ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
++
++ 2011-12-29 (0.2.7):
++
++ * Minor code clean up; no actual effect.
++
++ 2011-09-16 (0.2.6):
++
++ * The capacity is a power of 2. This seems to dramatically improve the
++ speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
++
++ - http://code.google.com/p/ulib/
++ - http://nothings.org/computer/judy/
++
++ * Allow to optionally use linear probing which usually has better
++ performance for random input. Double hashing is still the default as it
++ is more robust to certain non-random input.
++
++ * Added Wang's integer hash function (not used by default). This hash
++ function is more robust to certain non-random input.
++
++ 2011-02-14 (0.2.5):
++
++ * Allow to declare global functions.
++
++ 2009-09-26 (0.2.4):
++
++ * Improve portability
++
++ 2008-09-19 (0.2.3):
++
++ * Corrected the example
++ * Improved interfaces
++
++ 2008-09-11 (0.2.2):
++
++ * Improved speed a little in kh_put()
++
++ 2008-09-10 (0.2.1):
++
++ * Added kh_clear()
++ * Fixed a compiling error
++
++ 2008-09-02 (0.2.0):
++
++ * Changed to token concatenation which increases flexibility.
++
++ 2008-08-31 (0.1.2):
++
++ * Fixed a bug in kh_get(), which has not been tested previously.
++
++ 2008-08-31 (0.1.1):
++
++ * Added destructor
++*/
++
++
++#ifndef __AC_KHASH_H
++#define __AC_KHASH_H
++
++/*!
++ @header
++
++ Generic hash table library.
++ */
++
++#define AC_VERSION_KHASH_H "0.2.8"
++
++#include <stdlib.h>
++#include <string.h>
++#include <limits.h>
++
++/* compiler specific configuration */
++
++#if UINT_MAX == 0xffffffffu
++typedef unsigned int khint32_t;
++#elif ULONG_MAX == 0xffffffffu
++typedef unsigned long khint32_t;
++#endif
++
++#if ULONG_MAX == ULLONG_MAX
++typedef unsigned long khint64_t;
++#else
++typedef unsigned long long khint64_t;
++#endif
++
++#ifndef kh_inline
++#ifdef _MSC_VER
++#define kh_inline __inline
++#else
++#define kh_inline inline
++#endif
++#endif /* kh_inline */
++
++#ifndef klib_unused
++#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
++#define klib_unused __attribute__ ((__unused__))
++#else
++#define klib_unused
++#endif
++#endif /* klib_unused */
++
++typedef khint32_t khint_t;
++typedef khint_t khiter_t;
++
++#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
++#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
++#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
++#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
++#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
++#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
++#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
++
++#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
++
++#ifndef kroundup32
++#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
++#endif
++
++#ifndef kcalloc
++#define kcalloc(N,Z) calloc(N,Z)
++#endif
++#ifndef kmalloc
++#define kmalloc(Z) malloc(Z)
++#endif
++#ifndef krealloc
++#define krealloc(P,Z) realloc(P,Z)
++#endif
++#ifndef kfree
++#define kfree(P) free(P)
++#endif
++
++static const double __ac_HASH_UPPER = 0.77;
++
++#define __KHASH_TYPE(name, khkey_t, khval_t) \
++ typedef struct kh_##name##_s { \
++ khint_t n_buckets, size, n_occupied, upper_bound; \
++ khint32_t *flags; \
++ khkey_t *keys; \
++ khval_t *vals; \
++ } kh_##name##_t;
++
++#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \
++ extern kh_##name##_t *kh_init_##name(void); \
++ extern void kh_destroy_##name(kh_##name##_t *h); \
++ extern void kh_clear_##name(kh_##name##_t *h); \
++ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \
++ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
++ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
++ extern void kh_del_##name(kh_##name##_t *h, khint_t x);
++
++#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
++ SCOPE kh_##name##_t *kh_init_##name(void) { \
++ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \
++ } \
++ SCOPE void kh_destroy_##name(kh_##name##_t *h) \
++ { \
++ if (h) { \
++ kfree((void *)h->keys); kfree(h->flags); \
++ kfree((void *)h->vals); \
++ kfree(h); \
++ } \
++ } \
++ SCOPE void kh_clear_##name(kh_##name##_t *h) \
++ { \
++ if (h && h->flags) { \
++ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
++ h->size = h->n_occupied = 0; \
++ } \
++ } \
++ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \
++ { \
++ if (h->n_buckets) { \
++ khint_t k, i, last, mask, step = 0; \
++ mask = h->n_buckets - 1; \
++ k = __hash_func(key); i = k & mask; \
++ last = i; \
++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
++ i = (i + (++step)) & mask; \
++ if (i == last) return h->n_buckets; \
++ } \
++ return __ac_iseither(h->flags, i)? h->n_buckets : i; \
++ } else return 0; \
++ } \
++ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
++ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
++ khint32_t *new_flags = 0; \
++ khint_t j = 1; \
++ { \
++ kroundup32(new_n_buckets); \
++ if (new_n_buckets < 4) new_n_buckets = 4; \
++ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \
++ else { /* hash table size to be changed (shrink or expand); rehash */ \
++ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
++ if (!new_flags) return -1; \
++ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
++ if (h->n_buckets < new_n_buckets) { /* expand */ \
++ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
++ if (!new_keys) { kfree(new_flags); return -1; } \
++ h->keys = new_keys; \
++ if (kh_is_map) { \
++ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
++ if (!new_vals) { kfree(new_flags); return -1; } \
++ h->vals = new_vals; \
++ } \
++ } /* otherwise shrink */ \
++ } \
++ } \
++ if (j) { /* rehashing is needed */ \
++ for (j = 0; j != h->n_buckets; ++j) { \
++ if (__ac_iseither(h->flags, j) == 0) { \
++ khkey_t key = h->keys[j]; \
++ khval_t val; \
++ khint_t new_mask; \
++ new_mask = new_n_buckets - 1; \
++ if (kh_is_map) val = h->vals[j]; \
++ __ac_set_isdel_true(h->flags, j); \
++ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
++ khint_t k, i, step = 0; \
++ k = __hash_func(key); \
++ i = k & new_mask; \
++ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
++ __ac_set_isempty_false(new_flags, i); \
++ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
++ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
++ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
++ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
++ } else { /* write the element and jump out of the loop */ \
++ h->keys[i] = key; \
++ if (kh_is_map) h->vals[i] = val; \
++ break; \
++ } \
++ } \
++ } \
++ } \
++ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
++ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
++ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
++ } \
++ kfree(h->flags); /* free the working space */ \
++ h->flags = new_flags; \
++ h->n_buckets = new_n_buckets; \
++ h->n_occupied = h->size; \
++ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
++ } \
++ return 0; \
++ } \
++ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
++ { \
++ khint_t x; \
++ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
++ if (h->n_buckets > (h->size<<1)) { \
++ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
++ *ret = -1; return h->n_buckets; \
++ } \
++ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
++ *ret = -1; return h->n_buckets; \
++ } \
++ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
++ { \
++ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
++ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
++ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \
++ else { \
++ last = i; \
++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
++ if (__ac_isdel(h->flags, i)) site = i; \
++ i = (i + (++step)) & mask; \
++ if (i == last) { x = site; break; } \
++ } \
++ if (x == h->n_buckets) { \
++ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
++ else x = i; \
++ } \
++ } \
++ } \
++ if (__ac_isempty(h->flags, x)) { /* not present at all */ \
++ h->keys[x] = key; \
++ __ac_set_isboth_false(h->flags, x); \
++ ++h->size; ++h->n_occupied; \
++ *ret = 1; \
++ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \
++ h->keys[x] = key; \
++ __ac_set_isboth_false(h->flags, x); \
++ ++h->size; \
++ *ret = 2; \
++ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
++ return x; \
++ } \
++ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \
++ { \
++ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
++ __ac_set_isdel_true(h->flags, x); \
++ --h->size; \
++ } \
++ }
++
++#define KHASH_DECLARE(name, khkey_t, khval_t) \
++ __KHASH_TYPE(name, khkey_t, khval_t) \
++ __KHASH_PROTOTYPES(name, khkey_t, khval_t)
++
++#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
++ __KHASH_TYPE(name, khkey_t, khval_t) \
++ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
++
++#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
++ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
++
++/* --- BEGIN OF HASH FUNCTIONS --- */
++
++/*! @function
++ @abstract Integer hash function
++ @param key The integer [khint32_t]
++ @return The hash value [khint_t]
++ */
++#define kh_int_hash_func(key) (khint32_t)(key)
++/*! @function
++ @abstract Integer comparison function
++ */
++#define kh_int_hash_equal(a, b) ((a) == (b))
++/*! @function
++ @abstract 64-bit integer hash function
++ @param key The integer [khint64_t]
++ @return The hash value [khint_t]
++ */
++#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
++/*! @function
++ @abstract 64-bit integer comparison function
++ */
++#define kh_int64_hash_equal(a, b) ((a) == (b))
++/*! @function
++ @abstract const char* hash function
++ @param s Pointer to a null terminated string
++ @return The hash value
++ */
++static kh_inline khint_t __ac_X31_hash_string(const char *s)
++{
++ khint_t h = (khint_t)*s;
++ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
++ return h;
++}
++/*! @function
++ @abstract Another interface to const char* hash function
++ @param key Pointer to a null terminated string [const char*]
++ @return The hash value [khint_t]
++ */
++#define kh_str_hash_func(key) __ac_X31_hash_string(key)
++/*! @function
++ @abstract Const char* comparison function
++ */
++#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
++
++static kh_inline khint_t __ac_Wang_hash(khint_t key)
++{
++ key += ~(key << 15);
++ key ^= (key >> 10);
++ key += (key << 3);
++ key ^= (key >> 6);
++ key += ~(key << 11);
++ key ^= (key >> 16);
++ return key;
++}
++#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
++
++/* --- END OF HASH FUNCTIONS --- */
++
++/* Other convenient macros... */
++
++/*!
++ @abstract Type of the hash table.
++ @param name Name of the hash table [symbol]
++ */
++#define khash_t(name) kh_##name##_t
++
++/*! @function
++ @abstract Initiate a hash table.
++ @param name Name of the hash table [symbol]
++ @return Pointer to the hash table [khash_t(name)*]
++ */
++#define kh_init(name) kh_init_##name()
++
++/*! @function
++ @abstract Destroy a hash table.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ */
++#define kh_destroy(name, h) kh_destroy_##name(h)
++
++/*! @function
++ @abstract Reset a hash table without deallocating memory.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ */
++#define kh_clear(name, h) kh_clear_##name(h)
++
++/*! @function
++ @abstract Resize a hash table.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param s New size [khint_t]
++ */
++#define kh_resize(name, h, s) kh_resize_##name(h, s)
++
++/*! @function
++ @abstract Insert a key to the hash table.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param k Key [type of keys]
++ @param r Extra return code: -1 if the operation failed;
++ 0 if the key is present in the hash table;
++ 1 if the bucket is empty (never used); 2 if the element in
++ the bucket has been deleted [int*]
++ @return Iterator to the inserted element [khint_t]
++ */
++#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
++
++/*! @function
++ @abstract Retrieve a key from the hash table.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param k Key [type of keys]
++ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
++ */
++#define kh_get(name, h, k) kh_get_##name(h, k)
++
++/*! @function
++ @abstract Remove a key from the hash table.
++ @param name Name of the hash table [symbol]
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param k Iterator to the element to be deleted [khint_t]
++ */
++#define kh_del(name, h, k) kh_del_##name(h, k)
++
++/*! @function
++ @abstract Test whether a bucket contains data.
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param x Iterator to the bucket [khint_t]
++ @return 1 if containing data; 0 otherwise [int]
++ */
++#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
++
++/*! @function
++ @abstract Get key given an iterator
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param x Iterator to the bucket [khint_t]
++ @return Key [type of keys]
++ */
++#define kh_key(h, x) ((h)->keys[x])
++
++/*! @function
++ @abstract Get value given an iterator
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param x Iterator to the bucket [khint_t]
++ @return Value [type of values]
++ @discussion For hash sets, calling this results in segfault.
++ */
++#define kh_val(h, x) ((h)->vals[x])
++
++/*! @function
++ @abstract Alias of kh_val()
++ */
++#define kh_value(h, x) ((h)->vals[x])
++
++/*! @function
++ @abstract Get the start iterator
++ @param h Pointer to the hash table [khash_t(name)*]
++ @return The start iterator [khint_t]
++ */
++#define kh_begin(h) (khint_t)(0)
++
++/*! @function
++ @abstract Get the end iterator
++ @param h Pointer to the hash table [khash_t(name)*]
++ @return The end iterator [khint_t]
++ */
++#define kh_end(h) ((h)->n_buckets)
++
++/*! @function
++ @abstract Get the number of elements in the hash table
++ @param h Pointer to the hash table [khash_t(name)*]
++ @return Number of elements in the hash table [khint_t]
++ */
++#define kh_size(h) ((h)->size)
++
++/*! @function
++ @abstract Get the number of buckets in the hash table
++ @param h Pointer to the hash table [khash_t(name)*]
++ @return Number of buckets in the hash table [khint_t]
++ */
++#define kh_n_buckets(h) ((h)->n_buckets)
++
++/*! @function
++ @abstract Iterate over the entries in the hash table
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param kvar Variable to which key will be assigned
++ @param vvar Variable to which value will be assigned
++ @param code Block of code to execute
++ */
++#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \
++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
++ if (!kh_exist(h,__i)) continue; \
++ (kvar) = kh_key(h,__i); \
++ (vvar) = kh_val(h,__i); \
++ code; \
++ } }
++
++/*! @function
++ @abstract Iterate over the values in the hash table
++ @param h Pointer to the hash table [khash_t(name)*]
++ @param vvar Variable to which value will be assigned
++ @param code Block of code to execute
++ */
++#define kh_foreach_value(h, vvar, code) { khint_t __i; \
++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
++ if (!kh_exist(h,__i)) continue; \
++ (vvar) = kh_val(h,__i); \
++ code; \
++ } }
++
++/* More convenient interfaces */
++
++/*! @function
++ @abstract Instantiate a hash set containing integer keys
++ @param name Name of the hash table [symbol]
++ */
++#define KHASH_SET_INIT_INT(name) \
++ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
++
++/*! @function
++ @abstract Instantiate a hash map containing integer keys
++ @param name Name of the hash table [symbol]
++ @param khval_t Type of values [type]
++ */
++#define KHASH_MAP_INIT_INT(name, khval_t) \
++ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
++
++/*! @function
++ @abstract Instantiate a hash set containing 64-bit integer keys
++ @param name Name of the hash table [symbol]
++ */
++#define KHASH_SET_INIT_INT64(name) \
++ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
++
++/*! @function
++ @abstract Instantiate a hash map containing 64-bit integer keys
++ @param name Name of the hash table [symbol]
++ @param khval_t Type of values [type]
++ */
++#define KHASH_MAP_INIT_INT64(name, khval_t) \
++ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
++
++typedef const char *kh_cstr_t;
++/*! @function
++ @abstract Instantiate a hash map containing const char* keys
++ @param name Name of the hash table [symbol]
++ */
++#define KHASH_SET_INIT_STR(name) \
++ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
++
++/*! @function
++ @abstract Instantiate a hash map containing const char* keys
++ @param name Name of the hash table [symbol]
++ @param khval_t Type of values [type]
++ */
++#define KHASH_MAP_INIT_STR(name, khval_t) \
++ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
++
++#endif /* __AC_KHASH_H */
+diff --git a/kvec.h b/kvec.h
+new file mode 100644
+index 0000000..10f1c5b
+--- /dev/null
++++ b/kvec.h
+@@ -0,0 +1,90 @@
++/* The MIT License
++
++ Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk>
++
++ Permission is hereby granted, free of charge, to any person obtaining
++ a copy of this software and associated documentation files (the
++ "Software"), to deal in the Software without restriction, including
++ without limitation the rights to use, copy, modify, merge, publish,
++ distribute, sublicense, and/or sell copies of the Software, and to
++ permit persons to whom the Software is furnished to do so, subject to
++ the following conditions:
++
++ The above copyright notice and this permission notice shall be
++ included in all copies or substantial portions of the Software.
++
++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++ SOFTWARE.
++*/
++
++/*
++ An example:
++
++#include "kvec.h"
++int main() {
++ kvec_t(int) array;
++ kv_init(array);
++ kv_push(int, array, 10); // append
++ kv_a(int, array, 20) = 5; // dynamic
++ kv_A(array, 20) = 4; // static
++ kv_destroy(array);
++ return 0;
++}
++*/
++
++/*
++ 2008-09-22 (0.1.0):
++
++ * The initial version.
++
++*/
++
++#ifndef AC_KVEC_H
++#define AC_KVEC_H
++
++#include <stdlib.h>
++
++#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
++
++#define kvec_t(type) struct { size_t n, m; type *a; }
++#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0)
++#define kv_destroy(v) free((v).a)
++#define kv_A(v, i) ((v).a[(i)])
++#define kv_pop(v) ((v).a[--(v).n])
++#define kv_size(v) ((v).n)
++#define kv_max(v) ((v).m)
++
++#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m))
++
++#define kv_copy(type, v1, v0) do { \
++ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \
++ (v1).n = (v0).n; \
++ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \
++ } while (0) \
++
++#define kv_push(type, v, x) do { \
++ if ((v).n == (v).m) { \
++ (v).m = (v).m? (v).m<<1 : 2; \
++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \
++ } \
++ (v).a[(v).n++] = (x); \
++ } while (0)
++
++#define kv_pushp(type, v) ((((v).n == (v).m)? \
++ ((v).m = ((v).m? (v).m<<1 : 2), \
++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
++ : 0), ((v).a + ((v).n++)))
++
++#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \
++ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \
++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
++ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \
++ : 0), (v).a[(i)])
++
++#endif
+diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c
+new file mode 100644
+index 0000000..829c0fc
+--- /dev/null
++++ b/rowcolumn_diacritics_helpers.c
+@@ -0,0 +1,391 @@
++#include <stdint.h>
++
++uint16_t diacritic_to_num(uint32_t code)
++{
++ switch (code) {
++ case 0x305:
++ return code - 0x305 + 1;
++ case 0x30d:
++ case 0x30e:
++ return code - 0x30d + 2;
++ case 0x310:
++ return code - 0x310 + 4;
++ case 0x312:
++ return code - 0x312 + 5;
++ case 0x33d:
++ case 0x33e:
++ case 0x33f:
++ return code - 0x33d + 6;
++ case 0x346:
++ return code - 0x346 + 9;
++ case 0x34a:
++ case 0x34b:
++ case 0x34c:
++ return code - 0x34a + 10;
++ case 0x350:
++ case 0x351:
++ case 0x352:
++ return code - 0x350 + 13;
++ case 0x357:
++ return code - 0x357 + 16;
++ case 0x35b:
++ return code - 0x35b + 17;
++ case 0x363:
++ case 0x364:
++ case 0x365:
++ case 0x366:
++ case 0x367:
++ case 0x368:
++ case 0x369:
++ case 0x36a:
++ case 0x36b:
++ case 0x36c:
++ case 0x36d:
++ case 0x36e:
++ case 0x36f:
++ return code - 0x363 + 18;
++ case 0x483:
++ case 0x484:
++ case 0x485:
++ case 0x486:
++ case 0x487:
++ return code - 0x483 + 31;
++ case 0x592:
++ case 0x593:
++ case 0x594:
++ case 0x595:
++ return code - 0x592 + 36;
++ case 0x597:
++ case 0x598:
++ case 0x599:
++ return code - 0x597 + 40;
++ case 0x59c:
++ case 0x59d:
++ case 0x59e:
++ case 0x59f:
++ case 0x5a0:
++ case 0x5a1:
++ return code - 0x59c + 43;
++ case 0x5a8:
++ case 0x5a9:
++ return code - 0x5a8 + 49;
++ case 0x5ab:
++ case 0x5ac:
++ return code - 0x5ab + 51;
++ case 0x5af:
++ return code - 0x5af + 53;
++ case 0x5c4:
++ return code - 0x5c4 + 54;
++ case 0x610:
++ case 0x611:
++ case 0x612:
++ case 0x613:
++ case 0x614:
++ case 0x615:
++ case 0x616:
++ case 0x617:
++ return code - 0x610 + 55;
++ case 0x657:
++ case 0x658:
++ case 0x659:
++ case 0x65a:
++ case 0x65b:
++ return code - 0x657 + 63;
++ case 0x65d:
++ case 0x65e:
++ return code - 0x65d + 68;
++ case 0x6d6:
++ case 0x6d7:
++ case 0x6d8:
++ case 0x6d9:
++ case 0x6da:
++ case 0x6db:
++ case 0x6dc:
++ return code - 0x6d6 + 70;
++ case 0x6df:
++ case 0x6e0:
++ case 0x6e1:
++ case 0x6e2:
++ return code - 0x6df + 77;
++ case 0x6e4:
++ return code - 0x6e4 + 81;
++ case 0x6e7:
++ case 0x6e8:
++ return code - 0x6e7 + 82;
++ case 0x6eb:
++ case 0x6ec:
++ return code - 0x6eb + 84;
++ case 0x730:
++ return code - 0x730 + 86;
++ case 0x732:
++ case 0x733:
++ return code - 0x732 + 87;
++ case 0x735:
++ case 0x736:
++ return code - 0x735 + 89;
++ case 0x73a:
++ return code - 0x73a + 91;
++ case 0x73d:
++ return code - 0x73d + 92;
++ case 0x73f:
++ case 0x740:
++ case 0x741:
++ return code - 0x73f + 93;
++ case 0x743:
++ return code - 0x743 + 96;
++ case 0x745:
++ return code - 0x745 + 97;
++ case 0x747:
++ return code - 0x747 + 98;
++ case 0x749:
++ case 0x74a:
++ return code - 0x749 + 99;
++ case 0x7eb:
++ case 0x7ec:
++ case 0x7ed:
++ case 0x7ee:
++ case 0x7ef:
++ case 0x7f0:
++ case 0x7f1:
++ return code - 0x7eb + 101;
++ case 0x7f3:
++ return code - 0x7f3 + 108;
++ case 0x816:
++ case 0x817:
++ case 0x818:
++ case 0x819:
++ return code - 0x816 + 109;
++ case 0x81b:
++ case 0x81c:
++ case 0x81d:
++ case 0x81e:
++ case 0x81f:
++ case 0x820:
++ case 0x821:
++ case 0x822:
++ case 0x823:
++ return code - 0x81b + 113;
++ case 0x825:
++ case 0x826:
++ case 0x827:
++ return code - 0x825 + 122;
++ case 0x829:
++ case 0x82a:
++ case 0x82b:
++ case 0x82c:
++ case 0x82d:
++ return code - 0x829 + 125;
++ case 0x951:
++ return code - 0x951 + 130;
++ case 0x953:
++ case 0x954:
++ return code - 0x953 + 131;
++ case 0xf82:
++ case 0xf83:
++ return code - 0xf82 + 133;
++ case 0xf86:
++ case 0xf87:
++ return code - 0xf86 + 135;
++ case 0x135d:
++ case 0x135e:
++ case 0x135f:
++ return code - 0x135d + 137;
++ case 0x17dd:
++ return code - 0x17dd + 140;
++ case 0x193a:
++ return code - 0x193a + 141;
++ case 0x1a17:
++ return code - 0x1a17 + 142;
++ case 0x1a75:
++ case 0x1a76:
++ case 0x1a77:
++ case 0x1a78:
++ case 0x1a79:
++ case 0x1a7a:
++ case 0x1a7b:
++ case 0x1a7c:
++ return code - 0x1a75 + 143;
++ case 0x1b6b:
++ return code - 0x1b6b + 151;
++ case 0x1b6d:
++ case 0x1b6e:
++ case 0x1b6f:
++ case 0x1b70:
++ case 0x1b71:
++ case 0x1b72:
++ case 0x1b73:
++ return code - 0x1b6d + 152;
++ case 0x1cd0:
++ case 0x1cd1:
++ case 0x1cd2:
++ return code - 0x1cd0 + 159;
++ case 0x1cda:
++ case 0x1cdb:
++ return code - 0x1cda + 162;
++ case 0x1ce0:
++ return code - 0x1ce0 + 164;
++ case 0x1dc0:
++ case 0x1dc1:
++ return code - 0x1dc0 + 165;
++ case 0x1dc3:
++ case 0x1dc4:
++ case 0x1dc5:
++ case 0x1dc6:
++ case 0x1dc7:
++ case 0x1dc8:
++ case 0x1dc9:
++ return code - 0x1dc3 + 167;
++ case 0x1dcb:
++ case 0x1dcc:
++ return code - 0x1dcb + 174;
++ case 0x1dd1:
++ case 0x1dd2:
++ case 0x1dd3:
++ case 0x1dd4:
++ case 0x1dd5:
++ case 0x1dd6:
++ case 0x1dd7:
++ case 0x1dd8:
++ case 0x1dd9:
++ case 0x1dda:
++ case 0x1ddb:
++ case 0x1ddc:
++ case 0x1ddd:
++ case 0x1dde:
++ case 0x1ddf:
++ case 0x1de0:
++ case 0x1de1:
++ case 0x1de2:
++ case 0x1de3:
++ case 0x1de4:
++ case 0x1de5:
++ case 0x1de6:
++ return code - 0x1dd1 + 176;
++ case 0x1dfe:
++ return code - 0x1dfe + 198;
++ case 0x20d0:
++ case 0x20d1:
++ return code - 0x20d0 + 199;
++ case 0x20d4:
++ case 0x20d5:
++ case 0x20d6:
++ case 0x20d7:
++ return code - 0x20d4 + 201;
++ case 0x20db:
++ case 0x20dc:
++ return code - 0x20db + 205;
++ case 0x20e1:
++ return code - 0x20e1 + 207;
++ case 0x20e7:
++ return code - 0x20e7 + 208;
++ case 0x20e9:
++ return code - 0x20e9 + 209;
++ case 0x20f0:
++ return code - 0x20f0 + 210;
++ case 0x2cef:
++ case 0x2cf0:
++ case 0x2cf1:
++ return code - 0x2cef + 211;
++ case 0x2de0:
++ case 0x2de1:
++ case 0x2de2:
++ case 0x2de3:
++ case 0x2de4:
++ case 0x2de5:
++ case 0x2de6:
++ case 0x2de7:
++ case 0x2de8:
++ case 0x2de9:
++ case 0x2dea:
++ case 0x2deb:
++ case 0x2dec:
++ case 0x2ded:
++ case 0x2dee:
++ case 0x2def:
++ case 0x2df0:
++ case 0x2df1:
++ case 0x2df2:
++ case 0x2df3:
++ case 0x2df4:
++ case 0x2df5:
++ case 0x2df6:
++ case 0x2df7:
++ case 0x2df8:
++ case 0x2df9:
++ case 0x2dfa:
++ case 0x2dfb:
++ case 0x2dfc:
++ case 0x2dfd:
++ case 0x2dfe:
++ case 0x2dff:
++ return code - 0x2de0 + 214;
++ case 0xa66f:
++ return code - 0xa66f + 246;
++ case 0xa67c:
++ case 0xa67d:
++ return code - 0xa67c + 247;
++ case 0xa6f0:
++ case 0xa6f1:
++ return code - 0xa6f0 + 249;
++ case 0xa8e0:
++ case 0xa8e1:
++ case 0xa8e2:
++ case 0xa8e3:
++ case 0xa8e4:
++ case 0xa8e5:
++ case 0xa8e6:
++ case 0xa8e7:
++ case 0xa8e8:
++ case 0xa8e9:
++ case 0xa8ea:
++ case 0xa8eb:
++ case 0xa8ec:
++ case 0xa8ed:
++ case 0xa8ee:
++ case 0xa8ef:
++ case 0xa8f0:
++ case 0xa8f1:
++ return code - 0xa8e0 + 251;
++ case 0xaab0:
++ return code - 0xaab0 + 269;
++ case 0xaab2:
++ case 0xaab3:
++ return code - 0xaab2 + 270;
++ case 0xaab7:
++ case 0xaab8:
++ return code - 0xaab7 + 272;
++ case 0xaabe:
++ case 0xaabf:
++ return code - 0xaabe + 274;
++ case 0xaac1:
++ return code - 0xaac1 + 276;
++ case 0xfe20:
++ case 0xfe21:
++ case 0xfe22:
++ case 0xfe23:
++ case 0xfe24:
++ case 0xfe25:
++ case 0xfe26:
++ return code - 0xfe20 + 277;
++ case 0x10a0f:
++ return code - 0x10a0f + 284;
++ case 0x10a38:
++ return code - 0x10a38 + 285;
++ case 0x1d185:
++ case 0x1d186:
++ case 0x1d187:
++ case 0x1d188:
++ case 0x1d189:
++ return code - 0x1d185 + 286;
++ case 0x1d1aa:
++ case 0x1d1ab:
++ case 0x1d1ac:
++ case 0x1d1ad:
++ return code - 0x1d1aa + 291;
++ case 0x1d242:
++ case 0x1d243:
++ case 0x1d244:
++ return code - 0x1d242 + 295;
++ }
++ return 0;
++}
+diff --git a/st.c b/st.c
+index 57c6e96..f1c5299 100644
+--- a/st.c
++++ b/st.c
+@@ -19,6 +19,7 @@
+
+ #include "st.h"
+ #include "win.h"
++#include "graphics.h"
+
+ #if defined(__linux)
+ #include <pty.h>
+@@ -36,6 +37,10 @@
+ #define STR_BUF_SIZ ESC_BUF_SIZ
+ #define STR_ARG_SIZ ESC_ARG_SIZ
+
++/* PUA character used as an image placeholder */
++#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE
++#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE
++
+ /* macros */
+ #define IS_SET(flag) ((term.mode & (flag)) != 0)
+ #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f)
+@@ -113,6 +118,8 @@ typedef struct {
+ typedef struct {
+ int row; /* nb row */
+ int col; /* nb col */
++ int pixw; /* width of the text area in pixels */
++ int pixh; /* height of the text area in pixels */
+ Line *line; /* screen */
+ Line *alt; /* alternate screen */
+ int *dirty; /* dirtyness of lines */
+@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *);
+ static char utf8encodebyte(Rune, size_t);
+ static size_t utf8validate(Rune *, size_t);
+
+-static char *base64dec(const char *);
+ static char base64dec_getc(const char **);
+
+ static ssize_t xwrite(int, const char *, size_t);
+@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
+ static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
+ static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
++/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0
++ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */
++uint16_t diacritic_to_num(uint32_t code);
++
+ ssize_t
+ xwrite(int fd, const char *s, size_t len)
+ {
+@@ -616,6 +626,12 @@ getsel(void)
+ if (gp->mode & ATTR_WDUMMY)
+ continue;
+
++ if (gp->mode & ATTR_IMAGE) {
++ // TODO: Copy diacritics as well
++ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr);
++ continue;
++ }
++
+ ptr += utf8encode(gp->u, ptr);
+ }
+
+@@ -819,7 +835,11 @@ ttyread(void)
+ {
+ static char buf[BUFSIZ];
+ static int buflen = 0;
+- int ret, written;
++ static int already_processing = 0;
++ int ret, written = 0;
++
++ if (buflen >= LEN(buf))
++ return 0;
+
+ /* append read bytes to unprocessed bytes */
+ ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
+@@ -831,7 +851,24 @@ ttyread(void)
+ die("couldn't read from shell: %s\n", strerror(errno));
+ default:
+ buflen += ret;
+- written = twrite(buf, buflen, 0);
++ if (already_processing) {
++ /* Avoid recursive call to twrite() */
++ return ret;
++ }
++ already_processing = 1;
++ while (1) {
++ int buflen_before_processing = buflen;
++ written += twrite(buf + written, buflen - written, 0);
++ // If buflen changed during the call to twrite, there is
++ // new data, and we need to keep processing, otherwise
++ // we can exit. This will not loop forever because the
++ // buffer is limited, and we don't clean it in this
++ // loop, so at some point ttywrite will have to drop
++ // some data.
++ if (buflen_before_processing == buflen)
++ break;
++ }
++ already_processing = 0;
+ buflen -= written;
+ /* keep any incomplete UTF-8 byte sequence for the next call */
+ if (buflen > 0)
+@@ -874,6 +911,7 @@ ttywriteraw(const char *s, size_t n)
+ fd_set wfd, rfd;
+ ssize_t r;
+ size_t lim = 256;
++ int retries_left = 100;
+
+ /*
+ * Remember that we are using a pty, which might be a modem line.
+@@ -882,6 +920,9 @@ ttywriteraw(const char *s, size_t n)
+ * FIXME: Migrate the world to Plan 9.
+ */
+ while (n > 0) {
++ if (retries_left-- <= 0)
++ goto too_many_retries;
++
+ FD_ZERO(&wfd);
+ FD_ZERO(&rfd);
+ FD_SET(cmdfd, &wfd);
+@@ -923,11 +964,16 @@ ttywriteraw(const char *s, size_t n)
+
+ write_error:
+ die("write error on tty: %s\n", strerror(errno));
++too_many_retries:
++ fprintf(stderr, "Could not write %zu bytes to tty\n", n);
+ }
+
+ void
+ ttyresize(int tw, int th)
+ {
++ term.pixw = tw;
++ term.pixh = th;
++
+ struct winsize w;
+
+ w.ws_row = term.row;
+@@ -1015,7 +1061,8 @@ treset(void)
+ term.c = (TCursor){{
+ .mode = ATTR_NULL,
+ .fg = defaultfg,
+- .bg = defaultbg
++ .bg = defaultbg,
++ .decor = DECOR_DEFAULT_COLOR
+ }, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
+
+ memset(term.tabs, 0, term.col * sizeof(*term.tabs));
+@@ -1038,7 +1085,9 @@ treset(void)
+ void
+ tnew(int col, int row)
+ {
+- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
++ term = (Term){.c = {.attr = {.fg = defaultfg,
++ .bg = defaultbg,
++ .decor = DECOR_DEFAULT_COLOR}}};
+ tresize(col, row);
+ treset();
+ }
+@@ -1215,9 +1264,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
+ term.line[y][x-1].mode &= ~ATTR_WIDE;
+ }
+
++ if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE &&
++ tgetisclassicplaceholder(&term.line[y][x])) {
++ // This is a workaround: don't overwrite classic placement
++ // placeholders with space symbols (unlike Unicode placeholders
++ // which must be overwritten by anything).
++ term.line[y][x].bg = attr->bg;
++ term.dirty[y] = 1;
++ return;
++ }
++
+ term.dirty[y] = 1;
+ term.line[y][x] = *attr;
+ term.line[y][x].u = u;
++
++ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) {
++ term.line[y][x].u = 0;
++ term.line[y][x].mode |= ATTR_IMAGE;
++ }
+ }
+
+ void
+@@ -1244,12 +1308,104 @@ tclearregion(int x1, int y1, int x2, int y2)
+ selclear();
+ gp->fg = term.c.attr.fg;
+ gp->bg = term.c.attr.bg;
++ gp->decor = term.c.attr.decor;
+ gp->mode = 0;
+ gp->u = ' ';
+ }
+ }
+ }
+
++/// Fills a rectangle area with an image placeholder. The starting point is the
++/// cursor. Adds empty lines if needed. The placeholder will be marked as
++/// classic.
++void
++tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id,
++ int cols, int rows, char do_not_move_cursor)
++{
++ for (int row = 0; row < rows; ++row) {
++ int y = term.c.y;
++ term.dirty[y] = 1;
++ for (int col = 0; col < cols; ++col) {
++ int x = term.c.x + col;
++ if (x >= term.col)
++ break;
++ Glyph *gp = &term.line[y][x];
++ if (selected(x, y))
++ selclear();
++ gp->mode = ATTR_IMAGE;
++ gp->u = 0;
++ tsetimgrow(gp, row + 1);
++ tsetimgcol(gp, col + 1);
++ tsetimgid(gp, image_id);
++ tsetimgplacementid(gp, placement_id);
++ tsetimgdiacriticcount(gp, 3);
++ tsetisclassicplaceholder(gp, 1);
++ }
++ // If moving the cursor is not allowed and this is the last line
++ // of the terminal, we are done.
++ if (do_not_move_cursor && y == term.row - 1)
++ break;
++ // Move the cursor down, maybe creating a new line. The x is
++ // preserved (we never change term.c.x in the loop above).
++ if (row != rows - 1)
++ tnewline(/*first_col=*/0);
++ }
++ if (do_not_move_cursor) {
++ // Return the cursor to the original position.
++ tmoveto(term.c.x, term.c.y - rows + 1);
++ } else {
++ // Move the cursor beyond the last column, as required by the
++ // protocol. If the cursor goes beyond the screen edge, insert a
++ // newline to match the behavior of kitty.
++ if (term.c.x + cols >= term.col)
++ tnewline(/*first_col=*/1);
++ else
++ tmoveto(term.c.x + cols, term.c.y);
++ }
++}
++
++void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
++ uint32_t placement_id, int col,
++ int row, char is_classic),
++ void *data) {
++ for (int row = 0; row < term.row; ++row) {
++ for (int col = 0; col < term.col; ++col) {
++ Glyph *gp = &term.line[row][col];
++ if (gp->mode & ATTR_IMAGE) {
++ uint32_t image_id = tgetimgid(gp);
++ uint32_t placement_id = tgetimgplacementid(gp);
++ int ret =
++ callback(data, tgetimgid(gp),
++ tgetimgplacementid(gp),
++ tgetimgcol(gp), tgetimgrow(gp),
++ tgetisclassicplaceholder(gp));
++ if (ret == 1) {
++ term.dirty[row] = 1;
++ gp->mode = 0;
++ gp->u = ' ';
++ }
++ }
++ }
++ }
++}
++
++void gr_schedule_image_redraw_by_id(uint32_t image_id) {
++ for (int row = 0; row < term.row; ++row) {
++ if (term.dirty[row])
++ continue;
++ for (int col = 0; col < term.col; ++col) {
++ Glyph *gp = &term.line[row][col];
++ if (gp->mode & ATTR_IMAGE) {
++ uint32_t cell_image_id = tgetimgid(gp);
++ if (cell_image_id == image_id) {
++ term.dirty[row] = 1;
++ break;
++ }
++ }
++ }
++ }
++}
++
+ void
+ tdeletechar(int n)
+ {
+@@ -1368,6 +1524,7 @@ tsetattr(const int *attr, int l)
+ ATTR_STRUCK );
+ term.c.attr.fg = defaultfg;
+ term.c.attr.bg = defaultbg;
++ term.c.attr.decor = DECOR_DEFAULT_COLOR;
+ break;
+ case 1:
+ term.c.attr.mode |= ATTR_BOLD;
+@@ -1380,6 +1537,20 @@ tsetattr(const int *attr, int l)
+ break;
+ case 4:
+ term.c.attr.mode |= ATTR_UNDERLINE;
++ if (i + 1 < l) {
++ idx = attr[++i];
++ if (BETWEEN(idx, 1, 5)) {
++ tsetdecorstyle(&term.c.attr, idx);
++ } else if (idx == 0) {
++ term.c.attr.mode &= ~ATTR_UNDERLINE;
++ tsetdecorstyle(&term.c.attr, 0);
++ } else {
++ fprintf(stderr,
++ "erresc: unknown underline "
++ "style %d\n",
++ idx);
++ }
++ }
+ break;
+ case 5: /* slow blink */
+ /* FALLTHROUGH */
+@@ -1403,6 +1574,7 @@ tsetattr(const int *attr, int l)
+ break;
+ case 24:
+ term.c.attr.mode &= ~ATTR_UNDERLINE;
++ tsetdecorstyle(&term.c.attr, 0);
+ break;
+ case 25:
+ term.c.attr.mode &= ~ATTR_BLINK;
+@@ -1430,6 +1602,13 @@ tsetattr(const int *attr, int l)
+ case 49:
+ term.c.attr.bg = defaultbg;
+ break;
++ case 58:
++ if ((idx = tdefcolor(attr, &i, l)) >= 0)
++ tsetdecorcolor(&term.c.attr, idx);
++ break;
++ case 59:
++ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR);
++ break;
+ default:
+ if (BETWEEN(attr[i], 30, 37)) {
+ term.c.attr.fg = attr[i] - 30;
+@@ -1813,6 +1992,39 @@ csihandle(void)
+ goto unknown;
+ }
+ break;
++ case '>':
++ switch (csiescseq.mode[1]) {
++ case 'q': /* XTVERSION -- Print terminal name and version */
++ len = snprintf(buf, sizeof(buf),
++ "\033P>|st-graphics(%s)\033\\", VERSION);
++ ttywrite(buf, len, 0);
++ break;
++ default:
++ goto unknown;
++ }
++ break;
++ case 't': /* XTWINOPS -- Window manipulation */
++ switch (csiescseq.arg[0]) {
++ case 14: /* Report text area size in pixels. */
++ len = snprintf(buf, sizeof(buf), "\033[4;%i;%it",
++ term.pixh, term.pixw);
++ ttywrite(buf, len, 0);
++ break;
++ case 16: /* Report character cell size in pixels. */
++ len = snprintf(buf, sizeof(buf), "\033[6;%i;%it",
++ term.pixh / term.row,
++ term.pixw / term.col);
++ ttywrite(buf, len, 0);
++ break;
++ case 18: /* Report the size of the text area in characters. */
++ len = snprintf(buf, sizeof(buf), "\033[8;%i;%it",
++ term.row, term.col);
++ ttywrite(buf, len, 0);
++ break;
++ default:
++ goto unknown;
++ }
++ break;
+ }
+ }
+
+@@ -1962,8 +2174,26 @@ strhandle(void)
+ case 'k': /* old title set compatibility */
+ xsettitle(strescseq.args[0]);
+ return;
+- case 'P': /* DCS -- Device Control String */
+ case '_': /* APC -- Application Program Command */
++ if (gr_parse_command(strescseq.buf, strescseq.len)) {
++ GraphicsCommandResult *res = &graphics_command_result;
++ if (res->create_placeholder) {
++ tcreateimgplaceholder(
++ res->placeholder.image_id,
++ res->placeholder.placement_id,
++ res->placeholder.columns,
++ res->placeholder.rows,
++ res->placeholder.do_not_move_cursor);
++ }
++ if (res->response[0])
++ ttywrite(res->response, strlen(res->response),
++ 0);
++ if (res->redraw)
++ tfulldirt();
++ return;
++ }
++ return;
++ case 'P': /* DCS -- Device Control String */
+ case '^': /* PM -- Privacy Message */
+ return;
+ }
+@@ -2469,6 +2699,33 @@ check_control_code:
+ if (selected(term.c.x, term.c.y))
+ selclear();
+
++ if (width == 0) {
++ // It's probably a combining char. Combining characters are not
++ // supported, so we just ignore them, unless it denotes the row and
++ // column of an image character.
++ if (term.c.y <= 0 && term.c.x <= 0)
++ return;
++ else if (term.c.x == 0)
++ gp = &term.line[term.c.y-1][term.col-1];
++ else if (term.c.state & CURSOR_WRAPNEXT)
++ gp = &term.line[term.c.y][term.c.x];
++ else
++ gp = &term.line[term.c.y][term.c.x-1];
++ uint16_t num = diacritic_to_num(u);
++ if (num && (gp->mode & ATTR_IMAGE)) {
++ unsigned diaccount = tgetimgdiacriticcount(gp);
++ if (diaccount == 0)
++ tsetimgrow(gp, num);
++ else if (diaccount == 1)
++ tsetimgcol(gp, num);
++ else if (diaccount == 2)
++ tsetimg4thbyteplus1(gp, num);
++ tsetimgdiacriticcount(gp, diaccount + 1);
++ }
++ term.lastc = u;
++ return;
++ }
++
+ gp = &term.line[term.c.y][term.c.x];
+ if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
+ gp->mode |= ATTR_WRAP;
+@@ -2635,6 +2892,8 @@ drawregion(int x1, int y1, int x2, int y2)
+ {
+ int y;
+
++ xstartimagedraw(term.dirty, term.row);
++
+ for (y = y1; y < y2; y++) {
+ if (!term.dirty[y])
+ continue;
+@@ -2642,6 +2901,8 @@ drawregion(int x1, int y1, int x2, int y2)
+ term.dirty[y] = 0;
+ xdrawline(term.line[y], x1, y, x2);
+ }
++
++ xfinishimagedraw();
+ }
+
+ void
+@@ -2676,3 +2937,9 @@ redraw(void)
+ tfulldirt();
+ draw();
+ }
++
++Glyph
++getglyphat(int col, int row)
++{
++ return term.line[row][col];
++}
+diff --git a/st.h b/st.h
+index fd3b0d8..c5dd731 100644
+--- a/st.h
++++ b/st.h
+@@ -12,7 +12,7 @@
+ #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
+ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
+ #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
+- (a).bg != (b).bg)
++ (a).bg != (b).bg || (a).decor != (b).decor)
+ #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
+ (t1.tv_nsec-t2.tv_nsec)/1E6)
+ #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit)))
+@@ -20,6 +20,10 @@
+ #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b))
+ #define IS_TRUECOL(x) (1 << 24 & (x))
+
++// This decor color indicates that the fg color should be used. Note that it's
++// not a 24-bit color because the 25-th bit is not set.
++#define DECOR_DEFAULT_COLOR 0x0ffffff
++
+ enum glyph_attribute {
+ ATTR_NULL = 0,
+ ATTR_BOLD = 1 << 0,
+@@ -34,6 +38,7 @@ enum glyph_attribute {
+ ATTR_WIDE = 1 << 9,
+ ATTR_WDUMMY = 1 << 10,
+ ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
++ ATTR_IMAGE = 1 << 14,
+ };
+
+ enum selection_mode {
+@@ -52,6 +57,14 @@ enum selection_snap {
+ SNAP_LINE = 2
+ };
+
++enum underline_style {
++ UNDERLINE_STRAIGHT = 1,
++ UNDERLINE_DOUBLE = 2,
++ UNDERLINE_CURLY = 3,
++ UNDERLINE_DOTTED = 4,
++ UNDERLINE_DASHED = 5,
++};
++
+ typedef unsigned char uchar;
+ typedef unsigned int uint;
+ typedef unsigned long ulong;
+@@ -65,6 +78,7 @@ typedef struct {
+ ushort mode; /* attribute flags */
+ uint32_t fg; /* foreground */
+ uint32_t bg; /* background */
++ uint32_t decor; /* decoration (like underline) */
+ } Glyph;
+
+ typedef Glyph *Line;
+@@ -105,6 +119,8 @@ void selextend(int, int, int, int);
+ int selected(int, int);
+ char *getsel(void);
+
++Glyph getglyphat(int, int);
++
+ size_t utf8encode(Rune, char *);
+
+ void *xmalloc(size_t);
+@@ -124,3 +140,69 @@ extern unsigned int tabspaces;
+ extern unsigned int defaultfg;
+ extern unsigned int defaultbg;
+ extern unsigned int defaultcs;
++
++// Accessors to decoration properties stored in `decor`.
++// The 25-th bit is used to indicate if it's a 24-bit color.
++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; }
++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; }
++static inline void tsetdecorcolor(Glyph *g, uint32_t color) {
++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff);
++}
++static inline void tsetdecorstyle(Glyph *g, uint32_t style) {
++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25);
++}
++
++
++// Some accessors to image placeholder properties stored in `u`:
++// - row (1-base) - 9 bits
++// - column (1-base) - 9 bits
++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified,
++// don't forget to subtract 1).
++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits
++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit
++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; }
++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; }
++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; }
++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; }
++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; }
++static inline void tsetimgrow(Glyph *g, uint32_t row) {
++ g->u = (g->u & ~0x1ff) | (row & 0x1ff);
++}
++static inline void tsetimgcol(Glyph *g, uint32_t col) {
++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9);
++}
++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) {
++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18);
++}
++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) {
++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27);
++}
++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) {
++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29);
++}
++
++/// Returns the full image id. This is a naive implementation, if the most
++/// significant byte is not specified, it's assumed to be 0 instead of inferring
++/// it from the cells to the left.
++static inline uint32_t tgetimgid(Glyph *g) {
++ uint32_t msb = tgetimgid4thbyteplus1(g);
++ if (msb != 0)
++ --msb;
++ return (msb << 24) | (g->fg & 0xFFFFFF);
++}
++
++/// Sets the full image id.
++static inline void tsetimgid(Glyph *g, uint32_t id) {
++ g->fg = (id & 0xFFFFFF) | (1 << 24);
++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1);
++}
++
++static inline uint32_t tgetimgplacementid(Glyph *g) {
++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR)
++ return 0;
++ return g->decor & 0xFFFFFF;
++}
++
++static inline void tsetimgplacementid(Glyph *g, uint32_t id) {
++ g->decor = (id & 0xFFFFFF) | (1 << 24);
++}
+diff --git a/st.info b/st.info
+index efab2cf..ded76c1 100644
+--- a/st.info
++++ b/st.info
+@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor,
+ Ms=\E]52;%p1%s;%p2%s\007,
+ Se=\E[2 q,
+ Ss=\E[%p1%d q,
++ Smulx=\E[4:%p1%dm,
+
+ st| simpleterm,
+ use=st-mono,
+@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors,
+ initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
+ setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
+ setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
++# Underline colors
++ Su,
++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
++ Setulc1=\E[58:5:%p1%dm,
++ ol=\E[59m,
+
+ st-meta| simpleterm with meta key,
+ use=st,
+diff --git a/win.h b/win.h
+index 6de960d..31b3fff 100644
+--- a/win.h
++++ b/win.h
+@@ -39,3 +39,6 @@ void xsetpointermotion(int);
+ void xsetsel(char *);
+ int xstartdraw(void);
+ void xximspot(int, int);
++
++void xstartimagedraw(int *dirty, int rows);
++void xfinishimagedraw();
+diff --git a/x.c b/x.c
+index d73152b..6f1bf8c 100644
+--- a/x.c
++++ b/x.c
+@@ -4,6 +4,8 @@
+ #include <limits.h>
+ #include <locale.h>
+ #include <signal.h>
++#include <stdio.h>
++#include <stdlib.h>
+ #include <sys/select.h>
+ #include <time.h>
+ #include <unistd.h>
+@@ -19,6 +21,7 @@ char *argv0;
+ #include "arg.h"
+ #include "st.h"
+ #include "win.h"
++#include "graphics.h"
+
+ /* types used in config.h */
+ typedef struct {
+@@ -59,6 +62,12 @@ static void zoom(const Arg *);
+ static void zoomabs(const Arg *);
+ static void zoomreset(const Arg *);
+ static void ttysend(const Arg *);
++static void previewimage(const Arg *);
++static void showimageinfo(const Arg *);
++static void togglegrdebug(const Arg *);
++static void dumpgrstate(const Arg *);
++static void unloadimages(const Arg *);
++static void toggleimages(const Arg *);
+
+ /* config.h for applying patches and the configuration. */
+ #include "config.h"
+@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
+ typedef struct {
+ int tw, th; /* tty width and height */
+ int w, h; /* window width and height */
++ int hborderpx, vborderpx;
+ int ch; /* char height */
+ int cw; /* char width */
+ int mode; /* window state/mode flags */
+@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int);
+ static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
+ static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
+ static void xdrawglyph(Glyph, int, int);
++static void xdrawimages(Glyph, Line, int x1, int y1, int x2);
++static void xdrawoneimagecell(Glyph, int x, int y);
+ static void xclear(int, int, int, int);
+ static int xgeommasktogravity(int);
+ static int ximopen(Display *);
+@@ -220,6 +232,7 @@ static DC dc;
+ static XWindow xw;
+ static XSelection xsel;
+ static TermWindow win;
++static unsigned int mouse_col = 0, mouse_row = 0;
+
+ /* Font Ring Cache */
+ enum {
+@@ -328,10 +341,72 @@ ttysend(const Arg *arg)
+ ttywrite(arg->s, strlen(arg->s), 1);
+ }
+
++void
++previewimage(const Arg *arg)
++{
++ Glyph g = getglyphat(mouse_col, mouse_row);
++ if (g.mode & ATTR_IMAGE) {
++ uint32_t image_id = tgetimgid(&g);
++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
++ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
++ tgetimgrow(&g));
++ gr_preview_image(image_id, arg->s);
++ }
++}
++
++void
++showimageinfo(const Arg *arg)
++{
++ Glyph g = getglyphat(mouse_col, mouse_row);
++ if (g.mode & ATTR_IMAGE) {
++ uint32_t image_id = tgetimgid(&g);
++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
++ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
++ tgetimgrow(&g));
++ char stcommand[256] = {0};
++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0);
++ if (len > sizeof(stcommand) - 1) {
++ fprintf(stderr, "Executable name too long: %s\n",
++ argv0);
++ return;
++ }
++ gr_show_image_info(image_id, tgetimgplacementid(&g),
++ tgetimgcol(&g), tgetimgrow(&g),
++ tgetisclassicplaceholder(&g),
++ tgetimgdiacriticcount(&g), argv0);
++ }
++}
++
++void
++togglegrdebug(const Arg *arg)
++{
++ graphics_debug_mode = (graphics_debug_mode + 1) % 3;
++ redraw();
++}
++
++void
++dumpgrstate(const Arg *arg)
++{
++ gr_dump_state();
++}
++
++void
++unloadimages(const Arg *arg)
++{
++ gr_unload_images_to_reduce_ram();
++}
++
++void
++toggleimages(const Arg *arg)
++{
++ graphics_display_images = !graphics_display_images;
++ redraw();
++}
++
+ int
+ evcol(XEvent *e)
+ {
+- int x = e->xbutton.x - borderpx;
++ int x = e->xbutton.x - win.hborderpx;
+ LIMIT(x, 0, win.tw - 1);
+ return x / win.cw;
+ }
+@@ -339,7 +414,7 @@ evcol(XEvent *e)
+ int
+ evrow(XEvent *e)
+ {
+- int y = e->xbutton.y - borderpx;
++ int y = e->xbutton.y - win.vborderpx;
+ LIMIT(y, 0, win.th - 1);
+ return y / win.ch;
+ }
+@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release)
+ /* ignore Button<N>mask for Button<N> - it's set on release */
+ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
+
++ mouse_col = evcol(e);
++ mouse_row = evrow(e);
++
+ for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
+ if (ms->release == release &&
+ ms->button == e->xbutton.button &&
+@@ -739,6 +817,9 @@ cresize(int width, int height)
+ col = MAX(1, col);
+ row = MAX(1, row);
+
++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100;
++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100;
++
+ tresize(col, row);
+ xresize(col, row);
+ ttyresize(win.tw, win.th);
+@@ -869,8 +950,8 @@ xhints(void)
+ sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
+ sizeh->height = win.h;
+ sizeh->width = win.w;
+- sizeh->height_inc = win.ch;
+- sizeh->width_inc = win.cw;
++ sizeh->height_inc = 1;
++ sizeh->width_inc = 1;
+ sizeh->base_height = 2 * borderpx;
+ sizeh->base_width = 2 * borderpx;
+ sizeh->min_height = win.ch + 2 * borderpx;
+@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize)
+ FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12);
+ usedfontsize = 12;
+ }
+- defaultfontsize = usedfontsize;
++ if (defaultfontsize <= 0)
++ defaultfontsize = usedfontsize;
+ }
+
+ if (xloadfont(&dc.font, pattern))
+@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize)
+ FcPatternGetDouble(dc.font.match->pattern,
+ FC_PIXEL_SIZE, 0, &fontval);
+ usedfontsize = fontval;
+- if (fontsize == 0)
++ if (defaultfontsize <= 0 && fontsize == 0)
+ defaultfontsize = fontval;
+ }
+
+@@ -1152,8 +1234,8 @@ xinit(int cols, int rows)
+ xloadcols();
+
+ /* adjust fixed window geometry */
+- win.w = 2 * borderpx + cols * win.cw;
+- win.h = 2 * borderpx + rows * win.ch;
++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
+ if (xw.gm & XNegative)
+ xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
+ if (xw.gm & YNegative)
+@@ -1240,12 +1322,15 @@ xinit(int cols, int rows)
+ xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0);
+ if (xsel.xtarget == None)
+ xsel.xtarget = XA_STRING;
++
++ // Initialize the graphics (image display) module.
++ gr_init(xw.dpy, xw.vis, xw.cmap);
+ }
+
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
+ ushort mode, prevmode = USHRT_MAX;
+ Font *font = &dc.font;
+ int frcflags = FRC_NORMAL;
+@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
+ if (mode == ATTR_WDUMMY)
+ continue;
+
++ /* Draw spaces for image placeholders (images will be drawn
++ * separately). */
++ if (mode & ATTR_IMAGE)
++ rune = ' ';
++
+ /* Determine font for glyph if different from previous glyph. */
+ if (prevmode != mode) {
+ prevmode = mode;
+@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
+ return numspecs;
+ }
+
++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen`
++ * is the length of the dash plus the length of the gap. `fraction` is the
++ * fraction of the dash length compared to `wavelen`. */
++static void
++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w,
++ int wavelen, float fraction, int thick)
++{
++ int dashw = MAX(1, fraction * wavelen);
++ for (int i = x - x % wavelen; i < x + w; i += wavelen) {
++ int startx = MAX(i, x);
++ int endx = MIN(i + dashw, x + w);
++ if (startx < endx)
++ XftDrawRect(xw.draw, color, startx, y, endx - startx,
++ thick);
++ }
++}
++
++/* Draws an undercurl. `h` is the total height, including line thickness. */
++static void
++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick)
++{
++ XGCValues gcvals = {.foreground = color->pixel,
++ .line_width = thick,
++ .line_style = LineSolid,
++ .cap_style = CapRound};
++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw),
++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle,
++ &gcvals);
++
++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h};
++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted);
++
++ int yoffset = thick / 2;
++ int segh = MAX(1, h - thick);
++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't
++ * look good without antialiasing. */
++ int segw = segh;
++ int wavelen = MAX(1, segw * 2);
++
++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) {
++ XPoint points[3] = {{.x = i, .y = y + yoffset},
++ {.x = i + segw, .y = y + yoffset + segh},
++ {.x = i + wavelen, .y = y + yoffset}};
++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3,
++ CoordModeOrigin);
++ }
++
++ XFreeGC(xw.dpy, gc);
++}
++
+ void
+ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
+ {
+ int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
+- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
+ width = charlen * win.cw;
+ Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
+ XRenderColor colfg, colbg;
+@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
+
+ /* Intelligent cleaning up of the borders. */
+ if (x == 0) {
+- xclear(0, (y == 0)? 0 : winy, borderpx,
++ xclear(0, (y == 0)? 0 : winy, win.hborderpx,
+ winy + win.ch +
+- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
+ }
+- if (winx + width >= borderpx + win.tw) {
++ if (winx + width >= win.hborderpx + win.tw) {
+ xclear(winx + width, (y == 0)? 0 : winy, win.w,
+- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
+ }
+ if (y == 0)
+- xclear(winx, 0, winx + width, borderpx);
+- if (winy + win.ch >= borderpx + win.th)
++ xclear(winx, 0, winx + width, win.vborderpx);
++ if (winy + win.ch >= win.vborderpx + win.th)
+ xclear(winx, winy + win.ch, winx + width, win.h);
+
+ /* Clean up the region we want to draw to. */
+@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
+ r.width = width;
+ XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1);
+
+- /* Render the glyphs. */
+- XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
+-
+- /* Render underline and strikethrough. */
++ /* Decoration color. */
++ Color decor;
++ uint32_t decorcolor = tgetdecorcolor(&base);
++ if (decorcolor == DECOR_DEFAULT_COLOR) {
++ decor = *fg;
++ } else if (IS_TRUECOL(decorcolor)) {
++ colfg.alpha = 0xffff;
++ colfg.red = TRUERED(decorcolor);
++ colfg.green = TRUEGREEN(decorcolor);
++ colfg.blue = TRUEBLUE(decorcolor);
++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor);
++ } else {
++ decor = dc.col[decorcolor];
++ }
++ decor.color.alpha = 0xffff;
++ decor.pixel |= 0xff << 24;
++
++ /* Float thickness, used as a base to compute other values. */
++ float fthick = dc.font.height / 18.0;
++ /* Integer thickness in pixels. Must not be 0. */
++ int thick = MAX(1, roundf(fthick));
++ /* The default gap between the baseline and a single underline. */
++ int gap = roundf(fthick * 2);
++ /* The total thickness of a double underline. */
++ int doubleh = thick * 2 + ceilf(fthick * 0.5);
++ /* The total thickness of an undercurl. */
++ int curlh = thick * 2 + roundf(fthick * 0.75);
++
++ /* Render the underline before the glyphs. */
+ if (base.mode & ATTR_UNDERLINE) {
+- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
+- width, 1);
++ uint32_t style = tgetdecorstyle(&base);
++ int liney = winy + dc.font.ascent + gap;
++ /* Adjust liney to guarantee that a single underline fits. */
++ liney -= MAX(0, liney + thick - (winy + win.ch));
++ if (style == UNDERLINE_DOUBLE) {
++ liney -= MAX(0, liney + doubleh - (winy + win.ch));
++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
++ XftDrawRect(xw.draw, &decor, winx,
++ liney + doubleh - thick, width, thick);
++ } else if (style == UNDERLINE_DOTTED) {
++ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
++ thick * 2, 0.5, thick);
++ } else if (style == UNDERLINE_DASHED) {
++ int wavelen = MAX(2, win.cw * 0.9);
++ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
++ wavelen, 0.65, thick);
++ } else if (style == UNDERLINE_CURLY) {
++ liney -= MAX(0, liney + curlh - (winy + win.ch));
++ xdrawundercurl(xw.draw, &decor, winx, liney, width,
++ curlh, thick);
++ } else {
++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
++ }
+ }
+
++ /* Render the glyphs. */
++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
++
++ /* Render strikethrough. Alway use the fg color. */
+ if (base.mode & ATTR_STRUCK) {
+- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
+- width, 1);
++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3,
++ width, thick);
+ }
+
+ /* Reset clip to none. */
+@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y)
+
+ numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
+ xdrawglyphfontspecs(&spec, g, numspecs, x, y);
++ if (g.mode & ATTR_IMAGE) {
++ gr_start_drawing(xw.buf, win.cw, win.ch);
++ xdrawoneimagecell(g, x, y);
++ gr_finish_drawing(xw.buf);
++ }
+ }
+
+ void
+@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
+ if (IS_SET(MODE_HIDE))
+ return;
+
++ // If it's an image, just draw a ballot box for simplicity.
++ if (g.mode & ATTR_IMAGE)
++ g.u = 0x2610;
++
+ /*
+ * Select the right color for the right mode.
+ */
+@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
+ case 3: /* Blinking Underline */
+ case 4: /* Steady Underline */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - \
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - \
+ cursorthickness,
+ win.cw, cursorthickness);
+ break;
+ case 5: /* Blinking bar */
+ case 6: /* Steady bar */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ cursorthickness, win.ch);
+ break;
+ }
+ } else {
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ win.cw - 1, 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + (cx + 1) * win.cw - 1,
+- borderpx + cy * win.ch,
++ win.hborderpx + (cx + 1) * win.cw - 1,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - 1,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - 1,
+ win.cw, 1);
+ }
+ }
+
++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming
++ * that they have the same attributes (and thus the same lower 24 bits of the
++ * image ID and the same placement ID). */
++void
++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) {
++ int y_pix = win.vborderpx + y1 * win.ch;
++ uint32_t image_id_24bits = base.fg & 0xFFFFFF;
++ uint32_t placement_id = tgetimgplacementid(&base);
++ // Columns and rows are 1-based, 0 means unspecified.
++ int last_col = 0;
++ int last_row = 0;
++ int last_start_col = 0;
++ int last_start_x = x1;
++ // The most significant byte is also 1-base, subtract 1 before use.
++ uint32_t last_id_4thbyteplus1 = 0;
++ // We may need to inherit row/column/4th byte from the previous cell.
++ Glyph *prev = &line[x1 - 1];
++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) &&
++ (prev->fg & 0xFFFFFF) == image_id_24bits &&
++ prev->decor == base.decor) {
++ last_row = tgetimgrow(prev);
++ last_col = tgetimgcol(prev);
++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev);
++ last_start_col = last_col + 1;
++ }
++ for (int x = x1; x < x2; ++x) {
++ Glyph *g = &line[x];
++ uint32_t cur_row = tgetimgrow(g);
++ uint32_t cur_col = tgetimgcol(g);
++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g);
++ uint32_t num_diacritics = tgetimgdiacriticcount(g);
++ // If the row is not specified, assume it's the same as the row
++ // of the previous cell. Note that `cur_row` may contain a
++ // value imputed earlier, which will be preserved if `last_row`
++ // is zero (i.e. we don't know the row of the previous cell).
++ if (last_row && (num_diacritics == 0 || !cur_row))
++ cur_row = last_row;
++ // If the column is not specified and the row is the same as the
++ // row of the previous cell, then assume that the column is the
++ // next one.
++ if (last_col && (num_diacritics <= 1 || !cur_col) &&
++ cur_row == last_row)
++ cur_col = last_col + 1;
++ // If the additional id byte is not specified and the
++ // coordinates are consecutive, assume the byte is also the
++ // same.
++ if (last_id_4thbyteplus1 &&
++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) &&
++ cur_row == last_row && cur_col == last_col + 1)
++ cur_id_4thbyteplus1 = last_id_4thbyteplus1;
++ // If we couldn't infer row and column, start from the top left
++ // corner.
++ if (cur_row == 0)
++ cur_row = 1;
++ if (cur_col == 0)
++ cur_col = 1;
++ // If this cell breaks a contiguous stripe of image cells, draw
++ // that line and start a new one.
++ if (cur_col != last_col + 1 || cur_row != last_row ||
++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) {
++ uint32_t image_id = image_id_24bits;
++ if (last_id_4thbyteplus1)
++ image_id |= (last_id_4thbyteplus1 - 1) << 24;
++ if (last_row != 0) {
++ int x_pix =
++ win.hborderpx + last_start_x * win.cw;
++ gr_append_imagerect(
++ xw.buf, image_id, placement_id,
++ last_start_col - 1, last_col,
++ last_row - 1, last_row, last_start_x,
++ y1, x_pix, y_pix, win.cw, win.ch,
++ base.mode & ATTR_REVERSE);
++ }
++ last_start_col = cur_col;
++ last_start_x = x;
++ }
++ last_row = cur_row;
++ last_col = cur_col;
++ last_id_4thbyteplus1 = cur_id_4thbyteplus1;
++ // Populate the missing glyph data to enable inheritance between
++ // runs and support the naive implementation of tgetimgid.
++ if (!tgetimgrow(g))
++ tsetimgrow(g, cur_row);
++ // We cannot save this information if there are > 511 cols.
++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0)
++ tsetimgcol(g, cur_col);
++ if (!tgetimgid4thbyteplus1(g))
++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1);
++ }
++ uint32_t image_id = image_id_24bits;
++ if (last_id_4thbyteplus1)
++ image_id |= (last_id_4thbyteplus1 - 1) << 24;
++ // Draw the last contiguous stripe.
++ if (last_row != 0) {
++ int x_pix = win.hborderpx + last_start_x * win.cw;
++ gr_append_imagerect(xw.buf, image_id, placement_id,
++ last_start_col - 1, last_col, last_row - 1,
++ last_row, last_start_x, y1, x_pix, y_pix,
++ win.cw, win.ch, base.mode & ATTR_REVERSE);
++ }
++}
++
++/* Draw just one image cell without inheriting attributes from the left. */
++void xdrawoneimagecell(Glyph g, int x, int y) {
++ if (!(g.mode & ATTR_IMAGE))
++ return;
++ int x_pix = win.hborderpx + x * win.cw;
++ int y_pix = win.vborderpx + y * win.ch;
++ uint32_t row = tgetimgrow(&g) - 1;
++ uint32_t col = tgetimgcol(&g) - 1;
++ uint32_t placement_id = tgetimgplacementid(&g);
++ uint32_t image_id = tgetimgid(&g);
++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row,
++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch,
++ g.mode & ATTR_REVERSE);
++}
++
++/* Prepare for image drawing. */
++void xstartimagedraw(int *dirty, int rows) {
++ gr_start_drawing(xw.buf, win.cw, win.ch);
++ gr_mark_dirty_animations(dirty, rows);
++}
++
++/* Draw all queued image cells. */
++void xfinishimagedraw() {
++ gr_finish_drawing(xw.buf);
++}
++
+ void
+ xsetenv(void)
+ {
+@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2)
+ new.mode ^= ATTR_REVERSE;
+ if (i > 0 && ATTRCMP(base, new)) {
+ xdrawglyphfontspecs(specs, base, i, ox, y1);
++ if (base.mode & ATTR_IMAGE)
++ xdrawimages(base, line, ox, y1, x);
+ specs += i;
+ numspecs -= i;
+ i = 0;
+@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2)
+ }
+ if (i > 0)
+ xdrawglyphfontspecs(specs, base, i, ox, y1);
++ if (i > 0 && base.mode & ATTR_IMAGE)
++ xdrawimages(base, line, ox, y1, x);
+ }
+
+ void
+@@ -1907,6 +2238,7 @@ cmessage(XEvent *e)
+ }
+ } else if (e->xclient.data.l[0] == xw.wmdeletewin) {
+ ttyhangup();
++ gr_deinit();
+ exit(0);
+ }
+ }
+@@ -1957,6 +2289,13 @@ run(void)
+ if (XPending(xw.dpy))
+ timeout = 0; /* existing events might not set xfd */
+
++ /* Decrease the timeout if there are active animations. */
++ if (graphics_next_redraw_delay != INT_MAX &&
++ IS_SET(MODE_VISIBLE))
++ timeout = timeout < 0 ? graphics_next_redraw_delay
++ : MIN(timeout,
++ graphics_next_redraw_delay);
++
+ seltv.tv_sec = timeout / 1E3;
+ seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec);
+ tv = timeout >= 0 ? &seltv : NULL;
+--
+2.43.0
+
diff --git a/files/config/suckless/st/rowcolumn_diacritics_helpers.c b/files/config/suckless/st/rowcolumn_diacritics_helpers.c
new file mode 100644
index 0000000..829c0fc
--- /dev/null
+++ b/files/config/suckless/st/rowcolumn_diacritics_helpers.c
@@ -0,0 +1,391 @@
+#include <stdint.h>
+
+uint16_t diacritic_to_num(uint32_t code)
+{
+ switch (code) {
+ case 0x305:
+ return code - 0x305 + 1;
+ case 0x30d:
+ case 0x30e:
+ return code - 0x30d + 2;
+ case 0x310:
+ return code - 0x310 + 4;
+ case 0x312:
+ return code - 0x312 + 5;
+ case 0x33d:
+ case 0x33e:
+ case 0x33f:
+ return code - 0x33d + 6;
+ case 0x346:
+ return code - 0x346 + 9;
+ case 0x34a:
+ case 0x34b:
+ case 0x34c:
+ return code - 0x34a + 10;
+ case 0x350:
+ case 0x351:
+ case 0x352:
+ return code - 0x350 + 13;
+ case 0x357:
+ return code - 0x357 + 16;
+ case 0x35b:
+ return code - 0x35b + 17;
+ case 0x363:
+ case 0x364:
+ case 0x365:
+ case 0x366:
+ case 0x367:
+ case 0x368:
+ case 0x369:
+ case 0x36a:
+ case 0x36b:
+ case 0x36c:
+ case 0x36d:
+ case 0x36e:
+ case 0x36f:
+ return code - 0x363 + 18;
+ case 0x483:
+ case 0x484:
+ case 0x485:
+ case 0x486:
+ case 0x487:
+ return code - 0x483 + 31;
+ case 0x592:
+ case 0x593:
+ case 0x594:
+ case 0x595:
+ return code - 0x592 + 36;
+ case 0x597:
+ case 0x598:
+ case 0x599:
+ return code - 0x597 + 40;
+ case 0x59c:
+ case 0x59d:
+ case 0x59e:
+ case 0x59f:
+ case 0x5a0:
+ case 0x5a1:
+ return code - 0x59c + 43;
+ case 0x5a8:
+ case 0x5a9:
+ return code - 0x5a8 + 49;
+ case 0x5ab:
+ case 0x5ac:
+ return code - 0x5ab + 51;
+ case 0x5af:
+ return code - 0x5af + 53;
+ case 0x5c4:
+ return code - 0x5c4 + 54;
+ case 0x610:
+ case 0x611:
+ case 0x612:
+ case 0x613:
+ case 0x614:
+ case 0x615:
+ case 0x616:
+ case 0x617:
+ return code - 0x610 + 55;
+ case 0x657:
+ case 0x658:
+ case 0x659:
+ case 0x65a:
+ case 0x65b:
+ return code - 0x657 + 63;
+ case 0x65d:
+ case 0x65e:
+ return code - 0x65d + 68;
+ case 0x6d6:
+ case 0x6d7:
+ case 0x6d8:
+ case 0x6d9:
+ case 0x6da:
+ case 0x6db:
+ case 0x6dc:
+ return code - 0x6d6 + 70;
+ case 0x6df:
+ case 0x6e0:
+ case 0x6e1:
+ case 0x6e2:
+ return code - 0x6df + 77;
+ case 0x6e4:
+ return code - 0x6e4 + 81;
+ case 0x6e7:
+ case 0x6e8:
+ return code - 0x6e7 + 82;
+ case 0x6eb:
+ case 0x6ec:
+ return code - 0x6eb + 84;
+ case 0x730:
+ return code - 0x730 + 86;
+ case 0x732:
+ case 0x733:
+ return code - 0x732 + 87;
+ case 0x735:
+ case 0x736:
+ return code - 0x735 + 89;
+ case 0x73a:
+ return code - 0x73a + 91;
+ case 0x73d:
+ return code - 0x73d + 92;
+ case 0x73f:
+ case 0x740:
+ case 0x741:
+ return code - 0x73f + 93;
+ case 0x743:
+ return code - 0x743 + 96;
+ case 0x745:
+ return code - 0x745 + 97;
+ case 0x747:
+ return code - 0x747 + 98;
+ case 0x749:
+ case 0x74a:
+ return code - 0x749 + 99;
+ case 0x7eb:
+ case 0x7ec:
+ case 0x7ed:
+ case 0x7ee:
+ case 0x7ef:
+ case 0x7f0:
+ case 0x7f1:
+ return code - 0x7eb + 101;
+ case 0x7f3:
+ return code - 0x7f3 + 108;
+ case 0x816:
+ case 0x817:
+ case 0x818:
+ case 0x819:
+ return code - 0x816 + 109;
+ case 0x81b:
+ case 0x81c:
+ case 0x81d:
+ case 0x81e:
+ case 0x81f:
+ case 0x820:
+ case 0x821:
+ case 0x822:
+ case 0x823:
+ return code - 0x81b + 113;
+ case 0x825:
+ case 0x826:
+ case 0x827:
+ return code - 0x825 + 122;
+ case 0x829:
+ case 0x82a:
+ case 0x82b:
+ case 0x82c:
+ case 0x82d:
+ return code - 0x829 + 125;
+ case 0x951:
+ return code - 0x951 + 130;
+ case 0x953:
+ case 0x954:
+ return code - 0x953 + 131;
+ case 0xf82:
+ case 0xf83:
+ return code - 0xf82 + 133;
+ case 0xf86:
+ case 0xf87:
+ return code - 0xf86 + 135;
+ case 0x135d:
+ case 0x135e:
+ case 0x135f:
+ return code - 0x135d + 137;
+ case 0x17dd:
+ return code - 0x17dd + 140;
+ case 0x193a:
+ return code - 0x193a + 141;
+ case 0x1a17:
+ return code - 0x1a17 + 142;
+ case 0x1a75:
+ case 0x1a76:
+ case 0x1a77:
+ case 0x1a78:
+ case 0x1a79:
+ case 0x1a7a:
+ case 0x1a7b:
+ case 0x1a7c:
+ return code - 0x1a75 + 143;
+ case 0x1b6b:
+ return code - 0x1b6b + 151;
+ case 0x1b6d:
+ case 0x1b6e:
+ case 0x1b6f:
+ case 0x1b70:
+ case 0x1b71:
+ case 0x1b72:
+ case 0x1b73:
+ return code - 0x1b6d + 152;
+ case 0x1cd0:
+ case 0x1cd1:
+ case 0x1cd2:
+ return code - 0x1cd0 + 159;
+ case 0x1cda:
+ case 0x1cdb:
+ return code - 0x1cda + 162;
+ case 0x1ce0:
+ return code - 0x1ce0 + 164;
+ case 0x1dc0:
+ case 0x1dc1:
+ return code - 0x1dc0 + 165;
+ case 0x1dc3:
+ case 0x1dc4:
+ case 0x1dc5:
+ case 0x1dc6:
+ case 0x1dc7:
+ case 0x1dc8:
+ case 0x1dc9:
+ return code - 0x1dc3 + 167;
+ case 0x1dcb:
+ case 0x1dcc:
+ return code - 0x1dcb + 174;
+ case 0x1dd1:
+ case 0x1dd2:
+ case 0x1dd3:
+ case 0x1dd4:
+ case 0x1dd5:
+ case 0x1dd6:
+ case 0x1dd7:
+ case 0x1dd8:
+ case 0x1dd9:
+ case 0x1dda:
+ case 0x1ddb:
+ case 0x1ddc:
+ case 0x1ddd:
+ case 0x1dde:
+ case 0x1ddf:
+ case 0x1de0:
+ case 0x1de1:
+ case 0x1de2:
+ case 0x1de3:
+ case 0x1de4:
+ case 0x1de5:
+ case 0x1de6:
+ return code - 0x1dd1 + 176;
+ case 0x1dfe:
+ return code - 0x1dfe + 198;
+ case 0x20d0:
+ case 0x20d1:
+ return code - 0x20d0 + 199;
+ case 0x20d4:
+ case 0x20d5:
+ case 0x20d6:
+ case 0x20d7:
+ return code - 0x20d4 + 201;
+ case 0x20db:
+ case 0x20dc:
+ return code - 0x20db + 205;
+ case 0x20e1:
+ return code - 0x20e1 + 207;
+ case 0x20e7:
+ return code - 0x20e7 + 208;
+ case 0x20e9:
+ return code - 0x20e9 + 209;
+ case 0x20f0:
+ return code - 0x20f0 + 210;
+ case 0x2cef:
+ case 0x2cf0:
+ case 0x2cf1:
+ return code - 0x2cef + 211;
+ case 0x2de0:
+ case 0x2de1:
+ case 0x2de2:
+ case 0x2de3:
+ case 0x2de4:
+ case 0x2de5:
+ case 0x2de6:
+ case 0x2de7:
+ case 0x2de8:
+ case 0x2de9:
+ case 0x2dea:
+ case 0x2deb:
+ case 0x2dec:
+ case 0x2ded:
+ case 0x2dee:
+ case 0x2def:
+ case 0x2df0:
+ case 0x2df1:
+ case 0x2df2:
+ case 0x2df3:
+ case 0x2df4:
+ case 0x2df5:
+ case 0x2df6:
+ case 0x2df7:
+ case 0x2df8:
+ case 0x2df9:
+ case 0x2dfa:
+ case 0x2dfb:
+ case 0x2dfc:
+ case 0x2dfd:
+ case 0x2dfe:
+ case 0x2dff:
+ return code - 0x2de0 + 214;
+ case 0xa66f:
+ return code - 0xa66f + 246;
+ case 0xa67c:
+ case 0xa67d:
+ return code - 0xa67c + 247;
+ case 0xa6f0:
+ case 0xa6f1:
+ return code - 0xa6f0 + 249;
+ case 0xa8e0:
+ case 0xa8e1:
+ case 0xa8e2:
+ case 0xa8e3:
+ case 0xa8e4:
+ case 0xa8e5:
+ case 0xa8e6:
+ case 0xa8e7:
+ case 0xa8e8:
+ case 0xa8e9:
+ case 0xa8ea:
+ case 0xa8eb:
+ case 0xa8ec:
+ case 0xa8ed:
+ case 0xa8ee:
+ case 0xa8ef:
+ case 0xa8f0:
+ case 0xa8f1:
+ return code - 0xa8e0 + 251;
+ case 0xaab0:
+ return code - 0xaab0 + 269;
+ case 0xaab2:
+ case 0xaab3:
+ return code - 0xaab2 + 270;
+ case 0xaab7:
+ case 0xaab8:
+ return code - 0xaab7 + 272;
+ case 0xaabe:
+ case 0xaabf:
+ return code - 0xaabe + 274;
+ case 0xaac1:
+ return code - 0xaac1 + 276;
+ case 0xfe20:
+ case 0xfe21:
+ case 0xfe22:
+ case 0xfe23:
+ case 0xfe24:
+ case 0xfe25:
+ case 0xfe26:
+ return code - 0xfe20 + 277;
+ case 0x10a0f:
+ return code - 0x10a0f + 284;
+ case 0x10a38:
+ return code - 0x10a38 + 285;
+ case 0x1d185:
+ case 0x1d186:
+ case 0x1d187:
+ case 0x1d188:
+ case 0x1d189:
+ return code - 0x1d185 + 286;
+ case 0x1d1aa:
+ case 0x1d1ab:
+ case 0x1d1ac:
+ case 0x1d1ad:
+ return code - 0x1d1aa + 291;
+ case 0x1d242:
+ case 0x1d243:
+ case 0x1d244:
+ return code - 0x1d242 + 295;
+ }
+ return 0;
+}
diff --git a/files/config/suckless/st/st b/files/config/suckless/st/st
deleted file mode 100755
index 033be3f..0000000
--- a/files/config/suckless/st/st
+++ /dev/null
Binary files differ
diff --git a/files/config/suckless/st/st.c b/files/config/suckless/st/st.c
index 03e10d1..f634c9a 100644
--- a/files/config/suckless/st/st.c
+++ b/files/config/suckless/st/st.c
@@ -19,6 +19,7 @@
#include "st.h"
#include "win.h"
+#include "graphics.h"
#if defined(__linux)
#include <pty.h>
@@ -37,6 +38,14 @@
#define STR_ARG_SIZ ESC_ARG_SIZ
#define HISTSIZE 2000
+static inline void tsetimgrow(Glyph *g, uint32_t row) {
+ g->u.img.row = row;
+}
+
+/* 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)
@@ -117,6 +126,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 */
Line hist[HISTSIZE]; /* history buffer */
@@ -220,7 +231,6 @@ static Rune utf8decodebyte(char, size_t *);
static char utf8encodebyte(Rune, size_t);
static size_t utf8validate(Rune *, size_t);
-static char *base64dec(const char *);
static char base64dec_getc(const char **);
static ssize_t xwrite(int, const char *, size_t);
@@ -239,6 +249,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)
{
@@ -623,6 +637,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);
}
@@ -826,7 +846,11 @@ ttyread(void)
{
static char buf[BUFSIZ];
static int buflen = 0;
- int ret, written;
+ static int already_processing = 0;
+ int ret, written = 0;
+
+ if (buflen >= LEN(buf))
+ return 0;
/* append read bytes to unprocessed bytes */
ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
@@ -838,7 +862,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)
@@ -884,6 +925,7 @@ ttywriteraw(const char *s, size_t n)
fd_set wfd, rfd;
ssize_t r;
size_t lim = 256;
+ int retries_left = 100;
/*
* Remember that we are using a pty, which might be a modem line.
@@ -892,6 +934,9 @@ ttywriteraw(const char *s, size_t n)
* FIXME: Migrate the world to Plan 9.
*/
while (n > 0) {
+ if (retries_left-- <= 0)
+ goto too_many_retries;
+
FD_ZERO(&wfd);
FD_ZERO(&rfd);
FD_SET(cmdfd, &wfd);
@@ -933,11 +978,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;
@@ -1025,7 +1075,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));
@@ -1048,7 +1099,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();
}
@@ -1309,12 +1362,104 @@ tclearregion(int x1, int y1, int x2, int y2)
selclear();
gp->fg = term.c.attr.fg;
gp->bg = term.c.attr.bg;
+ gp->decor = term.c.attr.decor;
gp->mode = 0;
gp->u = ' ';
}
}
}
+/// Fills a rectangle area with an image placeholder. The starting point is the
+/// cursor. Adds empty lines if needed. The placeholder will be marked as
+/// classic.
+void
+tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id,
+ int cols, int rows, char do_not_move_cursor)
+{
+ for (int row = 0; row < rows; ++row) {
+ int y = term.c.y;
+ term.dirty[y] = 1;
+ for (int col = 0; col < cols; ++col) {
+ int x = term.c.x + col;
+ if (x >= term.col)
+ break;
+ Glyph *gp = &term.line[y][x];
+ if (selected(x, y))
+ selclear();
+ gp->mode = ATTR_IMAGE;
+ gp->u = 0;
+ tsetimgrow(gp, row + 1);
+ tsetimgcol(gp, col + 1);
+ tsetimgid(gp, image_id);
+ tsetimgplacementid(gp, placement_id);
+ tsetimgdiacriticcount(gp, 3);
+ tsetisclassicplaceholder(gp, 1);
+ }
+ // If moving the cursor is not allowed and this is the last line
+ // of the terminal, we are done.
+ if (do_not_move_cursor && y == term.row - 1)
+ break;
+ // Move the cursor down, maybe creating a new line. The x is
+ // preserved (we never change term.c.x in the loop above).
+ if (row != rows - 1)
+ tnewline(/*first_col=*/0);
+ }
+ if (do_not_move_cursor) {
+ // Return the cursor to the original position.
+ tmoveto(term.c.x, term.c.y - rows + 1);
+ } else {
+ // Move the cursor beyond the last column, as required by the
+ // protocol. If the cursor goes beyond the screen edge, insert a
+ // newline to match the behavior of kitty.
+ if (term.c.x + cols >= term.col)
+ tnewline(/*first_col=*/1);
+ else
+ tmoveto(term.c.x + cols, term.c.y);
+ }
+}
+
+void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
+ uint32_t placement_id, int col,
+ int row, char is_classic),
+ void *data) {
+ for (int row = 0; row < term.row; ++row) {
+ for (int col = 0; col < term.col; ++col) {
+ Glyph *gp = &term.line[row][col];
+ if (gp->mode & ATTR_IMAGE) {
+ uint32_t image_id = tgetimgid(gp);
+ uint32_t placement_id = tgetimgplacementid(gp);
+ int ret =
+ callback(data, tgetimgid(gp),
+ tgetimgplacementid(gp),
+ tgetimgcol(gp), tgetimgrow(gp),
+ tgetisclassicplaceholder(gp));
+ if (ret == 1) {
+ term.dirty[row] = 1;
+ gp->mode = 0;
+ gp->u = ' ';
+ }
+ }
+ }
+ }
+}
+
+void gr_schedule_image_redraw_by_id(uint32_t image_id) {
+ for (int row = 0; row < term.row; ++row) {
+ if (term.dirty[row])
+ continue;
+ for (int col = 0; col < term.col; ++col) {
+ Glyph *gp = &term.line[row][col];
+ if (gp->mode & ATTR_IMAGE) {
+ uint32_t cell_image_id = tgetimgid(gp);
+ if (cell_image_id == image_id) {
+ term.dirty[row] = 1;
+ break;
+ }
+ }
+ }
+ }
+}
+
void
tdeletechar(int n)
{
@@ -1433,6 +1578,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;
@@ -1445,6 +1591,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 */
@@ -1468,6 +1628,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;
@@ -1495,6 +1656,13 @@ tsetattr(const int *attr, int l)
case 49:
term.c.attr.bg = defaultbg;
break;
+ case 58:
+ if ((idx = tdefcolor(attr, &i, l)) >= 0)
+ tsetdecorcolor(&term.c.attr, idx);
+ break;
+ case 59:
+ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR);
+ break;
default:
if (BETWEEN(attr[i], 30, 37)) {
term.c.attr.fg = attr[i] - 30;
@@ -1878,6 +2046,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;
}
}
@@ -2027,8 +2228,26 @@ strhandle(void)
case 'k': /* old title set compatibility */
xsettitle(strescseq.args[0]);
return;
- case 'P': /* DCS -- Device Control String */
case '_': /* APC -- Application Program Command */
+ if (gr_parse_command(strescseq.buf, strescseq.len)) {
+ GraphicsCommandResult *res = &graphics_command_result;
+ if (res->create_placeholder) {
+ tcreateimgplaceholder(
+ res->placeholder.image_id,
+ res->placeholder.placement_id,
+ res->placeholder.columns,
+ res->placeholder.rows,
+ res->placeholder.do_not_move_cursor);
+ }
+ if (res->response[0])
+ ttywrite(res->response, strlen(res->response),
+ 0);
+ if (res->redraw)
+ tfulldirt();
+ return;
+ }
+ return;
+ case 'P': /* DCS -- Device Control String */
case '^': /* PM -- Privacy Message */
return;
}
@@ -2534,6 +2753,33 @@ check_control_code:
if (selected(term.c.x, term.c.y))
selclear();
+ if (width == 0) {
+ // It's probably a combining char. Combining characters are not
+ // supported, so we just ignore them, unless it denotes the row and
+ // column of an image character.
+ if (term.c.y <= 0 && term.c.x <= 0)
+ return;
+ else if (term.c.x == 0)
+ gp = &term.line[term.c.y-1][term.col-1];
+ else if (term.c.state & CURSOR_WRAPNEXT)
+ gp = &term.line[term.c.y][term.c.x];
+ else
+ gp = &term.line[term.c.y][term.c.x-1];
+ uint16_t num = diacritic_to_num(u);
+ if (num && (gp->mode & ATTR_IMAGE)) {
+ unsigned diaccount = tgetimgdiacriticcount(gp);
+ if (diaccount == 0)
+ tsetimgrow(gp, num);
+ else if (diaccount == 1)
+ tsetimgcol(gp, num);
+ else if (diaccount == 2)
+ tsetimg4thbyteplus1(gp, num);
+ tsetimgdiacriticcount(gp, diaccount + 1);
+ }
+ term.lastc = u;
+ return;
+ }
+
gp = &term.line[term.c.y][term.c.x];
if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
gp->mode |= ATTR_WRAP;
@@ -2708,6 +2954,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;
@@ -2715,6 +2963,8 @@ drawregion(int x1, int y1, int x2, int y2)
term.dirty[y] = 0;
xdrawline(TLINE(y), x1, y, x2);
}
+
+ xfinishimagedraw();
}
void
@@ -2750,3 +3000,9 @@ redraw(void)
tfulldirt();
draw();
}
+
+Glyph
+getglyphat(int col, int row)
+{
+ return term.line[row][col];
+}
diff --git a/files/config/suckless/st/st.c.orig b/files/config/suckless/st/st.c.orig
index 3370e7e..03e10d1 100644
--- a/files/config/suckless/st/st.c.orig
+++ b/files/config/suckless/st/st.c.orig
@@ -1280,6 +1280,9 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
term.dirty[y] = 1;
term.line[y][x] = *attr;
term.line[y][x].u = u;
+
+ if (isboxdraw(u))
+ term.line[y][x].mode |= ATTR_BOXDRAW;
}
void
diff --git a/files/config/suckless/st/st.c.rej b/files/config/suckless/st/st.c.rej
new file mode 100644
index 0000000..81bdbd9
--- /dev/null
+++ b/files/config/suckless/st/st.c.rej
@@ -0,0 +1,27 @@
+--- st.c
++++ st.c
+@@ -1264,9 +1313,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
diff --git a/files/config/suckless/st/st.h b/files/config/suckless/st/st.h
index 99b4e2b..a611939 100644
--- a/files/config/suckless/st/st.h
+++ b/files/config/suckless/st/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,
@@ -35,6 +39,7 @@ enum glyph_attribute {
ATTR_WDUMMY = 1 << 10,
ATTR_BOXDRAW = 1 << 11,
ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
+ ATTR_IMAGE = 1 << 14,
};
enum selection_mode {
@@ -43,6 +48,11 @@ enum selection_mode {
SEL_READY = 2
};
+static inline void tsetimgrow(Glyph *g, uint32_t row) {
+ g->u.img.row = row;
+}
+
+
enum selection_type {
SEL_REGULAR = 1,
SEL_RECTANGULAR = 2
@@ -53,6 +63,14 @@ enum selection_snap {
SNAP_LINE = 2
};
+enum underline_style {
+ UNDERLINE_STRAIGHT = 1,
+ UNDERLINE_DOUBLE = 2,
+ UNDERLINE_CURLY = 3,
+ UNDERLINE_DOTTED = 4,
+ UNDERLINE_DASHED = 5,
+};
+
typedef unsigned char uchar;
typedef unsigned int uint;
typedef unsigned long ulong;
@@ -66,6 +84,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;
@@ -108,6 +127,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);
diff --git a/files/config/suckless/st/st.h.orig b/files/config/suckless/st/st.h.orig
index 818a6f8..99b4e2b 100644
--- a/files/config/suckless/st/st.h.orig
+++ b/files/config/suckless/st/st.h.orig
@@ -33,6 +33,7 @@ enum glyph_attribute {
ATTR_WRAP = 1 << 8,
ATTR_WIDE = 1 << 9,
ATTR_WDUMMY = 1 << 10,
+ ATTR_BOXDRAW = 1 << 11,
ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
};
@@ -113,6 +114,14 @@ void *xmalloc(size_t);
void *xrealloc(void *, size_t);
char *xstrdup(const char *);
+int isboxdraw(Rune);
+ushort boxdrawindex(const Glyph *);
+#ifdef XFT_VERSION
+/* only exposed to x.c, otherwise we'll need Xft.h for the types */
+void boxdraw_xinit(Display *, Colormap, XftDraw *, Visual *);
+void drawboxes(int, int, int, int, XftColor *, XftColor *, const XftGlyphFontSpec *, int);
+#endif
+
/* config.h globals */
extern char *utmp;
extern char *scroll;
@@ -120,9 +129,12 @@ extern char *stty_args;
extern char *vtiden;
extern wchar_t *worddelimiters;
extern int allowaltscreen;
+extern int boxdraw, boxdraw_braille, boxdraw_bold;
extern int allowwindowops;
extern char *termname;
extern unsigned int tabspaces;
extern unsigned int defaultfg;
extern unsigned int defaultbg;
extern unsigned int defaultcs;
+extern int boxdraw, boxdraw_bold, boxdraw_braille;
+
diff --git a/files/config/suckless/st/st.h.rej b/files/config/suckless/st/st.h.rej
new file mode 100644
index 0000000..0434f72
--- /dev/null
+++ b/files/config/suckless/st/st.h.rej
@@ -0,0 +1,72 @@
+--- st.h
++++ st.h
+@@ -140,3 +156,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/files/config/suckless/st/st.info b/files/config/suckless/st/st.info
index efab2cf..ded76c1 100644
--- a/files/config/suckless/st/st.info
+++ b/files/config/suckless/st/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/files/config/suckless/st/st.o b/files/config/suckless/st/st.o
deleted file mode 100644
index 2e1ba07..0000000
--- a/files/config/suckless/st/st.o
+++ /dev/null
Binary files differ
diff --git a/files/config/suckless/st/win.h b/files/config/suckless/st/win.h
index 6de960d..31b3fff 100644
--- a/files/config/suckless/st/win.h
+++ b/files/config/suckless/st/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/files/config/suckless/st/x.c b/files/config/suckless/st/x.c
index 7157e56..c2c890d 100644
--- a/files/config/suckless/st/x.c
+++ b/files/config/suckless/st/x.c
@@ -4,6 +4,8 @@
#include <limits.h>
#include <locale.h>
#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
@@ -19,30 +21,31 @@ 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;
+ 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;
+ 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 */
+ 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 */
@@ -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"
@@ -79,71 +88,75 @@ typedef XftGlyphFontSpec GlyphFontSpec;
/* Purely graphic info */
typedef struct {
- int tw, th; /* tty width and height */
- int w, h; /* window width and height */
- int ch; /* char height */
- int cw; /* char width */
- int mode; /* window state/mode flags */
- int cursor; /* cursor style */
+ 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 */
+ 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;
+ 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;
+ 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;
+ Color *col;
+ size_t collen;
+ Font font, bfont, ifont, ibfont;
+ GC gc;
} DC;
static inline ushort sixd_to_16bit(int);
+static inline void tsetimgrow(Glyph *g, uint32_t row) {
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 *);
@@ -190,29 +203,29 @@ 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,
+ [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 */
@@ -220,19 +233,20 @@ 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
+ FRC_NORMAL,
+ FRC_ITALIC,
+ FRC_BOLD,
+ FRC_ITALICBOLD
};
typedef struct {
- XftFont *font;
- int flags;
- Rune unicodep;
+ XftFont *font;
+ int flags;
+ Rune unicodep;
} Fontcache;
/* Fontcache is an array now. A new font will be appended to the array. */
@@ -254,1866 +268,2190 @@ static char *opt_title = NULL;
static uint buttons; /* bit field of pressed buttons */
-void
+ void
clipcopy(const Arg *dummy)
{
- Atom clipboard;
+ Atom clipboard;
- free(xsel.clipboard);
- xsel.clipboard = NULL;
+ 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);
- }
+ if (xsel.primary != NULL) {
+ xsel.clipboard = xstrdup(xsel.primary);
+ clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0);
+ XSetSelectionOwner(xw.dpy, clipboard, xw.win, CurrentTime);
+ }
}
-void
+ void
clippaste(const Arg *dummy)
{
- Atom clipboard;
+ Atom clipboard;
- clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0);
- XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard,
- xw.win, CurrentTime);
+ clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0);
+ XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard,
+ xw.win, CurrentTime);
}
-void
+ void
selpaste(const Arg *dummy)
{
- XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY,
- xw.win, CurrentTime);
+ XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY,
+ xw.win, CurrentTime);
}
-void
+ void
numlock(const Arg *dummy)
{
- win.mode ^= MODE_NUMLOCK;
+ win.mode ^= MODE_NUMLOCK;
}
-void
+ void
zoom(const Arg *arg)
{
- Arg larg;
+ Arg larg;
- larg.f = usedfontsize + arg->f;
- zoomabs(&larg);
+ larg.f = usedfontsize + arg->f;
+ zoomabs(&larg);
}
-void
+ void
zoomabs(const Arg *arg)
{
- xunloadfonts();
- xloadfonts(usedfont, arg->f);
- cresize(0, 0);
- redraw();
- xhints();
+ xunloadfonts();
+ xloadfonts(usedfont, arg->f);
+ cresize(0, 0);
+ redraw();
+ xhints();
}
-void
+ void
zoomreset(const Arg *arg)
{
- Arg larg;
+ Arg larg;
- if (defaultfontsize > 0) {
- larg.f = defaultfontsize;
- zoomabs(&larg);
- }
+ if (defaultfontsize > 0) {
+ larg.f = defaultfontsize;
+ zoomabs(&larg);
+ }
}
-void
+ void
ttysend(const Arg *arg)
{
- ttywrite(arg->s, strlen(arg->s), 1);
+ 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();
}
-int
+ void
+toggleimages(const Arg *arg)
+{
+ graphics_display_images = !graphics_display_images;
+ redraw();
+}
+
+ int
evcol(XEvent *e)
{
- int x = e->xbutton.x - borderpx;
- LIMIT(x, 0, win.tw - 1);
- return x / win.cw;
+ int x = e->xbutton.x - win.hborderpx;
+ LIMIT(x, 0, win.tw - 1);
+ return x / win.cw;
}
-int
+ int
evrow(XEvent *e)
{
- int y = e->xbutton.y - borderpx;
- LIMIT(y, 0, win.th - 1);
- return y / win.ch;
+ int y = e->xbutton.y - win.vborderpx;
+ LIMIT(y, 0, win.th - 1);
+ return y / win.ch;
}
-void
+ 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);
+ 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
+ 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
+ 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;
+ return button == Button1 ? Button1Mask
+ : button == Button2 ? Button2Mask
+ : button == Button3 ? Button3Mask
+ : button == Button4 ? Button4Mask
+ : button == Button5 ? Button5Mask
+ : 0;
}
-int
+ int
mouseaction(XEvent *e, uint release)
{
- MouseShortcut *ms;
-
- /* ignore Button<N>mask for Button<N> - it's set on release */
- uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
-
- 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;
+ MouseShortcut *ms;
+
+ /* ignore Button<N>mask for Button<N> - it's set on release */
+ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
+
+ mouse_col = evcol(e);
+ mouse_row = evrow(e);
+
+ for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
+ if (ms->release == release &&
+ ms->button == e->xbutton.button &&
+ (match(ms->mod, state) || /* exact or forced */
+ match(ms->mod, state & ~forcemousemod))) {
+ ms->func(&(ms->arg));
+ return 1;
+ }
+ }
+
+ return 0;
}
-void
+ 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
+ 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);
- }
+ 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
+ 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
+ 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);
+ clipcopy(NULL);
}
-void
+ void
selclear_(XEvent *e)
{
- selclear();
+ selclear();
}
-void
+ 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
+ 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;
+ if (!str)
+ return;
- free(xsel.primary);
- xsel.primary = str;
+ free(xsel.primary);
+ xsel.primary = str;
- XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t);
- if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win)
- selclear();
+ XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t);
+ if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win)
+ selclear();
}
-void
+ void
xsetsel(char *str)
{
- setsel(str, CurrentTime);
+ setsel(str, CurrentTime);
}
-void
+ void
brelease(XEvent *e)
{
- int btn = e->xbutton.button;
+ int btn = e->xbutton.button;
- if (1 <= btn && btn <= 11)
- buttons &= ~(1 << (btn-1));
+ if (1 <= btn && btn <= 11)
+ buttons &= ~(1 << (btn-1));
- if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
- mousereport(e);
- return;
- }
+ if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
+ mousereport(e);
+ return;
+ }
- if (mouseaction(e, 1))
- return;
- if (btn == Button1)
- mousesel(e, 1);
+ if (mouseaction(e, 1))
+ return;
+ if (btn == Button1)
+ mousesel(e, 1);
}
-void
+ void
bmotion(XEvent *e)
{
- if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
- mousereport(e);
- return;
- }
+ if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
+ mousereport(e);
+ return;
+ }
- mousesel(e, 0);
+ mousesel(e, 0);
}
-void
+ void
cresize(int width, int height)
{
- int col, row;
+ int col, row;
- if (width != 0)
- win.w = width;
- if (height != 0)
- win.h = height;
+ 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);
+ col = (win.w - 2 * borderpx) / win.cw;
+ row = (win.h - 2 * borderpx) / win.ch;
+ col = MAX(1, col);
+ row = MAX(1, row);
- tresize(col, row);
- xresize(col, row);
- ttyresize(win.tw, win.th);
+ 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
+ void
xresize(int col, int row)
{
- win.tw = col * win.cw;
- win.th = row * win.ch;
+ 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);
+ 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));
+ /* resize to new width */
+ xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec));
}
-ushort
+ ushort
sixd_to_16bit(int x)
{
- return x == 0 ? 0 : 0x3737 + 0x2828 * x;
+ return x == 0 ? 0 : 0x3737 + 0x2828 * x;
}
-int
+ 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);
+ 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
+ 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
+ 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;
+ 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;
+ *r = dc.col[x].color.red >> 8;
+ *g = dc.col[x].color.green >> 8;
+ *b = dc.col[x].color.blue >> 8;
- return 0;
+ return 0;
}
-int
+ int
xsetcolorname(int x, const char *name)
{
- Color ncolor;
+ Color ncolor;
- if (!BETWEEN(x, 0, dc.collen - 1))
- return 1;
+ if (!BETWEEN(x, 0, dc.collen - 1))
+ return 1;
- if (!xloadcolor(x, name, &ncolor))
- return 1;
+ if (!xloadcolor(x, name, &ncolor))
+ return 1;
- XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]);
- dc.col[x] = ncolor;
+ XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]);
+ dc.col[x] = ncolor;
- return 0;
+ return 0;
}
/*
* Absolute coordinates.
*/
-void
+ 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);
+ XftDrawRect(xw.draw,
+ &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg],
+ x1, y1, x2-x1, y2-y1);
}
-void
+ 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 = win.ch;
- sizeh->width_inc = win.cw;
- 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
+ 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;
+ switch (mask & (XNegative|YNegative)) {
+ case 0:
+ return NorthWestGravity;
+ case XNegative:
+ return NorthEastGravity;
+ case YNegative:
+ return SouthWestGravity;
+ }
+
+ return SouthEastGravity;
}
-int
+ 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
+ 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;
- }
- 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 (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
+ 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);
+ XftFontClose(xw.dpy, f->match);
+ FcPatternDestroy(f->pattern);
+ if (f->set)
+ FcFontSetDestroy(f->set);
}
-void
+ 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);
+ /* 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
+ 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;
+ 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
+ void
ximinstantiate(Display *dpy, XPointer client, XPointer call)
{
- if (ximopen(dpy))
- XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL,
- ximinstantiate, NULL);
+ if (ximopen(dpy))
+ XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL,
+ ximinstantiate, NULL);
}
-void
+ void
ximdestroy(XIM xim, XPointer client, XPointer call)
{
- xw.ime.xim = NULL;
- XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL,
- ximinstantiate, NULL);
- XFree(xw.ime.spotlist);
+ xw.ime.xim = NULL;
+ XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL,
+ ximinstantiate, NULL);
+ XFree(xw.ime.spotlist);
}
-int
+ int
xicdestroy(XIC xim, XPointer client, XPointer call)
{
- xw.ime.xic = NULL;
- return 1;
+ xw.ime.xic = NULL;
+ return 1;
}
-void
+ 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 * borderpx + cols * win.cw;
- win.h = 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;
-
- boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis);
-}
-
-int
+ 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;
+
+ boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis);
+}
+
+ int
xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
{
- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
- ushort mode, 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;
-
- /* 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;
- }
-
- if (mode & ATTR_BOXDRAW) {
- /* minor shoehorning: boxdraw uses only this ushort */
- glyphidx = boxdrawindex(&glyphs[i]);
- } else {
- /* 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;
-}
-
-void
+ 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;
+ 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;
+ }
+
+ if (mode & ATTR_BOXDRAW) {
+ /* minor shoehorning: boxdraw uses only this ushort */
+ glyphidx = boxdrawindex(&glyphs[i]);
+ } else {
+ /* 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 = borderpx + x * win.cw, winy = borderpx + 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, borderpx,
- winy + win.ch +
- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
- }
- if (winx + width >= borderpx + win.tw) {
- xclear(winx + width, (y == 0)? 0 : winy, win.w,
- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
- }
- if (y == 0)
- xclear(winx, 0, winx + width, borderpx);
- if (winy + win.ch >= borderpx + win.th)
- xclear(winx, winy + win.ch, winx + width, win.h);
-
- /* Clean up the region we want to draw to. */
- XftDrawRect(xw.draw, bg, winx, winy, width, win.ch);
-
- /* 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);
-
- if (base.mode & ATTR_BOXDRAW) {
- drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len);
- } else {
- /* Render the glyphs. */
- XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
- }
-
- /* Render underline and strikethrough. */
- if (base.mode & ATTR_UNDERLINE) {
- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
- width, 1);
- }
-
- if (base.mode & ATTR_STRUCK) {
- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
- width, 1);
- }
-
- /* 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);
-}
-
-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;
-
- /*
- * Select the right color for the right mode.
- */
- g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE|ATTR_BOXDRAW;
-
- 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,
- borderpx + cx * win.cw,
- borderpx + (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,
- cursorthickness, win.ch);
- break;
- }
- } else {
- XftDrawRect(xw.draw, &drawcol,
- borderpx + cx * win.cw,
- borderpx + cy * win.ch,
- win.cw - 1, 1);
- XftDrawRect(xw.draw, &drawcol,
- borderpx + cx * win.cw,
- borderpx + cy * win.ch,
- 1, win.ch - 1);
- XftDrawRect(xw.draw, &drawcol,
- borderpx + (cx + 1) * win.cw - 1,
- borderpx + cy * win.ch,
- 1, win.ch - 1);
- XftDrawRect(xw.draw, &drawcol,
- borderpx + cx * win.cw,
- borderpx + (cy + 1) * win.ch - 1,
- win.cw, 1);
- }
-}
-
-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);
- specs += i;
- numspecs -= i;
- i = 0;
- }
- if (i == 0) {
- ox = x;
- base = new;
- }
- i++;
- }
- if (i > 0)
- xdrawglyphfontspecs(specs, base, i, ox, y1);
-}
-
-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();
- 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 */
-
- 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;
+ 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);
+
+ if (base.mode & ATTR_BOXDRAW) {
+ drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len);
+ } else {
+
+ /* 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|ATTR_BOXDRAW;
+
+ 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;
-}
+ 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/files/config/suckless/st/x.c.bk b/files/config/suckless/st/x.c.bk
new file mode 100644
index 0000000..38a4bc3
--- /dev/null
+++ b/files/config/suckless/st/x.c.bk
@@ -0,0 +1,2405 @@
+/* See LICENSE for license details. */
+#include <errno.h>
+#include <math.h>
+#include <limits.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/select.h>
+#include <time.h>
+#include <unistd.h>
+#include <libgen.h>
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <X11/cursorfont.h>
+#include <X11/keysym.h>
+#include <X11/Xft/Xft.h>
+#include <X11/XKBlib.h>
+
+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 Button<N>mask for Button<N> - it's set on release */
+ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
+
+ mouse_col = evcol(e);
+ mouse_row = evrow(e);
+
+ for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
+ if (ms->release == release &&
+ ms->button == e->xbutton.button &&
+ (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;
+
+ boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis);
+}
+
+int
+xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+{
+ float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
+ ushort mode, 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;
+ }
+
+ if (mode & ATTR_BOXDRAW) {
+ /* minor shoehorning: boxdraw uses only this ushort */
+ glyphidx = boxdrawindex(&glyphs[i]);
+ } else {
+ /* 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);
+
+ if (base.mode & ATTR_BOXDRAW) {
+ drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len);
+ } else {
+ /* Render the glyphs. */
+ XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
+ }
+
+ /* Render underline and strikethrough. */
+ if (base.mode & ATTR_UNDERLINE) {
+ XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
+ width, 1);
+ }
+
+ if (base.mode & ATTR_STRUCK) {
+ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
+ width, 1);
+ }
+
+ /* 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|ATTR_BOXDRAW;
+
+ 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/files/config/suckless/st/x.c.orig b/files/config/suckless/st/x.c.orig
index d73152b..7157e56 100644
--- a/files/config/suckless/st/x.c.orig
+++ b/files/config/suckless/st/x.c.orig
@@ -1240,6 +1240,8 @@ xinit(int cols, int rows)
xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0);
if (xsel.xtarget == None)
xsel.xtarget = XA_STRING;
+
+ boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis);
}
int
@@ -1286,8 +1288,13 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
yp = winy + font->ascent;
}
- /* Lookup character index with default font. */
- glyphidx = XftCharIndex(xw.dpy, font->match, rune);
+ if (mode & ATTR_BOXDRAW) {
+ /* minor shoehorning: boxdraw uses only this ushort */
+ glyphidx = boxdrawindex(&glyphs[i]);
+ } else {
+ /* Lookup character index with default font. */
+ glyphidx = XftCharIndex(xw.dpy, font->match, rune);
+ }
if (glyphidx) {
specs[numspecs].font = font->match;
specs[numspecs].glyph = glyphidx;
@@ -1491,8 +1498,12 @@ 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);
+ if (base.mode & ATTR_BOXDRAW) {
+ drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len);
+ } else {
+ /* Render the glyphs. */
+ XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
+ }
/* Render underline and strikethrough. */
if (base.mode & ATTR_UNDERLINE) {
@@ -1535,7 +1546,7 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
/*
* Select the right color for the right mode.
*/
- g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE;
+ g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE|ATTR_BOXDRAW;
if (IS_SET(MODE_REVERSE)) {
g.mode |= ATTR_REVERSE;
diff --git a/files/config/suckless/st/x.c.rej b/files/config/suckless/st/x.c.rej
new file mode 100644
index 0000000..fe46ef1
--- /dev/null
+++ b/files/config/suckless/st/x.c.rej
@@ -0,0 +1,96 @@
+--- x.c
++++ x.c
+@@ -1322,12 +1404,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;
+@@ -1628,18 +1768,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. */
diff --git a/files/config/suckless/st/x.o b/files/config/suckless/st/x.o
deleted file mode 100644
index ccf9b0a..0000000
--- a/files/config/suckless/st/x.o
+++ /dev/null
Binary files differ