diff --git a/.gitignore b/.gitignore index 890d107..5cdd0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,20 +2,20 @@ *.o # Binarios finales -dwm/dwm -st/st -scroll/scroll -scroll/ptty -dmenu/dmenu -dmenu/stest -slstatus/slstatus -slock/slock -clipmenu/src/clipmenu -clipmenu/src/clipmenud -clipmenu/src/clipdel -clipmenu/src/clipdelmenu -clipmenu/src/clipctl -clipmenu/src/clipserve +suckless/dwm/dwm +suckless/st/st +suckless/scroll/scroll +suckless/scroll/ptty +suckless/dmenu/dmenu +suckless/dmenu/stest +suckless/slstatus/slstatus +suckless/slock/slock +suckless/clipmenu/src/clipmenu +suckless/clipmenu/src/clipmenud +suckless/clipmenu/src/clipdel +suckless/clipmenu/src/clipdelmenu +suckless/clipmenu/src/clipctl +suckless/clipmenu/src/clipserve # Restos de parches *.orig diff --git a/README.md b/README.md index 1d0b4d8..aa6146b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,58 @@ # suckless -Mi configuración de dwm, st, dmenu, slstatus, slock, scroll y clipmenu. +Mis dotfiles para Void Linux: dwm, st, dmenu, slstatus, slock, scroll y +clipmenu, más la configuración de la sesión (xinitrc, bash, picom, dunst...). Tema Everforest y fuente Fira Code en todos. -Cada programa se importó con `git subtree --squash`, así que es un único repo -pero se puede seguir actualizando desde el proyecto original. +## Estructura -## Compilar e instalar +| Carpeta | Contenido | +|---|---| +| `suckless/` | Los programas, cada uno con su `config.h` y sus parches | +| `home/` | Archivos que van en `~`, con la misma ruta (se enlazan) | +| `sistema/` | Archivos que van en `/` (acpid, zzz, elogind) | +| `install.sh` | Lo instala todo en un Void recién instalado | - cd dwm && make && sudo make install +## Instalar -(Igual para `st`, `dmenu`, `slstatus`, `slock`, `scroll` y `clipmenu`.) +En un Void recién instalado, como tu usuario (pide sudo cuando hace falta): + + git clone ssh://forgejo@ssh.alejandrogs.es/alejandrogs73/suckless.git ~/suckless + cd ~/suckless && ./install.sh + +Hace cinco pasos, que también se pueden lanzar por separado +(`./install.sh enlaces`, por ejemplo) y repetir sin problema: + +1. `paquetes`: instala con xbps todo lo que usa la configuración. +2. `suckless`: compila e instala los programas en `/usr/local`. +3. `enlaces`: enlaza cada archivo de `home/` en `~`. Si ya existía un archivo + distinto, lo guarda como `.bak`. Como son enlaces, editar `~/.bashrc` es + editar el repo. +4. `gnupg`: para después de copiar `~/.gnupg` a mano (de un USB, por + ejemplo). Arregla los permisos, pone `pinentry-gtk` (en una tty cae a + curses solo), activa el agente SSH de gpg y añade a `sshcontrol` las + subclaves de autenticación. `.bashrc` ya apunta `SSH_AUTH_SOCK` al agente. + Si `~/.gnupg` no existe todavía, no hace nada. +5. `sistema`: copia `sistema/` en `/`, activa los servicios de runit (dbus, + elogind, polkitd, NetworkManager, bluetoothd, acpid, chronyd), quita + dhcpcd y wpa_supplicant (NetworkManager ya gestiona la red) y añade el + usuario a los grupos audio, video, input, network y bluetooth. + +Después hay que cerrar sesión y volver a entrar (por los grupos) y lanzar +`startx`. + +Nix no se instala: lo poco que viene de ahí (webcord) se instala a mano. + +## Recompilar tras cambiar la configuración + + cd suckless/dwm && make && sudo make install ## Actualizar desde el proyecto original - git subtree pull --prefix=dwm https://git.suckless.org/dwm master --squash - git subtree pull --prefix=clipmenu https://github.com/cdown/clipmenu develop --squash +Cada programa se importó con `git subtree --squash`: + + git subtree pull --prefix=suckless/dwm https://git.suckless.org/dwm master --squash + git subtree pull --prefix=suckless/clipmenu https://github.com/cdown/clipmenu develop --squash Los demás funcionan igual que dwm, cambiando el nombre en `--prefix` y en la URL. @@ -57,7 +94,14 @@ muestran una notificación. - La configuración personal está en `config.h`. `config.def.h` es la versión original más los parches. Si un parche cambia `config.def.h`, hay que pasar el cambio a mano a `config.h`: `make` solo lo copia si `config.h` no existe. -- Los parches aplicados están en `/patches/`. - - dwm: fullgaps - - st: ligatures, alpha y anysize (en ese orden; anysize necesita dos - arreglos a mano, ver el historial de git) +- Los parches aplicados están en `suckless//patches/`. + - dwm: fullgaps, restartsig, preserveonrestart, statuscmd-nosignal y swallow. + - st: kitty-graphics, alpha, glyph-wide-support y ligatures, en ese orden. + kitty-graphics ya incluye anysize. glyph-wide-support y la combinación + con ligatures vienen de la rama `graphics-with-patches` de + [st-graphics](https://github.com/sergei-grechanik/st-graphics), sin + boxdraw. +- st muestra imágenes con el protocolo de gráficos de kitty; yazi lo detecta + solo y enseña las vistas previas sin ueberzugpp. +- slock está modificado a mano (no es un parche): reloj, fecha, batería y + una barra inferior con el color del estado. diff --git a/home/.bash_profile b/home/.bash_profile new file mode 100644 index 0000000..43353b4 --- /dev/null +++ b/home/.bash_profile @@ -0,0 +1,8 @@ +# .bash_profile + +# Get the aliases and functions +[ -f $HOME/.bashrc ] && . $HOME/.bashrc + + +# Added by Antigravity CLI installer +export PATH="/home/alejandro/.local/bin:$PATH" diff --git a/home/.bashrc b/home/.bashrc new file mode 100644 index 0000000..85e0abe --- /dev/null +++ b/home/.bashrc @@ -0,0 +1,99 @@ +# ===================================================================== +# BASHRC MINIMALISTA +# ===================================================================== + +# --- 1. Entorno y Rutas --- +# Priorizar binarios locales y scripts de usuario +export PATH="$HOME/.local/bin:/usr/local/bin:$PATH" +export PATH="$HOME/.config/emacs/bin:$PATH" +export PATH=$PATH:$HOME/.local/share/flatpak/exports/bin +export XDG_DATA_DIRS=$HOME/.local/share/flatpak/exports/share:$XDG_DATA_DIRS +export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) +export GPG_TTY=$(tty) +gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 + + + +# Cargar Nix (Asegura que los comandos de Nix funcionen si entras sin X11) +if [ -e /etc/profile.d/nix.sh ]; then + . /etc/profile.d/nix.sh +fi + +# Editor por defecto +export EDITOR="nvim" # Cambia a "vim" si te gusta más sufrir/disfrutar + +# --- 2. Prompt (PS1) Limpio y Rápido --- +# Define colores básicos (compatibles con tu paleta Everforest en st) +RED='\[\e[0;31m\]' +GREEN='\[\e[0;32m\]' +YELLOW='\[\e[0;33m\]' +BLUE='\[\e[0;34m\]' +NC='\[\e[0m\]' # Sin color + +# Formato: usuario@maquina:~/directorio$ (en colores) +PS1="${GREEN}\u@\h${NC}:${BLUE}\w${NC}\$ " + + +# --- 3. Aliases Base (Comodidad y Seguridad) --- +alias ls='ls --color=auto' +alias ll='ls -lh' # Lista detallada humana +alias la='ls -lha' # Lista detallada con ocultos +alias ..='cd ..' +alias ...='cd ../..' +alias c='clear' +alias q='exit' + +# Red de seguridad: te preguntará antes de sobreescribir o borrar algo +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' + + +# --- 4. Aliases para Void Linux (xbps) --- +# Te ahorrarán teclear 'xbps-lo-que-sea' 40 veces al día +alias xi='sudo xbps-install' # Instalar paquete +alias xu='sudo xbps-install -Su' # Actualizar todo el sistema +alias xq='xbps-query -Rs' # Buscar paquete en el repositorio +alias xl='xbps-query -l' # Listar paquetes instalados +alias xr='sudo xbps-remove -R' # Eliminar paquete y sus huérfanos + +alias data='sshfs alejandro@10.0.0.10:/var/storage/PUBLIC ~/DATA/' +alias esp32='sudo chmod a+rw /dev/ttyUSB0' +alias factorio='~/Games/Factorio_Linux/factorio/bin/x64/factorio' +alias cstrike=' ~/Games/HLCS/cstrike.sh' + +# --- 5. Funciones Útiles --- + +# Comando 'ex': Descomprime LITERALMENTE CUALQUIER COSA +# Uso: ex archivo.zip | ex archivo.tar.gz +ex() { + if [ -f "$1" ]; then + case $1 in + *.tar.bz2) tar xjf "$1" ;; + *.tar.gz) tar xzf "$1" ;; + *.bz2) bunzip2 "$1" ;; + *.rar) unrar x "$1" ;; + *.gz) gunzip "$1" ;; + *.tar) tar xf "$1" ;; + *.tbz2) tar xjf "$1" ;; + *.tgz) tar xzf "$1" ;; + *.zip) unzip "$1" ;; + *.Z) uncompress "$1";; + *.7z) 7z x "$1" ;; + *) echo "'$1' formato no reconocido por la función ex()" ;; + esac + else + echo "'$1' no es un archivo válido" + fi +} + +# opencode +export PATH=/home/alejandro/.opencode/bin:$PATH + + +# Added by Antigravity CLI installer +export PATH="/home/alejandro/.local/bin:$PATH" +export PATH="$PATH:$HOME/.local/share/flatpak/exports/bin" + +# Forzar IPv4 en Nix (IPv6 del sistema está roto) +export NIX_CURL_FLAGS=-4 diff --git a/home/.config/dunst/dunstrc b/home/.config/dunst/dunstrc new file mode 100644 index 0000000..cc646ed --- /dev/null +++ b/home/.config/dunst/dunstrc @@ -0,0 +1,514 @@ +# See dunst(5) for all configuration options + +[global] + ### Display ### + + # Which monitor should the notifications be displayed on. + monitor = 0 + + # Display notification on focused monitor. Possible modes are: + # mouse: follow mouse pointer + # keyboard: follow window with keyboard focus + # none: don't follow anything + # + # "keyboard" needs a window manager that exports the + # _NET_ACTIVE_WINDOW property. + # This should be the case for almost all modern window managers. + # + # If this option is set to mouse or keyboard, the monitor option + # will be ignored. + follow = none + + ### Geometry ### + + # The width of the window, excluding the frame. + # dynamic width from 0 to 300 + # width = (0, 300) + # constant width of 300 + width = 300 + + # The height of a single notification, excluding the frame. + # dynamic height from 0 to 300 + height = (0, 300) + # constant height of 300 + # height = 300 + # NOTE: Dunst from version 1.11 and older don't support dynamic height + # and the given value is treated as the maximum height + + # Position the notification in the top right corner + origin = top-right + + # Offset from the origin + # NOTE: Dunst from version 1.11 and older use this alternative notation + # offset = 10x50 + offset = (10, 50) + + # Scale factor. It is auto-detected if value is 0. + scale = 0 + + # Maximum number of notification (0 means no limit) + notification_limit = 20 + + ### Progress bar ### + + # Turn on the progress bar. It appears when a progress hint is passed with + # for example dunstify -h int:value:12 + progress_bar = true + + # Set the progress bar height. This includes the frame, so make sure + # it's at least twice as big as the frame width. + progress_bar_height = 10 + + # Set the frame width of the progress bar + progress_bar_frame_width = 1 + + # Set the minimum width for the progress bar + progress_bar_min_width = 150 + + # Set the maximum width for the progress bar + progress_bar_max_width = 300 + + # Corner radius for the progress bar. 0 disables rounded corners. + progress_bar_corner_radius = 0 + + # Define which corners to round when drawing the progress bar. If progress_bar_corner_radius + # is set to 0 this option will be ignored. + progress_bar_corners = all + + # Corner radius for the icon image. + icon_corner_radius = 0 + + # Define which corners to round when drawing the icon image. If icon_corner_radius + # is set to 0 this option will be ignored. + icon_corners = all + + # Show how many messages are currently hidden (because of + # notification_limit). + indicate_hidden = yes + + # The transparency of the window. Range: [0; 100]. + # This option will only work if a compositing window manager is + # present (e.g. xcompmgr, compiz, etc.). (X11 only) + transparency = 0 + + # Draw a line of "separator_height" pixel height between two + # notifications. + # Set to 0 to disable. + # If gap_size is greater than 0, this setting will be ignored. + separator_height = 2 + + # Padding between text and separator. + padding = 8 + + # Horizontal padding. + horizontal_padding = 8 + + # Padding between text and icon. + text_icon_padding = 0 + + # Defines width in pixels of frame around the notification window. + # Set to 0 to disable. + frame_width = 3 + + # Defines color of the frame around the notification window. + frame_color = "#aaaaaa" + + # Size of gap to display between notifications - requires a compositor. + # If value is greater than 0, separator_height will be ignored and a border + # of size frame_width will be drawn around each notification instead. + # Click events on gaps do not currently propagate to applications below. + gap_size = 0 + + # Define a color for the separator. + # possible values are: + # * auto: dunst tries to find a color fitting to the background; + # * foreground: use the same color as the foreground; + # * frame: use the same color as the frame; + # * anything else will be interpreted as a X color. + separator_color = frame + + # Sort type. + # possible values are: + # * id: sort by id + # * urgency_ascending: sort by urgency (low then normal then critical) + # * urgency_descending: sort by urgency (critical then normal then low) + # * update: sort by update (most recent always at the top) + sort = yes + + # Don't remove messages, if the user is idle (no mouse or keyboard input) + # for longer than idle_threshold seconds. + # Set to 0 to disable. + # A client can set the 'transient' hint to bypass this. See the rules + # section for how to disable this if necessary + # idle_threshold = 120 + + ### Text ### + + font = Monospace 8 + + # The spacing between lines. If the height is smaller than the + # font height, it will get raised to the font height. + line_height = 0 + + # Possible values are: + # full: Allow a small subset of html markup in notifications: + # bold + # italic + # strikethrough + # underline + # + # For a complete reference see + # . + # + # strip: This setting is provided for compatibility with some broken + # clients that send markup even though it's not enabled on the + # server. Dunst will try to strip the markup but the parsing is + # simplistic so using this option outside of matching rules for + # specific applications *IS GREATLY DISCOURAGED*. + # + # no: Disable markup parsing, incoming notifications will be treated as + # plain text. Dunst will not advertise that it has the body-markup + # capability if this is set as a global setting. + # + # It's important to note that markup inside the format option will be parsed + # regardless of what this is set to. + markup = full + + # The format of the message. Possible variables are: + # %a appname + # %s summary + # %b body + # %c category + # %S stack_tag + # %i iconname (including its path) + # %I iconname (without its path) + # %p progress value if set ([ 0%] to [100%]) or nothing + # %n progress value if set without any extra characters + # %% literal % + # Markup is allowed + format = "%s\n%b" + + # Alignment of message text. + # Possible values are "left", "center" and "right". + alignment = left + + # Vertical alignment of message text and icon. + # Possible values are "top", "center" and "bottom". + vertical_alignment = center + + # Show age of message if message is older than show_age_threshold + # seconds. + # Set to -1 to disable. + show_age_threshold = 60 + + # Specify where to make an ellipsis in long lines. + # Possible values are "start", "middle" and "end". + ellipsize = middle + + # Ignore newlines '\n' in notifications. + ignore_newline = no + + # Stack together notifications with the same content + stack_duplicates = true + + # Hide the count of stacked notifications with the same content + hide_duplicate_count = false + + # Display indicators for URLs (U) and actions (A). + show_indicators = yes + + # When set to true (recommended), you can use POSIX regular expressions for filtering rules. + # If this is set to false (not recommended), dunst will use fnmatch(3) for matching strings. + # Dunst doesn't pass any flags to fnmatch, so you cannot make use of extended patterns. + # + # Note that this will eventually be true by default. + enable_posix_regex = false + + ### Icons ### + + # Recursive icon lookup. You can set a single theme, instead of having to + # define all lookup paths. + enable_recursive_icon_lookup = true + + # Set icon theme (only used for recursive icon lookup) + icon_theme = Adwaita + # You can also set multiple icon themes, with the leftmost one being used first. + # icon_theme = "Adwaita, breeze" + + # Align icons left/right/top/off + icon_position = left + + # Scale small icons up to this size, set to 0 to disable. Helpful + # for e.g. small files or high-dpi screens. In case of conflict, + # max_icon_size takes precedence over this. + min_icon_size = 32 + + # Scale larger icons down to this size, set to 0 to disable + max_icon_size = 128 + + # Paths to default icons (only necessary when not using recursive icon lookup) + icon_path = /usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/ + + ### History ### + + # Should a notification popped up from history be sticky or timeout + # as if it would normally do. + sticky_history = yes + + # Maximum amount of notifications kept in history + history_length = 20 + + ### Misc/Advanced ### + + # dmenu path. + dmenu = /usr/bin/dmenu -p dunst: + + # Browser for opening urls in context menu. + browser = /usr/bin/xdg-open + + # Always run rule-defined scripts, even if the notification is suppressed + always_run_script = true + + # Define the title of the windows spawned by dunst (X11 only) + title = Dunst + + # Define the class of the windows spawned by dunst (X11 only) + class = Dunst + + # Define the corner radius of the notification window + # in pixel size. If the radius is 0, you have no rounded + # corners. + # The radius will be automatically lowered if it exceeds half of the + # notification height to avoid clipping text and/or icons. + corner_radius = 0 + + # Define which corners to round when drawing the window. If the corner radius + # is set to 0 this option will be ignored. + # + # Comma-separated list of the corners. The accepted corner values are bottom-right, + # bottom-left, top-right, top-left, top, bottom, left, right or all. + corners = all + + # Ignore the dbus closeNotification message. + # Useful to enforce the timeout set by dunst configuration. Without this + # parameter, an application may close the notification sent before the + # user defined timeout. + ignore_dbusclose = false + + ### Wayland ### + # These settings are Wayland-specific. They have no effect when using X11 + + # Uncomment this if you want to let notifications appear under fullscreen + # applications (default: overlay) + # layer = top + + # Set this to true to use X11 output on Wayland. + force_xwayland = false + + ### Legacy + + # Use the Xinerama extension instead of RandR for multi-monitor support. + # This setting is provided for compatibility with older nVidia drivers that + # do not support RandR and using it on systems that support RandR is highly + # discouraged. + # + # By enabling this setting dunst will not be able to detect when a monitor + # is connected or disconnected which might break follow mode if the screen + # layout changes. + force_xinerama = false + + ### mouse + + # Defines list of actions for each mouse event + # Possible values are: + # * none: Don't do anything. + # * do_action: Invoke the action determined by the action_name rule. If there is no + # such action, open the context menu. + # * open_url: If the notification has exactly one url, open it. If there are multiple + # ones, open the context menu. + # * close_current: Close current notification. + # * remove_current: Remove current notification from history. + # * close_all: Close all notifications. + # * context: Open context menu for the notification. + # * context_all: Open context menu for all notifications. + # These values can be strung together for each mouse event, and + # will be executed in sequence. + mouse_left_click = close_current + mouse_middle_click = do_action, close_current + mouse_right_click = close_all + +# Experimental features that may or may not work correctly. Do not expect them +# to have a consistent behaviour across releases. +[experimental] + # Calculate the dpi to use on a per-monitor basis. + # If this setting is enabled the Xft.dpi value will be ignored and instead + # dunst will attempt to calculate an appropriate dpi value for each monitor + # using the resolution and physical size. This might be useful in setups + # where there are multiple screens with very different dpi values. + per_monitor_dpi = false + + # Pause notification timeout when mouse hovers over the notification window. + # When enabled, notifications won't timeout while the mouse pointer is over + # them. The timeout resumes when the pointer leaves the window. + # Only works on Wayland. + pause_on_mouse_over = false + + # Use PCRE regular expressions for filtering rules. + # This setting overrides enable_posix_regex. + enable_pcre_regex = false + +[urgency_low] + # IMPORTANT: colors have to be defined in quotation marks. + # Otherwise the "#" and following would be interpreted as a comment. + background = "#222222" + foreground = "#888888" + timeout = 10 + # Icon for notifications with low urgency + default_icon = dialog-information + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + override_pause_level = 30 + # Icon for notifications with normal urgency + default_icon = dialog-information + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + frame_color = "#ff0000" + timeout = 0 + override_pause_level = 60 + # Icon for notifications with critical urgency + default_icon = dialog-warning + +# Every section that isn't one of the above is interpreted as a rules to +# override settings for certain messages. +# +# Messages can be matched by +# appname (discouraged, see desktop_entry) +# body +# category +# desktop_entry +# icon +# match_transient +# msg_urgency +# stack_tag +# summary +# +# and you can override the +# background +# foreground +# format +# frame_color +# fullscreen +# new_icon +# set_stack_tag +# set_transient +# set_category +# timeout +# urgency +# icon_position +# skip_display +# history_ignore +# action_name +# word_wrap +# ellipsize +# alignment +# hide_text +# override_pause_level +# +# Shell-like globbing will get expanded. +# +# Instead of the appname filter, it's recommended to use the desktop_entry filter. +# GLib based applications export their desktop-entry name. In comparison to the appname, +# the desktop-entry won't get localized. +# +# You can also allow a notification to appear even when paused. Notification will appear whenever notification's override_pause_level >= dunst's paused level. +# This can be used to set partial pause modes, where more urgent notifications get through, but less urgent stay paused. To do that, you can override the following in the rules: +# override_pause_level = X + +# SCRIPTING +# You can specify a script that gets run when the rule matches by +# setting the "script" option. +# The script will be called as follows: +# script appname summary body icon urgency +# where urgency can be "LOW", "NORMAL" or "CRITICAL". +# +# NOTE: It might be helpful to run dunst -print in a terminal in order +# to find fitting options for rules. + +# Disable the transient hint so that idle_threshold cannot be bypassed from the +# client +#[transient_disable] +# match_transient = yes +# set_transient = no +# +# Make the handling of transient notifications more strict by making them not +# be placed in history. +#[transient_history_ignore] +# match_transient = yes +# history_ignore = yes + +# fullscreen values +# show: show the notifications, regardless if there is a fullscreen window opened +# delay: displays the new notification, if there is no fullscreen window active +# If the notification is already drawn, it won't get undrawn. +# pushback: same as delay, but when switching into fullscreen, the notification will get +# withdrawn from screen again and will get delayed like a new notification +# suppress: withdraw the displayed notification when entering fullscreen and never show +# the new notifications that arrive during fullscreen mode +#[fullscreen_delay_everything] +# fullscreen = delay +#[fullscreen_show_critical] +# msg_urgency = critical +# fullscreen = show + +#[espeak] +# summary = "*" +# script = dunst_espeak.sh + +#[script-test] +# summary = "*script*" +# script = dunst_test.sh + +#[ignore] +# # This notification will not be displayed +# summary = "foobar" +# skip_display = true + +#[history-ignore] +# # This notification will not be saved in history +# summary = "foobar" +# history_ignore = yes + +#[skip-display] +# # This notification will not be displayed, but will be included in the history +# summary = "foobar" +# skip_display = yes + +#[signed_on] +# appname = Pidgin +# summary = "*signed on*" +# urgency = low +# +#[signed_off] +# appname = Pidgin +# summary = *signed off* +# urgency = low +# +#[says] +# appname = Pidgin +# summary = *says* +# urgency = critical +# +#[twitter] +# appname = Pidgin +# summary = *twitter.com* +# urgency = normal +# +#[stack-volumes] +# appname = "some_volume_notifiers" +# set_stack_tag = "volume" +# diff --git a/home/.config/fontconfig/fonts.conf b/home/.config/fontconfig/fonts.conf new file mode 100644 index 0000000..c7d5023 --- /dev/null +++ b/home/.config/fontconfig/fonts.conf @@ -0,0 +1,47 @@ + + + + + + Fira Code + + Fira Code + Symbols Nerd Font Mono + + + + monospace + + Fira Code + Symbols Nerd Font Mono + + + + + + + Linux Libertine G + + + + charset + 0xe0000xf8ff + + + + + + Linux Biolinum G + + + + charset + 0xe0000xf8ff + + + + diff --git a/home/.config/mimeapps.list b/home/.config/mimeapps.list new file mode 100644 index 0000000..9d96999 --- /dev/null +++ b/home/.config/mimeapps.list @@ -0,0 +1,23 @@ +[Default Applications] +x-scheme-handler/http=firefox.desktop +x-scheme-handler/https=firefox.desktop +x-scheme-handler/chrome=firefox.desktop +text/html=firefox.desktop +application/x-extension-htm=firefox.desktop +application/x-extension-html=firefox.desktop +application/x-extension-shtml=firefox.desktop +application/xhtml+xml=firefox.desktop +application/x-extension-xhtml=firefox.desktop +application/x-extension-xht=firefox.desktop + +[Added Associations] +x-scheme-handler/http=firefox.desktop; +x-scheme-handler/https=firefox.desktop; +x-scheme-handler/chrome=firefox.desktop; +text/html=firefox.desktop; +application/x-extension-htm=firefox.desktop; +application/x-extension-html=firefox.desktop; +application/x-extension-shtml=firefox.desktop; +application/xhtml+xml=firefox.desktop; +application/x-extension-xhtml=firefox.desktop; +application/x-extension-xht=firefox.desktop; diff --git a/home/.config/picom/picom.conf b/home/.config/picom/picom.conf new file mode 100644 index 0000000..3f8f58c --- /dev/null +++ b/home/.config/picom/picom.conf @@ -0,0 +1,44 @@ +# ========================================== +# Configuración Minimalista de Picom (Everforest) +# ========================================== + +# --- Backend y Rendimiento --- +backend = "glx"; +vsync = true; + +# --- Sombras (Shadows) --- +shadow = true; +shadow-radius = 12; +shadow-offset-x = -12; +shadow-offset-y = -12; +shadow-opacity = 0.4; +# No dibujar sombra en ventanas flotantes extrañas +shadow-exclude = [ + "class_g = 'dmenu'", + "_GTK_FRAME_EXTENTS@:c" +]; + +# --- Desvanecimiento (Fading) --- +fading = true; +fade-delta = 5; +fade-in-step = 0.03; +fade-out-step = 0.03; + +# --- Transparencias (Opacity) --- +inactive-opacity = 1.0; +active-opacity = 1.0; +frame-opacity = 1.0; +inactive-opacity-override = false; + +# Hacer que SOLO la terminal (st) sea un 90% transparente +opacity-rule = [ + "90:class_g = 'st-256color' && focused", + "85:class_g = 'st-256color' && !focused" +]; + +# --- Esquinas redondeadas (Opcional, si tu build de picom lo soporta) --- +corner-radius = 8; +rounded-corners-exclude = [ + "class_g = 'dwm'", + "class_g = 'dmenu'" +]; diff --git a/home/.gitconfig b/home/.gitconfig new file mode 100644 index 0000000..036843f --- /dev/null +++ b/home/.gitconfig @@ -0,0 +1,3 @@ +[user] + name = alejandrogs73 + email = mail@alejandrogs.es diff --git a/home/.local/share/wallpaper.jpg b/home/.local/share/wallpaper.jpg new file mode 100644 index 0000000..177270d Binary files /dev/null and b/home/.local/share/wallpaper.jpg differ diff --git a/home/.xinitrc b/home/.xinitrc new file mode 100644 index 0000000..6f44269 --- /dev/null +++ b/home/.xinitrc @@ -0,0 +1,31 @@ +#!/bin/sh + +# elogind ya crea XDG_RUNTIME_DIR (/run/user/UID); esto es solo por si falta. +if [ -z "$XDG_RUNTIME_DIR" ]; then + export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir + mkdir -p "$XDG_RUNTIME_DIR" + chmod 0700 "$XDG_RUNTIME_DIR" +fi + +# Abrir el bus de sesión D-Bus antes de lanzar nada, para que pipewire, +# wireplumber, dunst, blueman... y dwm compartan el mismo bus. +if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then + exec dbus-run-session -- sh "$HOME/.xinitrc" +fi + +# Teclado y monitores primero; el fondo se pinta después con la geometría final. +setxkbmap es +xrandr --output eDP --primary --auto --output HDMI-A-0 --mode 1280x720 --rate 60.00 --right-of eDP +feh --no-fehbg --bg-fill "$HOME/.local/share/wallpaper.jpg" & + +pipewire & +wireplumber & +pipewire-pulse & + +dunst & +picom & +blueman-applet & +slstatus & +clipmenud & + +exec dwm diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..ef67d14 --- /dev/null +++ b/install.sh @@ -0,0 +1,161 @@ +#!/bin/sh +# Deja un Void Linux recién instalado con este escritorio: paquetes, programas +# de suckless, enlaces de la configuración en ~ y archivos de sistema. +# +# Uso: ./install.sh [paquetes] [suckless] [enlaces] [gnupg] [sistema] +# Sin argumentos hace los cinco pasos, en ese orden. Se ejecuta como el +# usuario normal; pide sudo cuando hace falta. Se puede repetir sin problema. + +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) + +PAQUETES=" + base-devel pkg-config + libX11-devel libXft-devel libXinerama-devel libXrandr-devel libXext-devel + libXfixes-devel libxcb-devel freetype-devel fontconfig-devel harfbuzz-devel + libXrender-devel xorgproto imlib2-devel zlib-devel libxcrypt-devel + + xorg xinit setxkbmap xrandr dbus elogind polkit + picom dunst libnotify feh maim xclip + pipewire wireplumber libspa-bluetooth alsa-pipewire pavucontrol + NetworkManager bluez blueman acpid chrony + font-firacode nerd-fonts-symbols-ttf + + gnupg pinentry-gtk + firefox thunderbird neovim git fuse-sshfs unzip + yazi file ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg ImageMagick +" + +SUCKLESS="dwm st dmenu slstatus slock scroll clipmenu" +SERVICIOS="dbus elogind polkitd NetworkManager bluetoothd acpid chronyd" +# NetworkManager gestiona la red él solo; estos servicios se pelean con él. +SERVICIOS_FUERA="dhcpcd wpa_supplicant" +GRUPOS="audio video input network bluetooth" + +msg() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } + +paquetes() { + msg "Instalando paquetes" + sudo xbps-install -Syu xbps + # shellcheck disable=SC2086 + sudo xbps-install -Syu $PAQUETES +} + +suckless() { + for p in $SUCKLESS; do + msg "Compilando $p" + make -C "$DIR/suckless/$p" clean >/dev/null + make -C "$DIR/suckless/$p" + sudo make -C "$DIR/suckless/$p" install + done +} + +# Enlaza cada archivo de home/ en la misma ruta dentro de ~. Si ya hay un +# archivo que no es el enlace, se guarda como .bak en vez de pisarlo. +enlaces() { + msg "Enlazando la configuración en $HOME" + cd "$DIR/home" + find . -type f | while read -r f; do + f=${f#./} + src="$DIR/home/$f" + dst="$HOME/$f" + if [ "$(readlink "$dst" 2>/dev/null)" = "$src" ]; then + continue + fi + mkdir -p "$(dirname "$dst")" + if [ -e "$dst" ] || [ -L "$dst" ]; then + mv "$dst" "$dst.bak" + echo " $f -> $f.bak" + fi + ln -s "$src" "$dst" + echo " $f" + done + cd "$DIR" +} + +# ~/.gnupg se copia a mano (de un USB, por ejemplo). Deja los permisos como +# los quiere gpg, usa un pinentry gráfico (en una tty cae a curses solo) y +# activa el agente SSH con las claves de autenticación. +gnupg() { + g="$HOME/.gnupg" + if [ ! -d "$g" ]; then + msg "No hay ~/.gnupg: cópialo y ejecuta ./install.sh gnupg" + return 0 + fi + + msg "Arreglando permisos de $g" + sudo chown -R "$(id -u):$(id -g)" "$g" + find "$g" -type d -exec chmod 700 {} + + find "$g" -type f -exec chmod 600 {} + + + msg "Configurando gpg-agent (pinentry gráfico y SSH)" + conf="$g/gpg-agent.conf" + touch "$conf" + chmod 600 "$conf" + sed -i '/^pinentry-program/d' "$conf" + echo "pinentry-program /usr/bin/pinentry-gtk-2" >>"$conf" + grep -qx enable-ssh-support "$conf" || echo enable-ssh-support >>"$conf" + + # sshcontrol dice qué claves usa el agente para SSH: se añaden las + # subclaves de autenticación [A] que falten. + touch "$g/sshcontrol" + chmod 600 "$g/sshcontrol" + gpg --with-colons --with-keygrip -K 2>/dev/null | + awk -F: '/^(sec|ssb):/ { auth = ($12 ~ /a/) } /^grp:/ && auth { print $10 }' | + while read -r kg; do + if ! grep -q "^$kg" "$g/sshcontrol"; then + echo "$kg" >>"$g/sshcontrol" + echo " clave SSH: $kg" + fi + done + + # Reiniciar el agente para que lea la configuración nueva. + gpgconf --kill gpg-agent +} + +sistema() { + msg "Copiando archivos de sistema" + cd "$DIR/sistema" + find . -type f | while read -r f; do + f=${f#.} + sudo install -D -m "$(stat -c %a ".$f")" ".$f" "$f" + echo " $f" + done + cd "$DIR" + + msg "Activando servicios" + for s in $SERVICIOS; do + [ -d "/etc/sv/$s" ] || die "no existe el servicio $s (¿faltan paquetes?)" + if [ ! -e "/var/service/$s" ]; then + sudo ln -s "/etc/sv/$s" /var/service/ + fi + done + for s in $SERVICIOS_FUERA; do + if [ -e "/var/service/$s" ]; then + sudo rm "/var/service/$s" + fi + done + + msg "Añadiendo $USER a los grupos: $GRUPOS" + for g in $GRUPOS; do + if getent group "$g" >/dev/null; then + sudo usermod -aG "$g" "$USER" + fi + done +} + +[ -f /etc/void-release ] || command -v xbps-install >/dev/null || + die "esto es solo para Void Linux" +[ "$(id -u)" -ne 0 ] || die "ejecútalo como tu usuario, no como root" + +[ $# -gt 0 ] || set -- paquetes suckless enlaces gnupg sistema +for paso; do + case $paso in + paquetes|suckless|enlaces|gnupg|sistema) "$paso" ;; + *) die "paso desconocido: $paso" ;; + esac +done + +msg "Hecho. Cierra sesión y vuelve a entrar para aplicar los grupos; luego startx." diff --git a/st/patches/st-anysize-20220718-baa9357.diff b/st/patches/st-anysize-20220718-baa9357.diff deleted file mode 100644 index 675ffdf..0000000 --- a/st/patches/st-anysize-20220718-baa9357.diff +++ /dev/null @@ -1,164 +0,0 @@ -From 8dcdc4b21a73268e167d98aa30f24315c7f3b7ff Mon Sep 17 00:00:00 2001 -From: Bakkeby -Date: Mon, 18 Jul 2022 16:52:03 +0200 -Subject: [PATCH] Adding anysize patch - ---- - x.c | 56 ++++++++++++++++++++++++++++++-------------------------- - 1 file changed, 30 insertions(+), 26 deletions(-) - -diff --git a/x.c b/x.c -index 2a3bd38..f534347 100644 ---- a/x.c -+++ b/x.c -@@ -81,6 +81,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 */ -@@ -331,7 +332,7 @@ ttysend(const Arg *arg) - 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 +340,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; - } -@@ -739,6 +740,9 @@ cresize(int width, int height) - col = MAX(1, col); - row = MAX(1, row); - -+ win.hborderpx = (win.w - col * win.cw) / 2; -+ win.vborderpx = (win.h - row * win.ch) / 2; -+ - tresize(col, row); - xresize(col, row); - ttyresize(win.tw, win.th); -@@ -869,8 +873,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; -@@ -1152,8 +1156,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) -@@ -1242,7 +1246,7 @@ xinit(int cols, int rows) - 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; -@@ -1375,7 +1379,7 @@ 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; -@@ -1465,17 +1469,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. */ -@@ -1569,35 +1573,35 @@ 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); - } - } --- -2.37.1 - diff --git a/st/st.h b/st/st.h deleted file mode 100644 index 142fdfe..0000000 --- a/st/st.h +++ /dev/null @@ -1,127 +0,0 @@ -/* See LICENSE for license details. */ - -#include -#include - -/* macros */ -#define MIN(a, b) ((a) < (b) ? (a) : (b)) -#define MAX(a, b) ((a) < (b) ? (b) : (a)) -#define LEN(a) (sizeof(a) / sizeof(a)[0]) -#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b)) -#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d)) -#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 & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \ - (a).fg != (b).fg || \ - (a).bg != (b).bg) -#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ - (t1.tv_nsec-t2.tv_nsec)/1E6) -#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) - -#define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) -#define IS_TRUECOL(x) (1 << 24 & (x)) - -enum glyph_attribute { - ATTR_NULL = 0, - ATTR_BOLD = 1 << 0, - ATTR_FAINT = 1 << 1, - ATTR_ITALIC = 1 << 2, - ATTR_UNDERLINE = 1 << 3, - ATTR_BLINK = 1 << 4, - ATTR_REVERSE = 1 << 5, - ATTR_INVISIBLE = 1 << 6, - ATTR_STRUCK = 1 << 7, - ATTR_WRAP = 1 << 8, - ATTR_WIDE = 1 << 9, - ATTR_WDUMMY = 1 << 10, - ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, -}; - -enum selection_mode { - SEL_IDLE = 0, - SEL_EMPTY = 1, - SEL_READY = 2 -}; - -enum selection_type { - SEL_REGULAR = 1, - SEL_RECTANGULAR = 2 -}; - -enum selection_snap { - SNAP_WORD = 1, - SNAP_LINE = 2 -}; - -typedef unsigned char uchar; -typedef unsigned int uint; -typedef unsigned long ulong; -typedef unsigned short ushort; - -typedef uint_least32_t Rune; - -#define Glyph Glyph_ -typedef struct { - Rune u; /* character code */ - ushort mode; /* attribute flags */ - uint32_t fg; /* foreground */ - uint32_t bg; /* background */ -} Glyph; - -typedef Glyph *Line; - -typedef union { - int i; - uint ui; - float f; - const void *v; - const char *s; -} Arg; - -void die(const char *, ...); -void redraw(void); -void draw(void); - -void printscreen(const Arg *); -void printsel(const Arg *); -void sendbreak(const Arg *); -void toggleprinter(const Arg *); - -int tattrset(int); -void tnew(int, int); -void tresize(int, int); -void tsetdirtattr(int); -void ttyhangup(void); -int ttynew(const char *, char *, const char *, char **); -size_t ttyread(void); -void ttyresize(int, int); -void ttywrite(const char *, size_t, int); - -void resettitle(void); - -void selclear(void); -void selinit(void); -void selstart(int, int, int); -void selextend(int, int, int, int); -int selected(int, int); -char *getsel(void); - -size_t utf8encode(Rune, char *); - -void *xmalloc(size_t); -void *xrealloc(void *, size_t); -char *xstrdup(const char *); - -/* config.h globals */ -extern char *utmp; -extern char *scroll; -extern char *stty_args; -extern char *vtiden; -extern wchar_t *worddelimiters; -extern int allowaltscreen; -extern int allowwindowops; -extern char *termname; -extern unsigned int tabspaces; -extern unsigned int defaultfg; -extern unsigned int defaultbg; -extern unsigned int defaultcs; diff --git a/clipmenu/.clang-format b/suckless/clipmenu/.clang-format similarity index 100% rename from clipmenu/.clang-format rename to suckless/clipmenu/.clang-format diff --git a/clipmenu/.github/workflows/ci.yml b/suckless/clipmenu/.github/workflows/ci.yml similarity index 100% rename from clipmenu/.github/workflows/ci.yml rename to suckless/clipmenu/.github/workflows/ci.yml diff --git a/clipmenu/.gitignore b/suckless/clipmenu/.gitignore similarity index 100% rename from clipmenu/.gitignore rename to suckless/clipmenu/.gitignore diff --git a/clipmenu/LICENSE b/suckless/clipmenu/LICENSE similarity index 100% rename from clipmenu/LICENSE rename to suckless/clipmenu/LICENSE diff --git a/clipmenu/Makefile b/suckless/clipmenu/Makefile similarity index 100% rename from clipmenu/Makefile rename to suckless/clipmenu/Makefile diff --git a/clipmenu/README.md b/suckless/clipmenu/README.md similarity index 100% rename from clipmenu/README.md rename to suckless/clipmenu/README.md diff --git a/clipmenu/init/clipmenud.service.in b/suckless/clipmenu/init/clipmenud.service.in similarity index 100% rename from clipmenu/init/clipmenud.service.in rename to suckless/clipmenu/init/clipmenud.service.in diff --git a/clipmenu/man/clipctl.1 b/suckless/clipmenu/man/clipctl.1 similarity index 100% rename from clipmenu/man/clipctl.1 rename to suckless/clipmenu/man/clipctl.1 diff --git a/clipmenu/man/clipdel.1 b/suckless/clipmenu/man/clipdel.1 similarity index 100% rename from clipmenu/man/clipdel.1 rename to suckless/clipmenu/man/clipdel.1 diff --git a/clipmenu/man/clipdelmenu.1 b/suckless/clipmenu/man/clipdelmenu.1 similarity index 100% rename from clipmenu/man/clipdelmenu.1 rename to suckless/clipmenu/man/clipdelmenu.1 diff --git a/clipmenu/man/clipmenu.1 b/suckless/clipmenu/man/clipmenu.1 similarity index 100% rename from clipmenu/man/clipmenu.1 rename to suckless/clipmenu/man/clipmenu.1 diff --git a/clipmenu/man/clipmenu.conf.5 b/suckless/clipmenu/man/clipmenu.conf.5 similarity index 100% rename from clipmenu/man/clipmenu.conf.5 rename to suckless/clipmenu/man/clipmenu.conf.5 diff --git a/clipmenu/man/clipmenud.1 b/suckless/clipmenu/man/clipmenud.1 similarity index 100% rename from clipmenu/man/clipmenud.1 rename to suckless/clipmenu/man/clipmenud.1 diff --git a/clipmenu/man/clipserve.1 b/suckless/clipmenu/man/clipserve.1 similarity index 100% rename from clipmenu/man/clipserve.1 rename to suckless/clipmenu/man/clipserve.1 diff --git a/clipmenu/src/clipctl.c b/suckless/clipmenu/src/clipctl.c similarity index 100% rename from clipmenu/src/clipctl.c rename to suckless/clipmenu/src/clipctl.c diff --git a/clipmenu/src/clipdel.c b/suckless/clipmenu/src/clipdel.c similarity index 100% rename from clipmenu/src/clipdel.c rename to suckless/clipmenu/src/clipdel.c diff --git a/clipmenu/src/clipdelmenu.c b/suckless/clipmenu/src/clipdelmenu.c similarity index 100% rename from clipmenu/src/clipdelmenu.c rename to suckless/clipmenu/src/clipdelmenu.c diff --git a/clipmenu/src/clipmenu.c b/suckless/clipmenu/src/clipmenu.c similarity index 100% rename from clipmenu/src/clipmenu.c rename to suckless/clipmenu/src/clipmenu.c diff --git a/clipmenu/src/clipmenud.c b/suckless/clipmenu/src/clipmenud.c similarity index 100% rename from clipmenu/src/clipmenud.c rename to suckless/clipmenu/src/clipmenud.c diff --git a/clipmenu/src/clipserve.c b/suckless/clipmenu/src/clipserve.c similarity index 100% rename from clipmenu/src/clipserve.c rename to suckless/clipmenu/src/clipserve.c diff --git a/clipmenu/src/config.c b/suckless/clipmenu/src/config.c similarity index 100% rename from clipmenu/src/config.c rename to suckless/clipmenu/src/config.c diff --git a/clipmenu/src/config.h b/suckless/clipmenu/src/config.h similarity index 100% rename from clipmenu/src/config.h rename to suckless/clipmenu/src/config.h diff --git a/clipmenu/src/menu_util.c b/suckless/clipmenu/src/menu_util.c similarity index 100% rename from clipmenu/src/menu_util.c rename to suckless/clipmenu/src/menu_util.c diff --git a/clipmenu/src/menu_util.h b/suckless/clipmenu/src/menu_util.h similarity index 100% rename from clipmenu/src/menu_util.h rename to suckless/clipmenu/src/menu_util.h diff --git a/clipmenu/src/store.c b/suckless/clipmenu/src/store.c similarity index 100% rename from clipmenu/src/store.c rename to suckless/clipmenu/src/store.c diff --git a/clipmenu/src/store.h b/suckless/clipmenu/src/store.h similarity index 100% rename from clipmenu/src/store.h rename to suckless/clipmenu/src/store.h diff --git a/clipmenu/src/util.c b/suckless/clipmenu/src/util.c similarity index 100% rename from clipmenu/src/util.c rename to suckless/clipmenu/src/util.c diff --git a/clipmenu/src/util.h b/suckless/clipmenu/src/util.h similarity index 100% rename from clipmenu/src/util.h rename to suckless/clipmenu/src/util.h diff --git a/clipmenu/src/x.c b/suckless/clipmenu/src/x.c similarity index 100% rename from clipmenu/src/x.c rename to suckless/clipmenu/src/x.c diff --git a/clipmenu/src/x.h b/suckless/clipmenu/src/x.h similarity index 100% rename from clipmenu/src/x.h rename to suckless/clipmenu/src/x.h diff --git a/clipmenu/tests/test_store.c b/suckless/clipmenu/tests/test_store.c similarity index 100% rename from clipmenu/tests/test_store.c rename to suckless/clipmenu/tests/test_store.c diff --git a/clipmenu/tests/x_integration_tests b/suckless/clipmenu/tests/x_integration_tests similarity index 100% rename from clipmenu/tests/x_integration_tests rename to suckless/clipmenu/tests/x_integration_tests diff --git a/dmenu/LICENSE b/suckless/dmenu/LICENSE similarity index 100% rename from dmenu/LICENSE rename to suckless/dmenu/LICENSE diff --git a/dmenu/Makefile b/suckless/dmenu/Makefile similarity index 100% rename from dmenu/Makefile rename to suckless/dmenu/Makefile diff --git a/dmenu/README b/suckless/dmenu/README similarity index 100% rename from dmenu/README rename to suckless/dmenu/README diff --git a/dmenu/arg.h b/suckless/dmenu/arg.h similarity index 100% rename from dmenu/arg.h rename to suckless/dmenu/arg.h diff --git a/dmenu/config.def.h b/suckless/dmenu/config.def.h similarity index 100% rename from dmenu/config.def.h rename to suckless/dmenu/config.def.h diff --git a/dmenu/config.h b/suckless/dmenu/config.h similarity index 100% rename from dmenu/config.h rename to suckless/dmenu/config.h diff --git a/dmenu/config.mk b/suckless/dmenu/config.mk similarity index 100% rename from dmenu/config.mk rename to suckless/dmenu/config.mk diff --git a/dmenu/dmenu.1 b/suckless/dmenu/dmenu.1 similarity index 100% rename from dmenu/dmenu.1 rename to suckless/dmenu/dmenu.1 diff --git a/dmenu/dmenu.c b/suckless/dmenu/dmenu.c similarity index 100% rename from dmenu/dmenu.c rename to suckless/dmenu/dmenu.c diff --git a/dmenu/dmenu_path b/suckless/dmenu/dmenu_path similarity index 100% rename from dmenu/dmenu_path rename to suckless/dmenu/dmenu_path diff --git a/dmenu/dmenu_run b/suckless/dmenu/dmenu_run similarity index 100% rename from dmenu/dmenu_run rename to suckless/dmenu/dmenu_run diff --git a/dmenu/drw.c b/suckless/dmenu/drw.c similarity index 100% rename from dmenu/drw.c rename to suckless/dmenu/drw.c diff --git a/dmenu/drw.h b/suckless/dmenu/drw.h similarity index 100% rename from dmenu/drw.h rename to suckless/dmenu/drw.h diff --git a/dmenu/stest.1 b/suckless/dmenu/stest.1 similarity index 100% rename from dmenu/stest.1 rename to suckless/dmenu/stest.1 diff --git a/dmenu/stest.c b/suckless/dmenu/stest.c similarity index 100% rename from dmenu/stest.c rename to suckless/dmenu/stest.c diff --git a/dmenu/util.c b/suckless/dmenu/util.c similarity index 100% rename from dmenu/util.c rename to suckless/dmenu/util.c diff --git a/dmenu/util.h b/suckless/dmenu/util.h similarity index 100% rename from dmenu/util.h rename to suckless/dmenu/util.h diff --git a/dwm/LICENSE b/suckless/dwm/LICENSE similarity index 100% rename from dwm/LICENSE rename to suckless/dwm/LICENSE diff --git a/dwm/Makefile b/suckless/dwm/Makefile similarity index 100% rename from dwm/Makefile rename to suckless/dwm/Makefile diff --git a/dwm/README b/suckless/dwm/README similarity index 100% rename from dwm/README rename to suckless/dwm/README diff --git a/dwm/config.def.h b/suckless/dwm/config.def.h similarity index 100% rename from dwm/config.def.h rename to suckless/dwm/config.def.h diff --git a/dwm/config.h b/suckless/dwm/config.h similarity index 100% rename from dwm/config.h rename to suckless/dwm/config.h diff --git a/dwm/config.mk b/suckless/dwm/config.mk similarity index 100% rename from dwm/config.mk rename to suckless/dwm/config.mk diff --git a/dwm/drw.c b/suckless/dwm/drw.c similarity index 100% rename from dwm/drw.c rename to suckless/dwm/drw.c diff --git a/dwm/drw.h b/suckless/dwm/drw.h similarity index 100% rename from dwm/drw.h rename to suckless/dwm/drw.h diff --git a/dwm/dwm.1 b/suckless/dwm/dwm.1 similarity index 100% rename from dwm/dwm.1 rename to suckless/dwm/dwm.1 diff --git a/dwm/dwm.c b/suckless/dwm/dwm.c similarity index 100% rename from dwm/dwm.c rename to suckless/dwm/dwm.c diff --git a/dwm/dwm.png b/suckless/dwm/dwm.png similarity index 100% rename from dwm/dwm.png rename to suckless/dwm/dwm.png diff --git a/dwm/patches/dwm-fullgaps-6.4.diff b/suckless/dwm/patches/dwm-fullgaps-6.4.diff similarity index 100% rename from dwm/patches/dwm-fullgaps-6.4.diff rename to suckless/dwm/patches/dwm-fullgaps-6.4.diff diff --git a/dwm/patches/dwm-preserveonrestart-6.3.diff b/suckless/dwm/patches/dwm-preserveonrestart-6.3.diff similarity index 100% rename from dwm/patches/dwm-preserveonrestart-6.3.diff rename to suckless/dwm/patches/dwm-preserveonrestart-6.3.diff diff --git a/dwm/patches/dwm-restartsig-20180523-6.2.diff b/suckless/dwm/patches/dwm-restartsig-20180523-6.2.diff similarity index 100% rename from dwm/patches/dwm-restartsig-20180523-6.2.diff rename to suckless/dwm/patches/dwm-restartsig-20180523-6.2.diff diff --git a/dwm/patches/dwm-statuscmd-nosignal-20210402-67d76bd.diff b/suckless/dwm/patches/dwm-statuscmd-nosignal-20210402-67d76bd.diff similarity index 100% rename from dwm/patches/dwm-statuscmd-nosignal-20210402-67d76bd.diff rename to suckless/dwm/patches/dwm-statuscmd-nosignal-20210402-67d76bd.diff diff --git a/dwm/patches/dwm-swallow-6.3.diff b/suckless/dwm/patches/dwm-swallow-6.3.diff similarity index 100% rename from dwm/patches/dwm-swallow-6.3.diff rename to suckless/dwm/patches/dwm-swallow-6.3.diff diff --git a/dwm/transient.c b/suckless/dwm/transient.c similarity index 100% rename from dwm/transient.c rename to suckless/dwm/transient.c diff --git a/dwm/util.c b/suckless/dwm/util.c similarity index 100% rename from dwm/util.c rename to suckless/dwm/util.c diff --git a/dwm/util.h b/suckless/dwm/util.h similarity index 100% rename from dwm/util.h rename to suckless/dwm/util.h diff --git a/scroll/.gitignore b/suckless/scroll/.gitignore similarity index 100% rename from scroll/.gitignore rename to suckless/scroll/.gitignore diff --git a/scroll/LICENSE b/suckless/scroll/LICENSE similarity index 100% rename from scroll/LICENSE rename to suckless/scroll/LICENSE diff --git a/scroll/Makefile b/suckless/scroll/Makefile similarity index 100% rename from scroll/Makefile rename to suckless/scroll/Makefile diff --git a/scroll/README b/suckless/scroll/README similarity index 100% rename from scroll/README rename to suckless/scroll/README diff --git a/scroll/TODO b/suckless/scroll/TODO similarity index 100% rename from scroll/TODO rename to suckless/scroll/TODO diff --git a/scroll/config.def.h b/suckless/scroll/config.def.h similarity index 100% rename from scroll/config.def.h rename to suckless/scroll/config.def.h diff --git a/scroll/config.h b/suckless/scroll/config.h similarity index 100% rename from scroll/config.h rename to suckless/scroll/config.h diff --git a/scroll/config.mk b/suckless/scroll/config.mk similarity index 100% rename from scroll/config.mk rename to suckless/scroll/config.mk diff --git a/scroll/perf.sh b/suckless/scroll/perf.sh similarity index 100% rename from scroll/perf.sh rename to suckless/scroll/perf.sh diff --git a/scroll/ptty.c b/suckless/scroll/ptty.c similarity index 100% rename from scroll/ptty.c rename to suckless/scroll/ptty.c diff --git a/scroll/scroll.1 b/suckless/scroll/scroll.1 similarity index 100% rename from scroll/scroll.1 rename to suckless/scroll/scroll.1 diff --git a/scroll/scroll.c b/suckless/scroll/scroll.c similarity index 100% rename from scroll/scroll.c rename to suckless/scroll/scroll.c diff --git a/scroll/up.log b/suckless/scroll/up.log similarity index 100% rename from scroll/up.log rename to suckless/scroll/up.log diff --git a/scroll/up.sh b/suckless/scroll/up.sh similarity index 100% rename from scroll/up.sh rename to suckless/scroll/up.sh diff --git a/slock/LICENSE b/suckless/slock/LICENSE similarity index 100% rename from slock/LICENSE rename to suckless/slock/LICENSE diff --git a/slock/Makefile b/suckless/slock/Makefile similarity index 100% rename from slock/Makefile rename to suckless/slock/Makefile diff --git a/slock/README b/suckless/slock/README similarity index 100% rename from slock/README rename to suckless/slock/README diff --git a/slock/config.def.h b/suckless/slock/config.def.h similarity index 100% rename from slock/config.def.h rename to suckless/slock/config.def.h diff --git a/slock/config.h b/suckless/slock/config.h similarity index 100% rename from slock/config.h rename to suckless/slock/config.h diff --git a/slock/config.mk b/suckless/slock/config.mk similarity index 100% rename from slock/config.mk rename to suckless/slock/config.mk diff --git a/slock/explicit_bzero.c b/suckless/slock/explicit_bzero.c similarity index 100% rename from slock/explicit_bzero.c rename to suckless/slock/explicit_bzero.c diff --git a/slock/slock.1 b/suckless/slock/slock.1 similarity index 100% rename from slock/slock.1 rename to suckless/slock/slock.1 diff --git a/slock/slock.c b/suckless/slock/slock.c similarity index 100% rename from slock/slock.c rename to suckless/slock/slock.c diff --git a/slock/util.h b/suckless/slock/util.h similarity index 100% rename from slock/util.h rename to suckless/slock/util.h diff --git a/slstatus/LICENSE b/suckless/slstatus/LICENSE similarity index 100% rename from slstatus/LICENSE rename to suckless/slstatus/LICENSE diff --git a/slstatus/Makefile b/suckless/slstatus/Makefile similarity index 100% rename from slstatus/Makefile rename to suckless/slstatus/Makefile diff --git a/slstatus/README b/suckless/slstatus/README similarity index 100% rename from slstatus/README rename to suckless/slstatus/README diff --git a/slstatus/arg.h b/suckless/slstatus/arg.h similarity index 100% rename from slstatus/arg.h rename to suckless/slstatus/arg.h diff --git a/slstatus/components/battery.c b/suckless/slstatus/components/battery.c similarity index 100% rename from slstatus/components/battery.c rename to suckless/slstatus/components/battery.c diff --git a/slstatus/components/cat.c b/suckless/slstatus/components/cat.c similarity index 100% rename from slstatus/components/cat.c rename to suckless/slstatus/components/cat.c diff --git a/slstatus/components/cpu.c b/suckless/slstatus/components/cpu.c similarity index 100% rename from slstatus/components/cpu.c rename to suckless/slstatus/components/cpu.c diff --git a/slstatus/components/datetime.c b/suckless/slstatus/components/datetime.c similarity index 100% rename from slstatus/components/datetime.c rename to suckless/slstatus/components/datetime.c diff --git a/slstatus/components/disk.c b/suckless/slstatus/components/disk.c similarity index 100% rename from slstatus/components/disk.c rename to suckless/slstatus/components/disk.c diff --git a/slstatus/components/entropy.c b/suckless/slstatus/components/entropy.c similarity index 100% rename from slstatus/components/entropy.c rename to suckless/slstatus/components/entropy.c diff --git a/slstatus/components/hostname.c b/suckless/slstatus/components/hostname.c similarity index 100% rename from slstatus/components/hostname.c rename to suckless/slstatus/components/hostname.c diff --git a/slstatus/components/ip.c b/suckless/slstatus/components/ip.c similarity index 100% rename from slstatus/components/ip.c rename to suckless/slstatus/components/ip.c diff --git a/slstatus/components/kernel_release.c b/suckless/slstatus/components/kernel_release.c similarity index 100% rename from slstatus/components/kernel_release.c rename to suckless/slstatus/components/kernel_release.c diff --git a/slstatus/components/keyboard_indicators.c b/suckless/slstatus/components/keyboard_indicators.c similarity index 100% rename from slstatus/components/keyboard_indicators.c rename to suckless/slstatus/components/keyboard_indicators.c diff --git a/slstatus/components/keymap.c b/suckless/slstatus/components/keymap.c similarity index 100% rename from slstatus/components/keymap.c rename to suckless/slstatus/components/keymap.c diff --git a/slstatus/components/load_avg.c b/suckless/slstatus/components/load_avg.c similarity index 100% rename from slstatus/components/load_avg.c rename to suckless/slstatus/components/load_avg.c diff --git a/slstatus/components/netspeeds.c b/suckless/slstatus/components/netspeeds.c similarity index 100% rename from slstatus/components/netspeeds.c rename to suckless/slstatus/components/netspeeds.c diff --git a/slstatus/components/num_files.c b/suckless/slstatus/components/num_files.c similarity index 100% rename from slstatus/components/num_files.c rename to suckless/slstatus/components/num_files.c diff --git a/slstatus/components/ram.c b/suckless/slstatus/components/ram.c similarity index 100% rename from slstatus/components/ram.c rename to suckless/slstatus/components/ram.c diff --git a/slstatus/components/run_command.c b/suckless/slstatus/components/run_command.c similarity index 100% rename from slstatus/components/run_command.c rename to suckless/slstatus/components/run_command.c diff --git a/slstatus/components/swap.c b/suckless/slstatus/components/swap.c similarity index 100% rename from slstatus/components/swap.c rename to suckless/slstatus/components/swap.c diff --git a/slstatus/components/temperature.c b/suckless/slstatus/components/temperature.c similarity index 100% rename from slstatus/components/temperature.c rename to suckless/slstatus/components/temperature.c diff --git a/slstatus/components/uptime.c b/suckless/slstatus/components/uptime.c similarity index 100% rename from slstatus/components/uptime.c rename to suckless/slstatus/components/uptime.c diff --git a/slstatus/components/user.c b/suckless/slstatus/components/user.c similarity index 100% rename from slstatus/components/user.c rename to suckless/slstatus/components/user.c diff --git a/slstatus/components/volume.c b/suckless/slstatus/components/volume.c similarity index 100% rename from slstatus/components/volume.c rename to suckless/slstatus/components/volume.c diff --git a/slstatus/components/wifi.c b/suckless/slstatus/components/wifi.c similarity index 100% rename from slstatus/components/wifi.c rename to suckless/slstatus/components/wifi.c diff --git a/slstatus/config.def.h b/suckless/slstatus/config.def.h similarity index 100% rename from slstatus/config.def.h rename to suckless/slstatus/config.def.h diff --git a/slstatus/config.h b/suckless/slstatus/config.h similarity index 100% rename from slstatus/config.h rename to suckless/slstatus/config.h diff --git a/slstatus/config.mk b/suckless/slstatus/config.mk similarity index 100% rename from slstatus/config.mk rename to suckless/slstatus/config.mk diff --git a/slstatus/slstatus.1 b/suckless/slstatus/slstatus.1 similarity index 100% rename from slstatus/slstatus.1 rename to suckless/slstatus/slstatus.1 diff --git a/slstatus/slstatus.c b/suckless/slstatus/slstatus.c similarity index 100% rename from slstatus/slstatus.c rename to suckless/slstatus/slstatus.c diff --git a/slstatus/slstatus.h b/suckless/slstatus/slstatus.h similarity index 100% rename from slstatus/slstatus.h rename to suckless/slstatus/slstatus.h diff --git a/slstatus/util.c b/suckless/slstatus/util.c similarity index 100% rename from slstatus/util.c rename to suckless/slstatus/util.c diff --git a/slstatus/util.h b/suckless/slstatus/util.h similarity index 100% rename from slstatus/util.h rename to suckless/slstatus/util.h diff --git a/st/FAQ b/suckless/st/FAQ similarity index 100% rename from st/FAQ rename to suckless/st/FAQ diff --git a/st/LEGACY b/suckless/st/LEGACY similarity index 100% rename from st/LEGACY rename to suckless/st/LEGACY diff --git a/st/LICENSE b/suckless/st/LICENSE similarity index 100% rename from st/LICENSE rename to suckless/st/LICENSE diff --git a/st/Makefile b/suckless/st/Makefile similarity index 84% rename from st/Makefile rename to suckless/st/Makefile index dfcea0f..4c5251c 100644 --- a/st/Makefile +++ b/suckless/st/Makefile @@ -4,7 +4,7 @@ include config.mk -SRC = st.c x.c hb.c +SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c hb.c OBJ = $(SRC:.c=.o) all: st @@ -15,9 +15,10 @@ config.h: .c.o: $(CC) $(STCFLAGS) -c $< -st.o: config.h st.h win.h -x.o: arg.h config.h st.h win.h hb.h +st.o: config.h st.h win.h graphics.h +x.o: arg.h config.h st.h win.h graphics.h hb.h hb.o: st.h +graphics.c: graphics.h khash.h kvec.h st.h $(OBJ): config.h config.mk diff --git a/st/README b/suckless/st/README similarity index 100% rename from st/README rename to suckless/st/README diff --git a/st/TODO b/suckless/st/TODO similarity index 100% rename from st/TODO rename to suckless/st/TODO diff --git a/st/arg.h b/suckless/st/arg.h similarity index 100% rename from st/arg.h rename to suckless/st/arg.h diff --git a/st/config.def.h b/suckless/st/config.def.h similarity index 92% rename from st/config.def.h rename to suckless/st/config.def.h index 019a4e1..3601e41 100644 --- a/st/config.def.h +++ b/suckless/st/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; @@ -166,6 +174,28 @@ static unsigned int mousebg = 0; */ static unsigned int defaultattr = 11; +/* + * Graphics configuration + */ + +/// The template for the cache directory. +const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX"; +/// The max size of a single image file, in bytes. +unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024; +/// The max size of the cache, in bytes. +unsigned graphics_total_file_cache_size = 300 * 1024 * 1024; +/// The max ram size of an image or placement, in bytes. +unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024; +/// The max total size of all images loaded into RAM. +unsigned graphics_max_total_ram_size = 300 * 1024 * 1024; +/// The max total number of image placements and images. +unsigned graphics_max_total_placements = 4096; +/// The ratio by which limits can be exceeded. This is to reduce the frequency +/// of image removal. +double graphics_excess_tolerance_ratio = 0.05; +/// The minimum delay between redraws caused by animations, in milliseconds. +unsigned graphics_animation_min_delay = 20; + /* * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). * Note that if you want to use ShiftMask with selmasks, set this to an other @@ -173,12 +203,18 @@ static unsigned int defaultattr = 11; */ static uint forcemousemod = ShiftMask; +/* Internal keyboard shortcuts. */ +#define MODKEY Mod1Mask +#define TERMMOD (ControlMask|ShiftMask) + /* * Internal mouse shortcuts. * Beware that overloading Button1 will disable the selection. */ static MouseShortcut mshortcuts[] = { /* mask button function argument release */ + { TERMMOD, Button3, previewimage, {.s = "feh"} }, + { TERMMOD, Button2, showimageinfo, {}, 1 }, { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, @@ -186,10 +222,6 @@ static MouseShortcut mshortcuts[] = { { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, }; -/* Internal keyboard shortcuts. */ -#define MODKEY Mod1Mask -#define TERMMOD (ControlMask|ShiftMask) - static Shortcut shortcuts[] = { /* mask keysym function argument */ { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, @@ -204,6 +236,10 @@ static Shortcut shortcuts[] = { { TERMMOD, XK_Y, selpaste, {.i = 0} }, { ShiftMask, XK_Insert, selpaste, {.i = 0} }, { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, + { TERMMOD, XK_F1, togglegrdebug, {.i = 0} }, + { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, + { TERMMOD, XK_F7, unloadimages, {.i = 0} }, + { TERMMOD, XK_F8, toggleimages, {.i = 0} }, }; /* diff --git a/st/config.h b/suckless/st/config.h similarity index 92% rename from st/config.h rename to suckless/st/config.h index 2e27b4d..78d8154 100644 --- a/st/config.h +++ b/suckless/st/config.h @@ -8,6 +8,13 @@ static char *font = "Fira Code:size=11:antialias=true:autohint=true"; static int borderpx = 5; +/* 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 = "scroll"; 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; @@ -167,6 +175,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 @@ -174,12 +204,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"} }, @@ -187,10 +223,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} }, @@ -205,6 +237,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/st/config.mk b/suckless/st/config.mk similarity index 84% rename from st/config.mk rename to suckless/st/config.mk index ec69d1a..922a9c5 100644 --- a/st/config.mk +++ b/suckless/st/config.mk @@ -14,10 +14,13 @@ PKG_CONFIG = pkg-config # includes and libs INCS = -I$(X11INC) \ + `$(PKG_CONFIG) --cflags imlib2` \ `$(PKG_CONFIG) --cflags fontconfig` \ `$(PKG_CONFIG) --cflags freetype2` \ `$(PKG_CONFIG) --cflags harfbuzz` -LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \ + `$(PKG_CONFIG) --libs imlib2` \ + `$(PKG_CONFIG) --libs zlib` \ `$(PKG_CONFIG) --libs fontconfig` \ `$(PKG_CONFIG) --libs freetype2` \ `$(PKG_CONFIG) --libs harfbuzz` diff --git a/suckless/st/graphics.c b/suckless/st/graphics.c new file mode 100644 index 0000000..801f5c9 --- /dev/null +++ b/suckless/st/graphics.c @@ -0,0 +1,4393 @@ +/* The MIT License + + Copyright (c) 2021-2024 Sergei Grechanik + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// This file implements a subset of the kitty graphics protocol. +// +//////////////////////////////////////////////////////////////////////////////// + +// A workaround for mac os to enable mkdtemp. +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "khash.h" +#include "kvec.h" + +#include "st.h" +#include "graphics.h" + +extern char **environ; + +#define MAX_FILENAME_SIZE 256 +#define MAX_INFO_LEN 256 +#define MAX_IMAGE_RECTS 20 + +/// The type used in this file to represent time. Used both for time differences +/// and absolute times (as milliseconds since an arbitrary point in time, see +/// `initialization_time`). +typedef int64_t Milliseconds; + +enum ScaleMode { + SCALE_MODE_UNSET = 0, + /// Stretch or shrink the image to fill the box, ignoring aspect ratio. + SCALE_MODE_FILL = 1, + /// Preserve aspect ratio and fit to width or to height so that the + /// whole image is visible. + SCALE_MODE_CONTAIN = 2, + /// Do not scale. The image may be cropped if the box is too small. + SCALE_MODE_NONE = 3, + /// Do not scale, unless the box is too small, in which case the image + /// will be shrunk like with `SCALE_MODE_CONTAIN`. + SCALE_MODE_NONE_OR_CONTAIN = 4, +}; + +enum AnimationState { + ANIMATION_STATE_UNSET = 0, + /// The animation is stopped. Display the current frame, but don't + /// advance to the next one. + ANIMATION_STATE_STOPPED = 1, + /// Run the animation to then end, then wait for the next frame. + ANIMATION_STATE_LOADING = 2, + /// Run the animation in a loop. + ANIMATION_STATE_LOOPING = 3, +}; + +/// The status of an image. Each image uploaded to the terminal is cached on +/// disk, then it is loaded to ram when needed. +enum ImageStatus { + STATUS_UNINITIALIZED = 0, + STATUS_UPLOADING = 1, + STATUS_UPLOADING_ERROR = 2, + STATUS_UPLOADING_SUCCESS = 3, + STATUS_RAM_LOADING_ERROR = 4, + STATUS_RAM_LOADING_SUCCESS = 6, +}; + +const char *image_status_strings[6] = { + "STATUS_UNINITIALIZED", + "STATUS_UPLOADING", + "STATUS_UPLOADING_ERROR", + "STATUS_UPLOADING_SUCCESS", + "STATUS_RAM_LOADING_ERROR", + "STATUS_RAM_LOADING_SUCCESS", +}; + +enum ImageUploadingFailure { + ERROR_OVER_SIZE_LIMIT = 1, + ERROR_CANNOT_OPEN_CACHED_FILE = 2, + ERROR_UNEXPECTED_SIZE = 3, + ERROR_CANNOT_COPY_FILE = 4, + ERROR_CANNOT_OPEN_SHM = 5, + ERROR_MTIME_MISMATCH = 3, +}; + +const char *image_uploading_failure_strings[7] = { + "NO_ERROR", + "ERROR_OVER_SIZE_LIMIT", + "ERROR_CANNOT_OPEN_CACHED_FILE", + "ERROR_UNEXPECTED_SIZE", + "ERROR_CANNOT_COPY_FILE", + "ERROR_CANNOT_OPEN_SHM", + "ERROR_MTIME_MISMATCH", +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// We use the following structures to represent images and placements: +// +// - Image: this is the main structure representing an image, usually created +// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id, +// specified by 'i='). An image may have multiple frames (ImageFrame) and +// placements (ImagePlacement). +// +// - ImageFrame: represents a single frame of an image, usually created by +// the action 'a=f' (and the first frame is created with the image itself). +// Each frame has an index and also: +// - a file containing the frame data (considered to be "on disk", although +// it's probably in tmpfs), +// - an imlib object containing the fully composed frame (i.e. the frame +// data from the file composed onto the background frame or color). It is +// not ready for display yet, because it needs to be scaled and uploaded +// to the X server. +// +// - ImagePlacement: represents a placement of an image, created by 'a=p' and +// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also +// each placement has an array of pixmaps: one for each frame of the image. +// Each pixmap is a scaled and uploaded image ready to be displayed. +// +// Images are store in the `images` hash table, mapping image ids to Image +// objects (allocated on the heap). +// +// Placements are stored in the `placements` hash table of each Image object, +// mapping placement ids to ImagePlacement objects (also allocated on the heap). +// +// ImageFrames are stored in the `first_frame` field and in the +// `frames_beyond_the_first` array of each Image object. They are stored by +// value, so ImageFrame pointer may be invalidated when frames are +// added/deleted, be careful. +// +//////////////////////////////////////////////////////////////////////////////// + +struct Image; +struct ImageFrame; +struct ImagePlacement; + +KHASH_MAP_INIT_INT(id2image, struct Image *) +KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *) + +/// A transformation to apply to a pixmap before drawing it. +typedef struct PixmapTransformation { + /// The width and height of the pixmap. + int pixmap_w, pixmap_h; + /// The width and height of the transformed pixmap. + int dst_w, dst_h; + /// The offset relative to the top-left corner of the box of cells where + /// the transformed pixmap is drawn. + int dst_x, dst_y; +} PixmapTransformation; + +typedef struct ImageFrame { + /// The image this frame belongs to. + struct Image *image; + /// The 1-based index of the frame. Zero if the frame isn't initialized. + int index; + /// The last time when the frame was displayed or otherwise touched. + Milliseconds atime; + /// The background color of the frame in the 0xRRGGBBAA format. + uint32_t background_color; + /// The index of the background frame. Zero to use the color instead. + int background_frame_index; + /// The duration of the frame in milliseconds. + int gap; + /// The expected size of the frame image file (specified with 'S='), + /// used to check if uploading succeeded. + unsigned expected_size; + /// Format specification (see the `f=` key). + int format; + /// Pixel width and height of the non-composed (on-disk) frame data. May + /// differ from the image (i.e. first frame) dimensions. + int data_pix_width, data_pix_height; + /// The offset of the frame relative to the first frame. + int x, y; + /// Compression mode (see the `o=` key). + char compression; + /// The status (see `ImageStatus`). + char status; + /// Whether loading into ram is in progress. This is used to avoid + /// cyclic dependencies between frames. + char ram_loading_in_progress; + /// The reason of uploading failure (see `ImageUploadingFailure`). + char uploading_failure; + /// Whether failures and successes should be reported ('q='). + char quiet; + /// Whether to blend the frame with the background or replace it. + char blend; + /// The original file name used with file transmission. Malloced. + char *original_filename; + /// The modification time of the original file used with file + /// transmission. + time_t original_file_mtime; + /// The file corresponding to the on-disk cache, used when uploading. + FILE *open_file; + /// The size of the corresponding file cached on disk. + unsigned disk_size; + /// The imlib object containing the fully composed frame. It's not + /// scaled for screen display yet. + Imlib_Image imlib_object; +} ImageFrame; + +typedef struct Image { + /// The client id (the one specified with 'i='). Must be nonzero. + uint32_t image_id; + /// The client id specified in the query command (`a=q`). This one must + /// be used to create the response if it's non-zero. + uint32_t query_id; + /// The number specified in the transmission command (`I=`). If + /// non-zero, it may be used to identify the image instead of the + /// image_id, and it also should be mentioned in responses. + uint32_t image_number; + /// The last time when the image was displayed or otherwise touched. + Milliseconds atime; + /// The total duration of the animation in milliseconds. + int total_duration; + /// The total size of cached image files for all frames. + int total_disk_size; + /// The global index of the creation command. Used to decide which image + /// is newer if they have the same image number. + uint64_t global_command_index; + /// The 1-based index of the currently displayed frame. + int current_frame; + /// The state of the animation, see `AnimationState`. + char animation_state; + /// The absolute time that is assumed to be the start of the current + /// frame (in ms since initialization). + Milliseconds current_frame_time; + /// The absolute time of the last redraw (in ms since initialization). + /// Used to check whether it's the first time we draw the image in the + /// current redraw cycle. + Milliseconds last_redraw; + /// The absolute time of the next redraw (in ms since initialization). + /// 0 means no redraw is scheduled. + Milliseconds next_redraw; + /// The unscaled pixel width and height of the image. Usually inherited + /// from the first frame. + int pix_width, pix_height; + /// The first frame. + ImageFrame first_frame; + /// The array of frames beyond the first one. + kvec_t(ImageFrame) frames_beyond_the_first; + /// Image placements. + khash_t(id2placement) *placements; + /// The default placement. + uint32_t default_placement; + /// The initial placement id, specified with the transmission command, + /// used to report success or failure. + uint32_t initial_placement_id; +} Image; + +typedef struct ImagePlacement { + /// The image this placement belongs to. + Image *image; + /// The id of the placement. Must be nonzero. + uint32_t placement_id; + /// The last time when the placement was displayed or otherwise touched. + Milliseconds atime; + /// The 1-based index of the protected pixmap. We protect a pixmap in + /// gr_load_pixmap to avoid unloading it right after it was loaded. + int protected_frame; + /// Whether the placement is used only for Unicode placeholders. + char virtual; + /// The scaling mode (see `ScaleMode`). + char scale_mode; + /// Height and width in cells. + uint16_t rows, cols; + /// Top-left corner of the source rectangle ('x=' and 'y='). + int src_pix_x, src_pix_y; + /// Height and width of the source rectangle (zero if full image). + int src_pix_width, src_pix_height; + /// The image appropriately scaled and uploaded to the X server. This + /// pixmap is premultiplied by alpha. + Pixmap first_pixmap; + /// The array of pixmaps beyond the first one. + kvec_t(Pixmap) pixmaps_beyond_the_first; + /// The dimensions of the cell used to scale the image. If cell + /// dimensions are changed (font change), the image will be rescaled. + uint16_t scaled_cw, scaled_ch; + /// The transformation to apply to the pixmap before drawing it. + PixmapTransformation pixmap_transformation; + /// If true, do not move the cursor when displaying this placement + /// (non-virtual placements only). + char do_not_move_cursor; + /// The text underneath this placement, valid only for classic + /// placements. On deletion, the text is restored. This is a malloced + /// array of rows*cols Glyphs. + Glyph *text_underneath; +} ImagePlacement; + +/// A rectangular piece of an image to be drawn. +typedef struct { + uint32_t image_id; + uint32_t placement_id; + /// The position of the rectangle in pixels. + int screen_x_pix, screen_y_pix; + /// The starting row on the screen. + int screen_y_row; + /// The part of the whole image to be drawn, in cells. Starts are + /// zero-based, ends are exclusive. + int img_start_col, img_end_col, img_start_row, img_end_row; + /// The current cell width and height in pixels. + int cw, ch; + /// Whether colors should be inverted. + int reverse; +} ImageRect; + +/// Executes `code` for each frame of an image. Example: +/// +/// foreach_frame(image, frame, { +/// printf("Frame %d\n", frame->index); +/// }); +/// +#define foreach_frame(image, framevar, code) { size_t __i; \ + for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \ + ImageFrame *framevar = \ + __i == 0 ? &(image).first_frame \ + : &kv_A((image).frames_beyond_the_first, __i - 1); \ + code; \ + } } + +/// Executes `code` for each pixmap of a placement. Example: +/// +/// foreach_pixmap(placement, pixmap, { +/// ... +/// }); +/// +#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \ + for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \ + Pixmap pixmapvar = \ + __i == 0 ? (placement).first_pixmap \ + : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \ + code; \ + } } + + +static Image *gr_find_image(uint32_t image_id); +static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len); +static void gr_delete_image(Image *img); +static void gr_erase_placement(ImagePlacement *placement); +static void gr_check_limits(); +static void gr_try_restore_imagefile(ImageFrame *frame); +static char *gr_base64dec(const char *src, size_t *size); +static void sanitize_str(char *str, size_t max_len); +static const char *sanitized_filename(const char *str); + +/// The array of image rectangles to draw. It is reset each frame. +static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}}; +/// The known images (including the ones being uploaded). +static khash_t(id2image) *images = NULL; +/// The total number of placements in all images. +static unsigned total_placement_count = 0; +/// The total size of all image files stored in the on-disk cache. +static int64_t images_disk_size = 0; +/// The total size of all images and placements loaded into ram. +static int64_t images_ram_size = 0; +/// The id of the last loaded image. +static uint32_t last_image_id = 0; +/// Current cell width and heigh in pixels. +static int current_cw = 0, current_ch = 0; +/// The id of the currently uploaded image (when using direct uploading). +static uint32_t current_upload_image_id = 0; +/// The index of the frame currently being uploaded. +static int current_upload_frame_index = 0; +/// The time when the graphics module was initialized. +static struct timespec initialization_time = {0}; +/// The time when the current frame drawing started, used for debugging fps and +/// to calculate the current frame for animations. +static Milliseconds drawing_start_time; +/// The global index of the current command. +static uint64_t global_command_counter = 0; +/// The next redraw times for each row of the terminal. Used for animations. +/// 0 means no redraw is scheduled. +static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL}; +/// The number of files loaded in the current redraw cycle or command execution. +static int debug_loaded_files_counter = 0; +/// The number of pixmaps loaded in the current redraw cycle or command execution. +static int debug_loaded_pixmaps_counter = 0; + +/// The directory where the cache files are stored. +static char cache_dir[MAX_FILENAME_SIZE - 16]; + +/// The table used for color inversion. +static unsigned char reverse_table[256]; + +// Declared in the header. +GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE; +char graphics_display_images = 1; +GraphicsCommandResult graphics_command_result = {0}; +int graphics_next_redraw_delay = INT_MAX; + +// Defined in config.h +extern const char graphics_cache_dir_template[]; +extern unsigned graphics_max_single_image_file_size; +extern unsigned graphics_total_file_cache_size; +extern unsigned graphics_max_single_image_ram_size; +extern unsigned graphics_max_total_ram_size; +extern unsigned graphics_max_total_placements; +extern double graphics_excess_tolerance_ratio; +extern unsigned graphics_animation_min_delay; + +// Constants that are not important enough to expose in the config. + +/// The time after which an interrupted (with another command) direct +/// transmission cannot be resumed. +static Milliseconds graphics_direct_transmission_timeout_ms = 2000; + +//////////////////////////////////////////////////////////////////////////////// +// Basic helpers. +//////////////////////////////////////////////////////////////////////////////// + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) < (b) ? (b) : (a)) + +/// Returns the difference between `end` and `start` in milliseconds. +static int64_t gr_timediff_ms(const struct timespec *end, + const struct timespec *start) { + return (end->tv_sec - start->tv_sec) * 1000 + + (end->tv_nsec - start->tv_nsec) / 1000000; +} + +/// Returns the current time in milliseconds since the initialization. +static Milliseconds gr_now_ms() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return gr_timediff_ms(&now, &initialization_time); +} + +//////////////////////////////////////////////////////////////////////////////// +// Logging. +//////////////////////////////////////////////////////////////////////////////// + +#define GR_LOG(...) \ + do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0) + +//////////////////////////////////////////////////////////////////////////////// +// Basic image management functions (create, delete, find, etc). +//////////////////////////////////////////////////////////////////////////////// + +/// Returns the 1-based index of the last frame. Note that you may want to use +/// `gr_last_uploaded_frame_index` instead since the last frame may be not +/// fully uploaded yet. +static inline int gr_last_frame_index(Image *img) { + return kv_size(img->frames_beyond_the_first) + 1; +} + +/// Returns the frame with the given index. Returns NULL if the index is out of +/// bounds. The index is 1-based. +static ImageFrame *gr_get_frame(Image *img, int index) { + if (!img) + return NULL; + if (index == 1) + return &img->first_frame; + if (2 <= index && index <= gr_last_frame_index(img)) + return &kv_A(img->frames_beyond_the_first, index - 2); + return NULL; +} + +/// Returns the last frame of the image. Returns NULL if `img` is NULL. +static ImageFrame *gr_get_last_frame(Image *img) { + if (!img) + return NULL; + return gr_get_frame(img, gr_last_frame_index(img)); +} + +/// Returns the 1-based index of the last frame or the second-to-last frame if +/// the last frame is not fully uploaded yet. +static inline int gr_last_uploaded_frame_index(Image *img) { + int last_index = gr_last_frame_index(img); + if (last_index > 1 && + gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS) + return last_index - 1; + return last_index; +} + +/// Returns the pixmap for the frame with the given index. Returns 0 if the +/// index is out of bounds. The index is 1-based. +static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) { + if (index == 1) + return placement->first_pixmap; + if (2 <= index && + index <= kv_size(placement->pixmaps_beyond_the_first) + 1) + return kv_A(placement->pixmaps_beyond_the_first, index - 2); + return 0; +} + +/// Sets the pixmap for the frame with the given index. The index is 1-based. +/// The array of pixmaps is resized if needed. +static void gr_set_frame_pixmap(ImagePlacement *placement, int index, + Pixmap pixmap) { + if (index == 1) { + placement->first_pixmap = pixmap; + return; + } + // Resize the array if needed. + size_t old_size = kv_size(placement->pixmaps_beyond_the_first); + if (old_size < index - 1) { + kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2); + for (size_t i = old_size; i < index - 1; i++) + kv_A(placement->pixmaps_beyond_the_first, i) = 0; + } + kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap; +} + +/// Finds the image corresponding to the client id. Returns NULL if cannot find. +static Image *gr_find_image(uint32_t image_id) { + khiter_t k = kh_get(id2image, images, image_id); + if (k == kh_end(images)) + return NULL; + Image *res = kh_value(images, k); + return res; +} + +/// Finds the newest image corresponding to the image number. Returns NULL if +/// cannot find. +static Image *gr_find_image_by_number(uint32_t image_number) { + if (image_number == 0) + return NULL; + Image *newest_img = NULL; + Image *img = NULL; + kh_foreach_value(images, img, { + if (img->image_number == image_number && + (!newest_img || newest_img->global_command_index < + img->global_command_index)) + newest_img = img; + }); + if (!newest_img) + GR_LOG("Image number %u not found\n", image_number); + else + GR_LOG("Found image number %u, its id is %u\n", image_number, + img->image_id); + return newest_img; +} + +/// Finds the placement corresponding to the id. If the placement id is 0, +/// returns some default placement. +static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) { + if (!img) + return NULL; + if (placement_id == 0) { + // Try to get the default placement. + ImagePlacement *dflt = NULL; + if (img->default_placement != 0) + dflt = gr_find_placement(img, img->default_placement); + if (dflt) + return dflt; + // If there is no default placement, return the first one and + // set it as the default. + kh_foreach_value(img->placements, dflt, { + img->default_placement = dflt->placement_id; + return dflt; + }); + // If there are no placements, return NULL. + return NULL; + } + khiter_t k = kh_get(id2placement, img->placements, placement_id); + if (k == kh_end(img->placements)) + return NULL; + ImagePlacement *res = kh_value(img->placements, k); + return res; +} + +/// Finds the placement by image id and placement id. +static ImagePlacement *gr_find_image_and_placement(uint32_t image_id, + uint32_t placement_id) { + return gr_find_placement(gr_find_image(image_id), placement_id); +} + +/// Returns a pointer to the glyph under the classic placement with `image_id` +/// and `placement_id` at `col` and `row` (1-based). May return NULL if the +/// underneath text is unknown. +Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, + int col, int row) { + ImagePlacement *placement = + gr_find_image_and_placement(image_id, placement_id); + if (!placement || !placement->text_underneath) + return NULL; + col--; + row--; + if (col < 0 || col >= placement->cols || row < 0 || + row >= placement->rows) + return NULL; + return &placement->text_underneath[row * placement->cols + col]; +} + +/// Writes the name of the on-disk cache file to `out`. `max_len` should be the +/// size of `out`. The name will be something like +/// "/tmp/st-images-xxx/img-ID-FRAME". +static void gr_get_frame_filename(ImageFrame *frame, char *out, + size_t max_len) { + snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir, + frame->image->image_id, frame->index); +} + +/// Returns the (estimation) of the RAM size used by the frame right now. +static unsigned gr_frame_current_ram_size(ImageFrame *frame) { + if (!frame->imlib_object) + return 0; + return (unsigned)frame->image->pix_width * frame->image->pix_height * 4; +} + +/// Returns the (estimation) of the RAM size used by a single frame pixmap. +static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) { + return (unsigned)placement->pixmap_transformation.pixmap_w * + placement->pixmap_transformation.pixmap_h * 4; +} + +/// Returns the (estimation) of the RAM size used by the placemenet right now. +static unsigned gr_placement_current_ram_size(ImagePlacement *placement) { + unsigned single_frame_size = + gr_placement_single_frame_ram_size(placement); + unsigned result = 0; + foreach_pixmap(*placement, pixmap, { + if (pixmap) + result += single_frame_size; + }); + return result; +} + +/// Unload the frame from RAM (i.e. delete the corresponding imlib object). +/// If the on-disk file of the frame is preserved, it can be reloaded later. +static void gr_unload_frame(ImageFrame *frame) { + if (!frame->imlib_object) + return; + + unsigned frame_ram_size = gr_frame_current_ram_size(frame); + images_ram_size -= frame_ram_size; + + imlib_context_set_image(frame->imlib_object); + imlib_free_image_and_decache(); + frame->imlib_object = NULL; + + GR_LOG("After unloading image %u frame %u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + frame->image->image_id, frame->index, + drawing_start_time - frame->atime, images_ram_size / 1024, + frame_ram_size / 1024); +} + +/// Unload all frames of the image. +static void gr_unload_all_frames(Image *img) { + foreach_frame(*img, frame, { + gr_unload_frame(frame); + }); +} + +/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps). +/// If the on-disk files or imlib objects of the corresponding image are +/// preserved, the placement can be reloaded later. +static void gr_unload_placement(ImagePlacement *placement) { + unsigned placement_ram_size = gr_placement_current_ram_size(placement); + images_ram_size -= placement_ram_size; + + Display *disp = imlib_context_get_display(); + foreach_pixmap(*placement, pixmap, { + if (pixmap) + XFreePixmap(disp, pixmap); + }); + + placement->first_pixmap = 0; + placement->pixmaps_beyond_the_first.n = 0; + placement->scaled_ch = placement->scaled_cw = 0; + + GR_LOG("After unloading placement %u/%u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + placement->image->image_id, placement->placement_id, + drawing_start_time - placement->atime, images_ram_size / 1024, + placement_ram_size / 1024); +} + +/// Unload a single pixmap of the placement from RAM. +static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) { + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); + if (!pixmap) + return; + + Display *disp = imlib_context_get_display(); + XFreePixmap(disp, pixmap); + gr_set_frame_pixmap(placement, frameidx, 0); + images_ram_size -= gr_placement_single_frame_ram_size(placement); + + GR_LOG("After unloading pixmap %ld of " + "placement %u/%u (atime %ld ms ago) " + "frame %u (atime %ld ms ago) " + "ram: %ld KiB (- %u KiB)\n", + pixmap, placement->image->image_id, placement->placement_id, + drawing_start_time - placement->atime, frameidx, + drawing_start_time - + gr_get_frame(placement->image, frameidx)->atime, + images_ram_size / 1024, + gr_placement_single_frame_ram_size(placement) / 1024); +} + +/// Closes the on-disk cache file of the frame `frame`. +static void gr_close_disk_cache_file(ImageFrame *frame) { + if (frame && frame->open_file) { + fclose(frame->open_file); + frame->open_file = NULL; + } +} + +/// Deletes the on-disk cache file corresponding to the frame. The in-ram image +/// object (if it exists) is not deleted, placements are not unloaded either. +static void gr_delete_imagefile(ImageFrame *frame) { + // It may still be being loaded. Close the file in this case. + gr_close_disk_cache_file(frame); + + if (frame->disk_size == 0) + return; + + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + remove(filename); + + unsigned disk_size = frame->disk_size; + images_disk_size -= disk_size; + frame->image->total_disk_size -= disk_size; + frame->disk_size = 0; + + GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) " + "disk: %ld KiB (- %u KiB)\n", + frame->image->image_id, frame->index, + drawing_start_time - frame->atime, images_disk_size / 1024, + disk_size / 1024); +} + +/// Deletes all on-disk cache files of the image (for each frame). +static void gr_delete_imagefiles(Image *img) { + foreach_frame(*img, frame, { + gr_delete_imagefile(frame); + }); +} + +/// Deletes the given placement: unloads, frees the object, erases it from the +/// screen in the classic case, but doesn't change the `placements` hash table. +static void gr_delete_placement_keep_id(ImagePlacement *placement) { + if (!placement) + return; + GR_LOG("Deleting placement %u/%u\n", placement->image->image_id, + placement->placement_id); + // Erase the placement from the screen if it's classic and there is some + // saved text underneath. + if (placement->text_underneath && !placement->virtual) + gr_erase_placement(placement); + gr_unload_placement(placement); + kv_destroy(placement->pixmaps_beyond_the_first); + free(placement->text_underneath); + free(placement); + total_placement_count--; +} + +/// Deletes all placements of `img`. +static void gr_delete_all_placements(Image *img) { + ImagePlacement *placement = NULL; + kh_foreach_value(img->placements, placement, { + gr_delete_placement_keep_id(placement); + }); + kh_clear(id2placement, img->placements); +} + +/// Deletes the given image: unloads, deletes the file, frees the Image object, +/// but doesn't change the `images` hash table. +static void gr_delete_image_keep_id(Image *img) { + if (!img) + return; + GR_LOG("Deleting image %u\n", img->image_id); + foreach_frame(*img, frame, { + gr_delete_imagefile(frame); + gr_unload_frame(frame); + if (frame->original_filename) + free(frame->original_filename); + }); + kv_destroy(img->frames_beyond_the_first); + gr_delete_all_placements(img); + kh_destroy(id2placement, img->placements); + free(img); +} + +/// Deletes the given image: unloads, deletes the file, frees the Image object, +/// and also removes it from `images`. +static void gr_delete_image(Image *img) { + if (!img) + return; + uint32_t id = img->image_id; + gr_delete_image_keep_id(img); + khiter_t k = kh_get(id2image, images, id); + kh_del(id2image, images, k); +} + +/// Deletes the given placement: unloads, frees the object, erases from the +/// screen (in the classic case), and also removes it from `placements`. +static void gr_delete_placement(ImagePlacement *placement) { + if (!placement) + return; + uint32_t id = placement->placement_id; + Image *img = placement->image; + gr_delete_placement_keep_id(placement); + khiter_t k = kh_get(id2placement, img->placements, id); + kh_del(id2placement, img->placements, k); +} + +/// Deletes all images and clears `images`. +static void gr_delete_all_images() { + Image *img = NULL; + kh_foreach_value(images, img, { + gr_delete_image_keep_id(img); + }); + kh_clear(id2image, images); +} + +/// Update the atime of the image. +static void gr_touch_image(Image *img) { + img->atime = gr_now_ms(); +} + +/// Update the atime of the frame. +static void gr_touch_frame(ImageFrame *frame) { + frame->image->atime = frame->atime = gr_now_ms(); +} + +/// Update the atime of the placement. Touches the images too. +static void gr_touch_placement(ImagePlacement *placement) { + placement->image->atime = placement->atime = gr_now_ms(); +} + +/// Creates a new image with the given id. If an image with that id already +/// exists, it is deleted first. If the provided id is 0, generates a +/// random id. +static Image *gr_new_image(uint32_t id) { + if (id == 0) { + do { + id = rand(); + // Avoid IDs that don't need full 32 bits. + } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 || + gr_find_image(id)); + GR_LOG("Generated random image id %u\n", id); + } + Image *img = gr_find_image(id); + gr_delete_image_keep_id(img); + GR_LOG("Creating image %u\n", id); + img = malloc(sizeof(Image)); + memset(img, 0, sizeof(Image)); + img->placements = kh_init(id2placement); + int ret; + khiter_t k = kh_put(id2image, images, id, &ret); + kh_value(images, k) = img; + img->image_id = id; + gr_touch_image(img); + img->global_command_index = global_command_counter; + return img; +} + +/// Creates a new frame at the end of the frame array. It may be the first frame +/// if there are no frames yet. +static ImageFrame *gr_append_new_frame(Image *img) { + ImageFrame *frame = NULL; + if (img->first_frame.index == 0 && + kv_size(img->frames_beyond_the_first) == 0) { + frame = &img->first_frame; + frame->index = 1; + } else { + frame = kv_pushp(ImageFrame, img->frames_beyond_the_first); + memset(frame, 0, sizeof(ImageFrame)); + frame->index = kv_size(img->frames_beyond_the_first) + 1; + } + frame->image = img; + gr_touch_frame(frame); + GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id); + return frame; +} + +/// Creates a new placement with the given id. If a placement with that id +/// already exists, it is deleted first. If the provided id is 0, generates a +/// random id. +static ImagePlacement *gr_new_placement(Image *img, uint32_t id) { + if (id == 0) { + do { + // Currently we support only 24-bit IDs. + id = rand() & 0xFFFFFF; + // Avoid IDs that need only one byte. + } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id)); + } + ImagePlacement *placement = gr_find_placement(img, id); + gr_delete_placement_keep_id(placement); + GR_LOG("Creating placement %u/%u\n", img->image_id, id); + placement = malloc(sizeof(ImagePlacement)); + memset(placement, 0, sizeof(ImagePlacement)); + total_placement_count++; + int ret; + khiter_t k = kh_put(id2placement, img->placements, id, &ret); + kh_value(img->placements, k) = placement; + placement->image = img; + placement->placement_id = id; + gr_touch_placement(placement); + if (img->default_placement == 0) + img->default_placement = id; + return placement; +} + +static int64_t ceil_div(int64_t a, int64_t b) { + return (a + b - 1) / b; +} + +/// Computes the best number of rows and columns for a placement if it's not +/// specified, and also adjusts the source rectangle size. +static void gr_infer_placement_size_maybe(ImagePlacement *placement) { + // The size of the image. + int image_pix_width = placement->image->pix_width; + int image_pix_height = placement->image->pix_height; + // Negative values are not allowed. Quietly set them to 0. + if (placement->src_pix_x < 0) + placement->src_pix_x = 0; + if (placement->src_pix_y < 0) + placement->src_pix_y = 0; + if (placement->src_pix_width < 0) + placement->src_pix_width = 0; + if (placement->src_pix_height < 0) + placement->src_pix_height = 0; + // If the source rectangle is outside the image, truncate it. + if (placement->src_pix_x > image_pix_width) + placement->src_pix_x = image_pix_width; + if (placement->src_pix_y > image_pix_height) + placement->src_pix_y = image_pix_height; + // If the source rectangle is not specified, use the whole image. If + // it's partially outside the image, truncate it. + if (placement->src_pix_width == 0 || + placement->src_pix_x + placement->src_pix_width > image_pix_width) + placement->src_pix_width = + image_pix_width - placement->src_pix_x; + if (placement->src_pix_height == 0 || + placement->src_pix_y + placement->src_pix_height > image_pix_height) + placement->src_pix_height = + image_pix_height - placement->src_pix_y; + + if (placement->cols != 0 && placement->rows != 0) + return; + if (placement->src_pix_width == 0 || placement->src_pix_height == 0) + return; + if (current_cw == 0 || current_ch == 0) + return; + + // If no size is specified, use the image size. + if (placement->cols == 0 && placement->rows == 0) { + placement->cols = + ceil_div(placement->src_pix_width, current_cw); + placement->rows = + ceil_div(placement->src_pix_height, current_ch); + return; + } + + // Some applications specify only one of the dimensions. + if (placement->scale_mode == SCALE_MODE_CONTAIN) { + // If we preserve aspect ratio and fit to width/height, the most + // logical thing is to find the minimum size of the + // non-specified dimension that allows the image to fit the + // specified dimension. + if (placement->cols == 0) { + placement->cols = ceil_div( + placement->src_pix_width * placement->rows * + current_ch, + placement->src_pix_height * current_cw); + return; + } + if (placement->rows == 0) { + placement->rows = + ceil_div(placement->src_pix_height * + placement->cols * current_cw, + placement->src_pix_width * current_ch); + return; + } + } else { + // Otherwise we stretch the image or preserve the original size. + // In both cases we compute the best number of columns from the + // pixel size and cell size. + // TODO: In the case of stretching it's not the most logical + // thing to do, may need to revisit in the future. + // Currently we switch to SCALE_MODE_CONTAIN when only one + // of the dimensions is specified, so this case shouldn't + // happen in practice. + if (!placement->cols) + placement->cols = + ceil_div(placement->src_pix_width, current_cw); + if (!placement->rows) + placement->rows = + ceil_div(placement->src_pix_height, current_ch); + } +} + +/// Adjusts the current frame index if enough time has passed since the display +/// of the current frame. Also computes the time of the next redraw of this +/// image (`img->next_redraw`). The current time is passed as an argument so +/// that all animations are in sync. +static void gr_update_frame_index(Image *img, Milliseconds now) { + if (img->current_frame == 0) { + img->current_frame_time = now; + img->current_frame = 1; + img->next_redraw = now + MAX(1, img->first_frame.gap); + return; + } + // If the animation is stopped, show the current frame. + if (!img->animation_state || + img->animation_state == ANIMATION_STATE_STOPPED || + img->animation_state == ANIMATION_STATE_UNSET) { + // The next redraw is never (unless the state is changed). + img->next_redraw = 0; + return; + } + int last_uploaded_frame_index = gr_last_uploaded_frame_index(img); + // If we are loading and we reached the last frame, show the last frame. + if (img->animation_state == ANIMATION_STATE_LOADING && + img->current_frame == last_uploaded_frame_index) { + // The next redraw is never (unless the state is changed or + // frames are added). + img->next_redraw = 0; + return; + } + + // Check how many milliseconds passed since the current frame was shown. + int passed_ms = now - img->current_frame_time; + // If the animation is looping and too much time has passes, we can + // make a shortcut. + if (img->animation_state == ANIMATION_STATE_LOOPING && + img->total_duration > 0 && passed_ms >= img->total_duration) { + passed_ms %= img->total_duration; + img->current_frame_time = now - passed_ms; + } + // Find the next frame. + int original_frame_index = img->current_frame; + while (1) { + ImageFrame *frame = gr_get_frame(img, img->current_frame); + if (!frame) { + // The frame doesn't exist, go to the first frame. + img->current_frame = 1; + img->current_frame_time = now; + img->next_redraw = now + MAX(1, img->first_frame.gap); + return; + } + if (frame->gap >= 0 && passed_ms < frame->gap) { + // Not enough time has passed, we are still in the same + // frame, and it's not a gapless frame. + img->next_redraw = + img->current_frame_time + MAX(1, frame->gap); + return; + } + // Otherwise go to the next frame. + passed_ms -= MAX(0, frame->gap); + if (img->current_frame >= last_uploaded_frame_index) { + // It's the last frame, if the animation is loading, + // remain on it. + if (img->animation_state == ANIMATION_STATE_LOADING) { + img->next_redraw = 0; + return; + } + // Otherwise the animation is looping. + img->current_frame = 1; + // TODO: Support finite number of loops. + } else { + img->current_frame++; + } + // Make sure we don't get stuck in an infinite loop. + if (img->current_frame == original_frame_index) { + // We looped through all frames, but haven't reached the + // next frame yet. This may happen if too much time has + // passed since the last redraw or all the frames are + // gapless. Just move on to the next frame. + img->current_frame++; + if (img->current_frame > + last_uploaded_frame_index) + img->current_frame = 1; + img->current_frame_time = now; + img->next_redraw = now + MAX( + 1, gr_get_frame(img, img->current_frame)->gap); + return; + } + // Adjust the start time of the frame. The next redraw time will + // be set in the next iteration. + img->current_frame_time += MAX(0, frame->gap); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Unloading and deleting images to save resources. +//////////////////////////////////////////////////////////////////////////////// + +/// Returns whether the original file of the frame is still available (exists, +/// is a regular file, has the same size and mtime). +static int gr_is_original_file_still_available(ImageFrame *frame) { + if (!frame->original_filename) + return 0; + struct stat st; + if (stat(frame->original_filename, &st) != 0) + return 0; + if (!S_ISREG(st.st_mode)) + return 0; + if (st.st_size == 0 || st.st_size > graphics_max_single_image_file_size) + return 0; + if (frame->expected_size && st.st_size != frame->expected_size) + return 0; + if (st.st_mtime != frame->original_file_mtime) + return 0; + return 1; +} + +/// A helper to compare frames by atime for qsort. +static int gr_cmp_frames_by_atime(const void *a, const void *b) { + ImageFrame *frame_a = *(ImageFrame *const *)a; + ImageFrame *frame_b = *(ImageFrame *const *)b; + if (frame_a->atime == frame_b->atime) + return frame_a->image->global_command_index - + frame_b->image->global_command_index; + return frame_a->atime - frame_b->atime; +} + +/// A helper to compare images by atime for qsort. +static int gr_cmp_images_by_atime(const void *a, const void *b) { + Image *img_a = *(Image *const *)a; + Image *img_b = *(Image *const *)b; + if (img_a->atime == img_b->atime) + return img_a->global_command_index - + img_b->global_command_index; + return img_a->atime - img_b->atime; +} + +/// A helper to compare placements by atime for qsort. +static int gr_cmp_placements_by_atime(const void *a, const void *b) { + ImagePlacement *p_a = *(ImagePlacement **)a; + ImagePlacement *p_b = *(ImagePlacement **)b; + if (p_a->atime == p_b->atime) + return p_a->image->global_command_index - + p_b->image->global_command_index; + return p_a->atime - p_b->atime; +} + +typedef kvec_t(Image *) ImageVec; +typedef kvec_t(ImagePlacement *) ImagePlacementVec; +typedef kvec_t(ImageFrame *) ImageFrameVec; + +/// Returns an array of pointers to all images sorted by atime. +static ImageVec gr_get_images_sorted_by_atime() { + ImageVec vec; + kv_init(vec); + if (kh_size(images) == 0) + return vec; + kv_resize(Image *, vec, kh_size(images)); + Image *img = NULL; + kh_foreach_value(images, img, { kv_push(Image *, vec, img); }); + qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime); + return vec; +} + +/// Returns an array of pointers to all placements sorted by atime. +static ImagePlacementVec gr_get_placements_sorted_by_atime() { + ImagePlacementVec vec; + kv_init(vec); + if (total_placement_count == 0) + return vec; + kv_resize(ImagePlacement *, vec, total_placement_count); + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + kh_foreach_value(img->placements, placement, { + kv_push(ImagePlacement *, vec, placement); + }); + }); + qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *), + gr_cmp_placements_by_atime); + return vec; +} + +/// Returns an array of pointers to all frames sorted by atime. +static ImageFrameVec gr_get_frames_sorted_by_atime() { + ImageFrameVec frames; + kv_init(frames); + Image *img = NULL; + kh_foreach_value(images, img, { + foreach_frame(*img, frame, { + kv_push(ImageFrame *, frames, frame); + }); + }); + qsort(frames.a, kv_size(frames), sizeof(ImageFrame *), + gr_cmp_frames_by_atime); + return frames; +} + +/// An object that can be unloaded from RAM. +typedef struct { + /// Some score, probably based on access time. The lower the score, the + /// more likely that the object should be unloaded. + int64_t score; + union { + ImagePlacement *placement; + ImageFrame *frame; + }; + /// If zero, the object is the imlib object of `frame`, if non-zero, + /// the object is a pixmap of `frameidx`-th frame of `placement`. + int frameidx; +} UnloadableObject; + +typedef kvec_t(UnloadableObject) UnloadableObjectVec; + +/// A helper to compare unloadable objects by score for qsort. +static int gr_cmp_unloadable_objects(const void *a, const void *b) { + UnloadableObject *obj_a = (UnloadableObject *)a; + UnloadableObject *obj_b = (UnloadableObject *)b; + return obj_a->score - obj_b->score; +} + +/// Unloads an unloadable object from RAM. +static void gr_unload_object(UnloadableObject *obj) { + if (obj->frameidx) { + if (obj->placement->protected_frame == obj->frameidx) + return; + gr_unload_pixmap(obj->placement, obj->frameidx); + } else { + gr_unload_frame(obj->frame); + } +} + +/// Returns the recency threshold for an image. Frames that were accessed within +/// this threshold from now are considered recent and may be handled +/// differently because we may need them again very soon. +static Milliseconds gr_recency_threshold(Image *img) { + return img->total_duration * 2 + 1000; +} + +/// Creates an unloadable object for the imlib object of a frame. +static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now, + ImageFrame *frame) { + UnloadableObject obj = {0}; + obj.frameidx = 0; + obj.frame = frame; + Milliseconds atime = frame->atime; + obj.score = atime; + if (atime >= now - gr_recency_threshold(frame->image)) { + // This is a recent frame, probably from an active animation. + // Score it above `now` to prefer unloading non-active frames. + // Randomize the score because it's not very clear in which + // order we want to unload them: reloading a frame may require + // reloading other frames. + obj.score = now + 1000 + rand() % 1000; + } + return obj; +} + +/// Creates an unloadable object for a pixmap. +static UnloadableObject +gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame, + ImagePlacement *placement) { + UnloadableObject obj = {0}; + obj.frameidx = frame->index; + obj.placement = placement; + obj.score = placement->atime; + // Since we don't store pixmap atimes, use the + // oldest atime of the frame and the placement. + Milliseconds atime = MIN(placement->atime, frame->atime); + obj.score = atime; + if (atime >= now - gr_recency_threshold(frame->image)) { + // This is a recent pixmap, probably from an active animation. + // Score it above `now` to prefer unloading non-active frames. + // Also assign higher scores to frames that are closer to the + // current frame (more likely to be used soon). + int num_frames = gr_last_frame_index(frame->image); + int dist = frame->index - frame->image->current_frame; + if (dist < 0) + dist += num_frames; + obj.score = + now + 1000 + (num_frames - dist) * 1000 / num_frames; + // If the pixmap is much larger than the imlib image, prefer to + // unload the pixmap by adding up to -1000 to the score. If the + // imlib image is larger, add up to +1000. + float imlib_size = gr_frame_current_ram_size(frame); + float pixmap_size = + gr_placement_single_frame_ram_size(placement); + obj.score += + 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5); + } + return obj; +} + +/// Returns an array of unloadable objects sorted by score. +static UnloadableObjectVec +gr_get_unloadable_objects_sorted_by_score(Milliseconds now) { + UnloadableObjectVec objects; + kv_init(objects); + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + foreach_frame(*img, frame, { + if (frame->imlib_object) { + kv_push(UnloadableObject, objects, + gr_unloadable_object_for_frame(now, + frame)); + } + int frameidx = frame->index; + kh_foreach_value(img->placements, placement, { + if (!gr_get_frame_pixmap(placement, frameidx)) + continue; + kv_push(UnloadableObject, objects, + gr_unloadable_object_for_pixmap( + now, frame, placement)); + }); + }); + }); + qsort(objects.a, kv_size(objects), sizeof(UnloadableObject), + gr_cmp_unloadable_objects); + return objects; +} + +/// Returns the limit adjusted by the excess tolerance ratio. +static inline unsigned apply_tolerance(unsigned limit) { + return limit + (unsigned)(limit * graphics_excess_tolerance_ratio); +} + +/// Checks RAM and disk cache limits and deletes/unloads some images. +static void gr_check_limits() { + Milliseconds now = gr_now_ms(); + ImageVec images_sorted = {0}; + ImagePlacementVec placements_sorted = {0}; + ImageFrameVec frames_sorted = {0}; + UnloadableObjectVec objects_sorted = {0}; + int images_begin = 0; + int placements_begin = 0; + char changed = 0; + // First reduce the number of images if there are too many. + if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) { + GR_LOG("Too many images: %d\n", kh_size(images)); + changed = 1; + images_sorted = gr_get_images_sorted_by_atime(); + int to_delete = kv_size(images_sorted) - + graphics_max_total_placements; + for (; images_begin < to_delete; images_begin++) + gr_delete_image(images_sorted.a[images_begin]); + } + // Then reduce the number of placements if there are too many. + if (total_placement_count > + apply_tolerance(graphics_max_total_placements)) { + GR_LOG("Too many placements: %d\n", total_placement_count); + changed = 1; + placements_sorted = gr_get_placements_sorted_by_atime(); + int to_delete = kv_size(placements_sorted) - + graphics_max_total_placements; + for (; placements_begin < to_delete; placements_begin++) { + ImagePlacement *placement = + placements_sorted.a[placements_begin]; + if (placement->protected_frame) + break; + gr_delete_placement(placement); + } + } + // Then reduce the size of the image file cache. The files correspond to + // image frames. + if (images_disk_size > + apply_tolerance(graphics_total_file_cache_size)) { + GR_LOG("Too big disk cache: %ld KiB\n", + images_disk_size / 1024); + changed = 1; + frames_sorted = gr_get_frames_sorted_by_atime(); + for (int i = 0; i < kv_size(frames_sorted); i++) { + if (images_disk_size <= graphics_total_file_cache_size) + break; + gr_delete_imagefile(kv_A(frames_sorted, i)); + } + } + // Then unload images from RAM. + if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) { + changed = 1; + int frames_begin = 0; + GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024); + objects_sorted = gr_get_unloadable_objects_sorted_by_score(now); + for (int i = 0; i < kv_size(objects_sorted); i++) { + if (images_ram_size <= graphics_max_total_ram_size) + break; + gr_unload_object(&kv_A(objects_sorted, i)); + } + } + if (changed) { + Milliseconds end = gr_now_ms(); + GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " + "img count: %d placement count: %d Took %ld ms\n", + images_ram_size / 1024, images_disk_size / 1024, + kh_size(images), total_placement_count, end - now); + } + kv_destroy(images_sorted); + kv_destroy(placements_sorted); + kv_destroy(frames_sorted); + kv_destroy(objects_sorted); +} + +/// Unloads all images by user request. +void gr_unload_images_to_reduce_ram() { + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + kh_foreach_value(img->placements, placement, { + if (placement->protected_frame) + continue; + gr_unload_placement(placement); + }); + gr_unload_all_frames(img); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Image loading. +//////////////////////////////////////////////////////////////////////////////// + +/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2 +/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted +/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout +/// on little-endian architectures). +static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format, + size_t num_pixels) { + size_t pixel_size = format == 24 ? 3 : 4; + if (format == 32) { + for (unsigned i = 0; i < num_pixels; ++i) { + unsigned byte_i = i * pixel_size; + to[i] = ((DATA32)from[byte_i + 2]) | + ((DATA32)from[byte_i + 1]) << 8 | + ((DATA32)from[byte_i]) << 16 | + ((DATA32)from[byte_i + 3]) << 24; + } + } else { + for (unsigned i = 0; i < num_pixels; ++i) { + unsigned byte_i = i * pixel_size; + to[i] = ((DATA32)from[byte_i + 2]) | + ((DATA32)from[byte_i + 1]) << 8 | + ((DATA32)from[byte_i]) << 16 | 0xFF000000; + } + } +} + +/// Loads uncompressed RGB or RGBA image data from a file. +static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file, + int format, + size_t total_pixels) { + unsigned char chunk[BUFSIZ]; + size_t pixel_size = format == 24 ? 3 : 4; + size_t chunk_size_pix = BUFSIZ / 4; + size_t chunk_size_bytes = chunk_size_pix * pixel_size; + size_t bytes = total_pixels * pixel_size; + for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels; + chunk_start_pix += chunk_size_pix) { + size_t read_size = fread(chunk, 1, chunk_size_bytes, file); + size_t read_pixels = read_size / pixel_size; + if (chunk_start_pix + read_pixels > total_pixels) + read_pixels = total_pixels - chunk_start_pix; + gr_copy_pixels(data + chunk_start_pix, chunk, format, + read_pixels); + } +} + +#define COMPRESSED_CHUNK_SIZE BUFSIZ +#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4) + +/// Loads compressed RGB or RGBA image data from a file. +static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file, + int format, size_t total_pixels) { + size_t pixel_size = format == 24 ? 3 : 4; + unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE]; + unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE]; + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_out = decompressed_chunk; + strm.avail_out = DECOMPRESSED_CHUNK_SIZE; + strm.avail_in = 0; + strm.next_in = Z_NULL; + int ret = inflateInit(&strm); + if (ret != Z_OK) + return 1; + + int error = 0; + int progress = 0; + size_t total_copied_pixels = 0; + while (1) { + // If we don't have enough data in the input buffer, try to read + // from the file. + if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) { + // Move the existing data to the beginning. + memmove(compressed_chunk, strm.next_in, strm.avail_in); + strm.next_in = compressed_chunk; + // Read more data. + size_t bytes_read = fread( + compressed_chunk + strm.avail_in, 1, + COMPRESSED_CHUNK_SIZE - strm.avail_in, file); + strm.avail_in += bytes_read; + if (bytes_read != 0) + progress = 1; + } + + // Try to inflate the data. + int ret = inflate(&strm, Z_SYNC_FLUSH); + if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) { + error = 1; + fprintf(stderr, + "error: could not decompress the image, error " + "%s\n", + ret == Z_MEM_ERROR ? "Z_MEM_ERROR" + : "Z_DATA_ERROR"); + break; + } + + // Copy the data from the output buffer to the image. + size_t full_pixels = + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size; + // Make sure we don't overflow the image. + if (full_pixels > total_pixels - total_copied_pixels) + full_pixels = total_pixels - total_copied_pixels; + if (full_pixels > 0) { + // Copy pixels. + gr_copy_pixels(data, decompressed_chunk, format, + full_pixels); + data += full_pixels; + total_copied_pixels += full_pixels; + if (total_copied_pixels >= total_pixels) { + // We filled the whole image, there may be some + // data left, but we just truncate it. + break; + } + // Move the remaining data to the beginning. + size_t copied_bytes = full_pixels * pixel_size; + size_t leftover = + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) - + copied_bytes; + memmove(decompressed_chunk, + decompressed_chunk + copied_bytes, leftover); + strm.next_out -= copied_bytes; + strm.avail_out += copied_bytes; + progress = 1; + } + + // If we haven't made any progress, then we have reached the end + // of both the file and the inflated data. + if (!progress) + break; + progress = 0; + } + + inflateEnd(&strm); + return error; +} + +#undef COMPRESSED_CHUNK_SIZE +#undef DECOMPRESSED_CHUNK_SIZE + +/// Load the image from a file containing raw pixel data (RGB or RGBA), the data +/// may be compressed. +static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame, + const char *filename) { + size_t total_pixels = frame->data_pix_width * frame->data_pix_height; + if (total_pixels * 4 > graphics_max_single_image_ram_size) { + fprintf(stderr, + "error: image %u frame %u is too big too load: %zu > %u\n", + frame->image->image_id, frame->index, total_pixels * 4, + graphics_max_single_image_ram_size); + return NULL; + } + + FILE* file = fopen(filename, "rb"); + if (!file) { + fprintf(stderr, + "error: could not open image file: %s\n", + sanitized_filename(filename)); + return NULL; + } + + Imlib_Image image = imlib_create_image(frame->data_pix_width, + frame->data_pix_height); + if (!image) { + fprintf(stderr, + "error: could not create an image of size %d x %d\n", + frame->data_pix_width, frame->data_pix_height); + fclose(file); + return NULL; + } + + imlib_context_set_image(image); + imlib_image_set_has_alpha(1); + DATA32* data = imlib_image_get_data(); + + if (frame->compression == 0) { + gr_load_raw_pixel_data_uncompressed(data, file, frame->format, + total_pixels); + } else { + int ret = gr_load_raw_pixel_data_compressed( + data, file, frame->format, total_pixels); + if (ret != 0) { + imlib_image_put_back_data(data); + imlib_free_image(); + fclose(file); + return NULL; + } + } + + fclose(file); + imlib_image_put_back_data(data); + return image; +} + +/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object +/// is fully composed on top of the background frame. If the frame is already +/// loaded, does nothing. Loading may fail, in which case the status of the +/// frame will be set to STATUS_RAM_LOADING_ERROR. +static void gr_load_imlib_object(ImageFrame *frame) { + if (frame->imlib_object) + return; + + // If the image is uninitialized or uploading has failed, or the file + // has been deleted, we cannot load the image. + if (frame->status < STATUS_UPLOADING_SUCCESS) + return; + if (frame->disk_size == 0) { + // In some cases the original image file may still be available, + // try to restore it. + gr_try_restore_imagefile(frame); + } + if (frame->disk_size == 0) { + if (frame->status != STATUS_RAM_LOADING_ERROR && + frame->status >= STATUS_UPLOADING_SUCCESS) { + fprintf(stderr, + "error: cached image was deleted: %u frame %u\n", + frame->image->image_id, frame->index); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + // Prevent recursive dependences between frames. + if (frame->ram_loading_in_progress) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: recursive loading of image %u frame " + "%u\n", + frame->image->image_id, frame->index); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + frame->ram_loading_in_progress = 1; + + // Load the background frame if needed. Hopefully it's not recursive. + ImageFrame *bg_frame = NULL; + if (frame->background_frame_index) { + bg_frame = gr_get_frame(frame->image, + frame->background_frame_index); + if (!bg_frame) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: could not find background " + "frame %d for image %u frame %d\n", + frame->background_frame_index, + frame->image->image_id, frame->index); + frame->status = STATUS_RAM_LOADING_ERROR; + } + return; + } + gr_load_imlib_object(bg_frame); + if (!bg_frame->imlib_object) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: could not load background " + "frame %d for image %u frame %d\n", + frame->background_frame_index, + frame->image->image_id, frame->index); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + } + + frame->ram_loading_in_progress = 0; + + // We exclude background frames from the time to load the frame. + Milliseconds loading_start = gr_now_ms(); + + // Load the frame data image. + Imlib_Image frame_data_image = NULL; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + GR_LOG("Loading image: %s\n", sanitized_filename(filename)); + if (frame->format == 100) + frame_data_image = imlib_load_image(filename); + if (frame->format == 32 || frame->format == 24) + frame_data_image = gr_load_raw_pixel_data(frame, filename); + debug_loaded_files_counter++; + + if (!frame_data_image) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, "error: could not load image: %s\n", + sanitized_filename(filename)); + } + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + imlib_context_set_image(frame_data_image); + int frame_data_width = imlib_image_get_width(); + int frame_data_height = imlib_image_get_height(); + + // Check that the size of the image we are loading does not exceed the + // limit. + if (frame_data_width * frame_data_height * 4 > + graphics_max_single_image_ram_size) { + if (frame->status != STATUS_RAM_LOADING_ERROR) { + fprintf(stderr, + "error: image %u frame %u is too big too load: " + "%d x %d * 4 = %d > %u\n", + frame->image->image_id, frame->index, + frame_data_width, frame_data_height, + frame_data_width * frame_data_height * 4, + graphics_max_single_image_ram_size); + } + imlib_free_image(); + frame->status = STATUS_RAM_LOADING_ERROR; + return; + } + + GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width, + frame_data_height); + // If imlib loading succeeded, and it is the first frame, set the + // information about the original image size, unless it's already set. + if (frame->index == 1 && frame->image->pix_width == 0 && + frame->image->pix_height == 0) { + frame->image->pix_width = frame_data_width; + frame->image->pix_height = frame_data_height; + } + + int image_width = frame->image->pix_width; + int image_height = frame->image->pix_height; + + // Compose the image with the background color or frame. + if (frame->background_color != 0 || bg_frame || + image_width != frame_data_width || + image_height != frame_data_height) { + GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n", + frame->background_color, frame->background_frame_index); + Imlib_Image composed_image = imlib_create_image( + image_width, image_height); + imlib_context_set_image(composed_image); + imlib_image_set_has_alpha(1); + imlib_context_set_anti_alias(0); + + // Start with the background frame or color. + imlib_context_set_blend(0); + if (bg_frame && bg_frame->imlib_object) { + imlib_blend_image_onto_image( + bg_frame->imlib_object, 1, 0, 0, + image_width, image_height, 0, 0, + image_width, image_height); + } else { + int r = (frame->background_color >> 24) & 0xFF; + int g = (frame->background_color >> 16) & 0xFF; + int b = (frame->background_color >> 8) & 0xFF; + int a = frame->background_color & 0xFF; + imlib_context_set_color(r, g, b, a); + imlib_image_fill_rectangle(0, 0, image_width, + image_height); + } + + // Blend the frame data image onto the background. + imlib_context_set_blend(1); + imlib_blend_image_onto_image( + frame_data_image, 1, 0, 0, frame->data_pix_width, + frame->data_pix_height, frame->x, frame->y, + frame->data_pix_width, frame->data_pix_height); + + // Free the frame data image. + imlib_context_set_image(frame_data_image); + imlib_free_image(); + + frame_data_image = composed_image; + } + + frame->imlib_object = frame_data_image; + + images_ram_size += gr_frame_current_ram_size(frame); + frame->status = STATUS_RAM_LOADING_SUCCESS; + + Milliseconds loading_end = gr_now_ms(); + GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB) Took " + "%ld ms\n", + frame->image->image_id, frame->index, images_ram_size / 1024, + gr_frame_current_ram_size(frame) / 1024, + loading_end - loading_start); +} + +/// Premultiplies the alpha channel of the image data. The data is an array of +/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB. +static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) { + for (size_t i = 0; i < num_pixels; ++i) { + DATA32 pixel = data[i]; + unsigned char a = pixel >> 24; + if (a == 0) { + data[i] = 0; + } else if (a != 255) { + unsigned char b = (pixel & 0xFF) * a / 255; + unsigned char g = ((pixel >> 8) & 0xFF) * a / 255; + unsigned char r = ((pixel >> 16) & 0xFF) * a / 255; + data[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + } +} + +/// Computes the pixmap transformation, which is essentially the destination +/// rectangle for drawing an image placement in a box of size `cols*cw` x +/// `rows*ch`. The destination rectangle depends on the parameters of the +/// placement, like scaling mode. The computed transformation is stored in the +/// placement. +void gr_compute_pixmap_transformation(ImagePlacement *placement) { + // Infer the placement size if needed. + gr_infer_placement_size_maybe(placement); + + // The size of the box in which the image is placed. + int box_w = (int)placement->cols * placement->scaled_cw; + int box_h = (int)placement->rows * placement->scaled_ch; + + int src_w = placement->src_pix_width; + int src_h = placement->src_pix_height; + + // Whether the box is too small to use the true size of the image. + char box_too_small = box_w < src_w || box_h < src_h; + char mode = placement->scale_mode; + + PixmapTransformation *tr = &placement->pixmap_transformation; + + if (src_w <= 0 || src_h <= 0) { + tr->dst_x = tr->dst_y = tr->dst_w = tr->dst_h = 0; + } else if (mode == SCALE_MODE_FILL) { + tr->dst_x = tr->dst_y = 0; + tr->dst_w = box_w; + tr->dst_h = box_h; + } else if (mode == SCALE_MODE_NONE || + (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { + tr->dst_x = tr->dst_y = 0; + tr->dst_w = src_w; + tr->dst_h = src_h; + } else { + if (mode != SCALE_MODE_CONTAIN && + mode != SCALE_MODE_NONE_OR_CONTAIN) { + fprintf(stderr, + "warning: unknown scale mode %u, using " + "'contain' instead\n", + mode); + } + if (box_w * src_h > src_w * box_h) { + // If the box is wider than the original image, fit to + // height. + tr->dst_h = box_h; + tr->dst_y = 0; + tr->dst_w = src_w * box_h / src_h; + tr->dst_x = (box_w - tr->dst_w) / 2; + } else { + // Otherwise, fit to width. + tr->dst_w = box_w; + tr->dst_x = 0; + tr->dst_h = src_h * box_w / src_w; + tr->dst_y = (box_h - tr->dst_h) / 2; + } + } + + // Make sure that the size of the destination image is non-zero. + tr->dst_w = MAX(1, tr->dst_w); + tr->dst_h = MAX(1, tr->dst_h); + + // Normally we want the pixmap to be exactly the size of the destination + // rectangle. + tr->pixmap_w = tr->dst_w; + tr->pixmap_h = tr->dst_h; + // However, if the pixmap would be larger than the source image, use the + // source image size. The upscaling will be done by XRender then. + if (tr->pixmap_w * tr->pixmap_h > src_w * src_h) { + tr->pixmap_w = MAX(1, src_w); + tr->pixmap_h = MAX(1, src_h); + } + + // If the pixmap would be over the limit, scale it down. + if (tr->pixmap_w * tr->pixmap_h * 4 > + graphics_max_single_image_ram_size) { + double scale = sqrt((double)graphics_max_single_image_ram_size / + (tr->pixmap_w * tr->pixmap_h * 4)); + tr->pixmap_w = MAX(1, (int)(tr->pixmap_w * scale)); + tr->pixmap_h = MAX(1, (int)(tr->pixmap_h * scale)); + } +} + +/// Creates and returns a scaled imlib image for the image placement. The +/// original imlib object must be loaded and the placement size must be inferred +/// by the caller (by calling `gr_compute_pixmap_transformation`). The caller is +/// responsible for freeing the returned image. +Imlib_Image gr_create_scaled_image_object(ImagePlacement *placement, + ImageFrame *frame) { + // The source rectangle (inside the original image). + int src_x = placement->src_pix_x; + int src_y = placement->src_pix_y; + int src_w = placement->src_pix_width; + int src_h = placement->src_pix_height; + // The pixmap dimensions. + int pixmap_w = placement->pixmap_transformation.pixmap_w; + int pixmap_h = placement->pixmap_transformation.pixmap_h; + + if (pixmap_w * pixmap_h * 4 > graphics_max_single_image_ram_size) { + fprintf(stderr, + "error: placement %u/%u would be too big to load: %d x " + "%d x 4 > %u\n", + placement->image->image_id, placement->placement_id, + pixmap_w, pixmap_h, graphics_max_single_image_ram_size); + return 0; + } + + if (pixmap_w == 0 || pixmap_h == 0) + fprintf(stderr, "warning: image of zero size\n"); + + imlib_context_set_image(frame->imlib_object); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + + return imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, + pixmap_w, pixmap_h); +} + +/// Creates a pixmap for the frame of an image placement. The pixmap contains +/// the image data correctly scaled and fit to the box defined by the number of +/// rows/columns of the image placement and the provided cell dimensions in +/// pixels. If the placement is already loaded, it will be reloaded only if the +/// cell dimensions have changed. +Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) { + Milliseconds loading_start = gr_now_ms(); + Image *img = placement->image; + ImageFrame *frame = gr_get_frame(img, frameidx); + + // Update the atime unconditionally. + gr_touch_placement(placement); + if (frame) + gr_touch_frame(frame); + + // If cw or ch are different, unload all the pixmaps and recompute the + // pixmap geometry and transformation (shared for all pixmaps). + if (placement->scaled_cw != cw || placement->scaled_ch != ch) { + gr_unload_placement(placement); + placement->scaled_cw = cw; + placement->scaled_ch = ch; + gr_compute_pixmap_transformation(placement); + } + + // If it's already loaded, do nothing. + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); + if (pixmap) + return pixmap; + + GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id, + placement->placement_id, frameidx); + + if (placement->pixmap_transformation.pixmap_w == 0 || + placement->pixmap_transformation.pixmap_h == 0) { + GR_LOG("Not loading because the pixmap size is zero\n"); + return 0; + } + + // Load the imlib object for the frame. + if (!frame) { + fprintf(stderr, + "error: could not find frame %u for image %u\n", + frameidx, img->image_id); + return 0; + } + gr_load_imlib_object(frame); + if (!frame->imlib_object) + return 0; + + // Create the scaled image. This is temporary, we will upload to the X + // server, and then delete immediately. + Imlib_Image scaled_image = + gr_create_scaled_image_object(placement, frame); + if (!scaled_image) + return 0; + imlib_context_set_image(scaled_image); + int pixmap_w = imlib_image_get_width(); + int pixmap_h = imlib_image_get_height(); + + // XRender needs the alpha channel premultiplied. + DATA32 *data = imlib_image_get_data(); + gr_premultiply_alpha(data, pixmap_w * pixmap_h); + + // Upload the image to the X server. + Display *disp = imlib_context_get_display(); + Visual *vis = imlib_context_get_visual(); + Colormap cmap = imlib_context_get_colormap(); + Drawable drawable = imlib_context_get_drawable(); + if (!drawable) + drawable = DefaultRootWindow(disp); + pixmap = XCreatePixmap(disp, drawable, pixmap_w, pixmap_h, 32); + XVisualInfo visinfo = {0}; + Status visual_found = XMatchVisualInfo(disp, DefaultScreen(disp), 32, + TrueColor, &visinfo) || + XMatchVisualInfo(disp, DefaultScreen(disp), 24, + TrueColor, &visinfo); + if (!visual_found) { + fprintf(stderr, + "error: could not find 32-bit TrueColor visual\n"); + // Proceed anyway. + visinfo.visual = NULL; + } + XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, + (char *)data, pixmap_w, pixmap_h, 32, 0); + if (!ximage) { + fprintf(stderr, "error: could not create XImage\n"); + imlib_image_put_back_data(data); + imlib_free_image(); + return 0; + } + GC gc = XCreateGC(disp, pixmap, 0, NULL); + XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, pixmap_w, pixmap_h); + XFreeGC(disp, gc); + // XDestroyImage will free the data as well, but it is managed by imlib, + // so set it to NULL. + ximage->data = NULL; + XDestroyImage(ximage); + imlib_image_put_back_data(data); + imlib_free_image(); + + // Assign the pixmap to the frame and increase the ram size. + gr_set_frame_pixmap(placement, frameidx, pixmap); + images_ram_size += gr_placement_single_frame_ram_size(placement); + debug_loaded_pixmaps_counter++; + + Milliseconds loading_end = gr_now_ms(); + GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " + "KiB) Took %ld ms\n", + frame->image->image_id, placement->placement_id, frame->index, + images_ram_size / 1024, + gr_placement_single_frame_ram_size(placement) / 1024, + loading_end - loading_start); + + // Free up ram if needed, but keep the pixmap we've loaded no matter + // what. + placement->protected_frame = frameidx; + gr_check_limits(); + placement->protected_frame = 0; + + return pixmap; +} + +//////////////////////////////////////////////////////////////////////////////// +// Initialization and deinitialization. +//////////////////////////////////////////////////////////////////////////////// + +/// Creates a temporary directory. +static int gr_create_cache_dir() { + strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); + if (!mkdtemp(cache_dir)) { + fprintf(stderr, + "error: could not create temporary dir from template " + "%s\n", + sanitized_filename(cache_dir)); + return 0; + } + fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); + return 1; +} + +/// Checks whether `tmp_dir` exists and recreates it if it doesn't. +static void gr_make_sure_tmpdir_exists() { + struct stat st; + if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) + return; + fprintf(stderr, + "error: %s is not a directory, will need to create a new " + "graphics cache directory\n", + sanitized_filename(cache_dir)); + gr_create_cache_dir(); +} + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm) { + // Set the initialization time. + clock_gettime(CLOCK_MONOTONIC, &initialization_time); + + // Create the temporary dir. + if (!gr_create_cache_dir()) + abort(); + + // Initialize imlib. + imlib_context_set_display(disp); + imlib_context_set_visual(vis); + imlib_context_set_colormap(cm); + imlib_context_set_anti_alias(1); + imlib_context_set_blend(1); + // Imlib2 checks only the file name when caching, which is not enough + // for us since we reuse file names. Disable caching. + imlib_set_cache_size(0); + + // Prepare for color inversion. + for (size_t i = 0; i < 256; ++i) + reverse_table[i] = 255 - i; + + // Create data structures. + images = kh_init(id2image); + kv_init(next_redraw_times); + + atexit(gr_deinit); +} + +/// Deinitialize the graphics module. +void gr_deinit() { + // Remove the cache dir. + remove(cache_dir); + kv_destroy(next_redraw_times); + if (images) { + // Delete all images. + gr_delete_all_images(); + // Destroy the data structures. + kh_destroy(id2image, images); + images = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Dumping, debugging, and image preview. +//////////////////////////////////////////////////////////////////////////////// + +/// Returns a string containing a time difference in a human-readable format. +/// Uses a static buffer, so be careful. +static const char *gr_ago(Milliseconds diff) { + static char result[32]; + double seconds = (double)diff / 1000.0; + if (seconds < 1) + snprintf(result, sizeof(result), "%.2f sec ago", seconds); + else if (seconds < 60) + snprintf(result, sizeof(result), "%d sec ago", (int)seconds); + else if (seconds < 3600) + snprintf(result, sizeof(result), "%d min %d sec ago", + (int)(seconds / 60), (int)(seconds) % 60); + else { + snprintf(result, sizeof(result), "%d hr %d min %d sec ago", + (int)(seconds / 3600), (int)(seconds) % 3600 / 60, + (int)(seconds) % 60); + } + return result; +} + +/// Prints to `file` with an indentation of `ind` spaces. +static void fprintf_ind(FILE *file, int ind, const char *format, ...) { + fprintf(file, "%*s", ind, ""); + va_list args; + va_start(args, format); + vfprintf(file, format, args); + va_end(args); +} + +/// Dumps the image info to `file` with an indentation of `ind` spaces. +static void gr_dump_image_info(FILE *file, Image *img, int ind) { + if (!img) { + fprintf_ind(file, ind, "Image is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Image %u\n", img->image_id); + ind += 4; + fprintf_ind(file, ind, "number: %u\n", img->image_number); + fprintf_ind(file, ind, "global command index: %lu\n", + img->global_command_index); + fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, + gr_ago(now - img->atime)); + fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, + img->pix_height); + fprintf_ind(file, ind, "cur frame start time: %ld %s\n", + img->current_frame_time, + gr_ago(now - img->current_frame_time)); + if (img->next_redraw) + fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", + img->next_redraw, img->next_redraw - now); + fprintf_ind(file, ind, "total disk size: %u KiB\n", + img->total_disk_size / 1024); + fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); + fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); + fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); + fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); + fprintf_ind(file, ind, "default_placement: %u\n", + img->default_placement); +} + +/// Dumps the frame info to `file` with an indentation of `ind` spaces. +static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { + if (!frame) { + fprintf_ind(file, ind, "Frame is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Frame %d\n", frame->index); + ind += 4; + if (frame->index == 0) { + fprintf_ind(file, ind, "NOT INITIALIZED\n"); + return; + } + if (frame->original_filename) { + fprintf_ind(file, ind, "original filename (sanitized): %s\n", + sanitized_filename(frame->original_filename)); + fprintf_ind(file, ind, "original file %s\n", + gr_is_original_file_still_available(frame) + ? "is still available" + : "is NOT available anymore"); + } + if (frame->uploading_failure) + fprintf_ind(file, ind, "uploading failure: %s\n", + image_uploading_failure_strings + [frame->uploading_failure]); + fprintf_ind(file, ind, "gap: %d\n", frame->gap); + fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, + gr_ago(now - frame->atime)); + fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, + frame->data_pix_height); + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (access(filename, F_OK) != -1) + fprintf_ind(file, ind, "file: %s\n", + sanitized_filename(filename)); + else + fprintf_ind(file, ind, "not on disk\n"); + fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); + if (frame->imlib_object) { + unsigned ram_size = gr_frame_current_ram_size(frame); + fprintf_ind(file, ind, + "loaded into ram, size: %d " + "KiB\n", + ram_size / 1024); + } else { + fprintf_ind(file, ind, "not loaded into ram\n"); + } +} + +/// Dumps the placement info to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) { + fprintf_ind(file, ind, "Placement is NULL\n"); + return; + } + Milliseconds now = gr_now_ms(); + fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); + ind += 4; + fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, + gr_ago(now - placement->atime)); + fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); + fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, + placement->rows); + fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, + placement->scaled_ch); + PixmapTransformation *tr = &placement->pixmap_transformation; + fprintf_ind(file, ind, "pixmap size: %ux%u\n", tr->pixmap_w, + tr->pixmap_h); + fprintf_ind(file, ind, "dst size: %ux%u offset: (%d, %d)\n", tr->dst_w, + tr->dst_h, tr->dst_x, tr->dst_y); + fprintf_ind(file, ind, "ram per frame: %u KiB\n", + gr_placement_single_frame_ram_size(placement) / 1024); + unsigned ram_size = gr_placement_current_ram_size(placement); + fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); +} + +/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. +static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, + int ind) { + if (!placement) + return; + int frameidx = 1; + foreach_pixmap(*placement, pixmap, { + fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, + pixmap); + ++frameidx; + }); +} + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state() { + FILE *file = stderr; + int ind = 0; + fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); + fprintf_ind(file, ind, + "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " + "sizeof(ImagePlacement) = %lu\n", + sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); + fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); + fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); + fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", + images_ram_size / 1024); + fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", + images_disk_size / 1024); + + Milliseconds now = gr_now_ms(); + + int64_t images_ram_size_computed = 0; + int64_t images_disk_size_computed = 0; + + Image *img = NULL; + ImagePlacement *placement = NULL; + kh_foreach_value(images, img, { + fprintf_ind(file, ind, "----------------\n"); + gr_dump_image_info(file, img, 0); + int64_t total_disk_size_computed = 0; + int total_duration_computed = 0; + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + if (frame->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + total_duration_computed += frame->gap; + images_disk_size_computed += frame->disk_size; + total_disk_size_computed += frame->disk_size; + if (frame->imlib_object) + images_ram_size_computed += + gr_frame_current_ram_size(frame); + }); + if (img->total_disk_size != total_disk_size_computed) { + fprintf_ind(file, ind, + " ERROR: total_disk_size is %u, but " + "computed value is %ld\n", + img->total_disk_size, total_disk_size_computed); + } + if (img->total_duration != total_duration_computed) { + fprintf_ind(file, ind, + " ERROR: total_duration is %d, but computed " + "value is %d\n", + img->total_duration, total_duration_computed); + } + kh_foreach_value(img->placements, placement, { + gr_dump_placement_info(file, placement, 4); + if (placement->image != img) + fprintf_ind(file, 8, + "ERROR: WRONG IMAGE POINTER\n"); + fprintf_ind(file, 8, + "Pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 12); + unsigned ram_size = + gr_placement_current_ram_size(placement); + images_ram_size_computed += ram_size; + }); + }); + if (images_ram_size != images_ram_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_ram_size is %ld, but computed value " + "is %ld\n", + images_ram_size, images_ram_size_computed); + } + if (images_disk_size != images_disk_size_computed) { + fprintf_ind(file, ind, + "ERROR: images_disk_size is %ld, but computed value " + "is %ld\n", + images_disk_size, images_disk_size_computed); + } + fprintf_ind(file, ind, "===========================================\n"); +} + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +// TODO: Currently we do this for the first frame only. Not sure what to do with +// animations. +void gr_preview_image(uint32_t image_id, const char *exec) { + char command[256]; + size_t len; + Image *img = gr_find_image(image_id); + if (img) { + ImageFrame *frame = &img->first_frame; + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + if (frame->disk_size == 0) { + len = snprintf(command, 255, + "xmessage 'Image with id=%u is not " + "fully copied to %s'", + image_id, sanitized_filename(filename)); + } else { + len = snprintf(command, 255, "%s %s &", exec, + sanitized_filename(filename)); + } + } else { + len = snprintf(command, 255, + "xmessage 'Cannot find image with id=%u'", + image_id); + } + if (len > 255) { + fprintf(stderr, "error: command too long: %s\n", command); + snprintf(command, 255, "xmessage 'error: command too long'"); + } + if (system(command) != 0) { + fprintf(stderr, "error: could not execute command %s\n", + command); + } +} + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable) { + char filename[MAX_FILENAME_SIZE]; + snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); + FILE *file = fopen(filename, "w"); + if (!file) { + perror("fopen"); + return; + } + // Basic information about the cell. + fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); + fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); + fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); + fprintf(file, "classic/unicode placeholder = %s\n", + is_classic_placeholder ? "classic" : "unicode"); + fprintf(file, "original diacritic count = %d\n", diacritic_count); + // Information about the image and the placement. + Image *img = gr_find_image(image_id); + ImagePlacement *placement = gr_find_placement(img, placement_id); + gr_dump_image_info(file, img, 0); + gr_dump_placement_info(file, placement, 0); + // The text underneath this particular cell. + if (placement && placement->text_underneath && imgcol >= 1 && + imgrow >= 1 && imgcol <= placement->cols && + imgrow <= placement->rows) { + fprintf(file, "Glyph underneath:\n"); + Glyph *glyph = + &placement->text_underneath[(imgrow - 1) * + placement->cols + + imgcol - 1]; + fprintf(file, " rune = 0x%08X\n", glyph->u); + fprintf(file, " bg = 0x%08X\n", glyph->bg); + fprintf(file, " fg = 0x%08X\n", glyph->fg); + fprintf(file, " decor = 0x%08X\n", glyph->decor); + fprintf(file, " mode = 0x%08X\n", glyph->mode); + } + if (img) { + fprintf(file, "Frames:\n"); + foreach_frame(*img, frame, { + gr_dump_frame_info(file, frame, 4); + }); + } + if (placement) { + fprintf(file, "Placement pixmaps:\n"); + gr_dump_placement_pixmaps(file, placement, 4); + } + fclose(file); + char *argv[] = {st_executable, "-e", "less", filename, NULL}; + if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) { + perror("posix_spawnp"); + return; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Appending and displaying image rectangles. +//////////////////////////////////////////////////////////////////////////////// + +/// Displays debug information in the rectangle using colors col1 and col2. +static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2, + const char *message) { + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + char info[MAX_INFO_LEN]; + if (rect->placement_id) + snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message, + rect->image_id, rect->placement_id, + rect->img_start_col, rect->img_end_col, + rect->img_start_row, rect->img_end_row); + else + snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message, + rect->image_id, rect->img_start_col, rect->img_end_col, + rect->img_start_row, rect->img_end_row); + XSetForeground(disp, gc, col1); + XDrawString(disp, buf, gc, rect->screen_x_pix + 4, + rect->screen_y_pix + h_pix - 3, info, strlen(info)); + XSetForeground(disp, gc, col2); + XDrawString(disp, buf, gc, rect->screen_x_pix + 2, + rect->screen_y_pix + h_pix - 5, info, strlen(info)); + XFreeGC(disp, gc); +} + +/// Draws a rectangle (bounding box) for debugging. +static void gr_showrect(Drawable buf, ImageRect *rect) { + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + XSetForeground(disp, gc, 0xFF00FF00); + XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix, + w_pix - 1, h_pix - 1); + XSetForeground(disp, gc, 0xFFFF0000); + XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1, + rect->screen_y_pix + 1, w_pix - 3, h_pix - 3); + XFreeGC(disp, gc); +} + +/// Updates the next redraw time for the given row. Resizes the +/// next_redraw_times array if needed. +static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) { + if (next_redraw == 0) + return; + if (row >= kv_size(next_redraw_times)) { + size_t old_size = kv_size(next_redraw_times); + kv_a(Milliseconds, next_redraw_times, row); + for (size_t i = old_size; i <= row; ++i) + kv_A(next_redraw_times, i) = 0; + } + Milliseconds old_value = kv_A(next_redraw_times, row); + if (old_value == 0 || old_value > next_redraw) + kv_A(next_redraw_times, row) = next_redraw; +} + +/// Draws the given part of an image. +static void gr_drawimagerect(Drawable buf, ImageRect *rect) { + ImagePlacement *placement = + gr_find_image_and_placement(rect->image_id, rect->placement_id); + // If the image does not exist or image display is switched off, draw + // the bounding box. + if (!placement || !graphics_display_images) { + gr_showrect(buf, rect); + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + return; + } + + Image *img = placement->image; + + if (img->last_redraw < drawing_start_time) { + // This is the first time we draw this image in this redraw + // cycle. Update the frame index we are going to display. Note + // that currently all image placements are synchronized. + int old_frame = img->current_frame; + gr_update_frame_index(img, drawing_start_time); + img->last_redraw = drawing_start_time; + } + + // Adjust next redraw times for the rows of this image rect. + if (img->next_redraw) { + for (int row = rect->screen_y_row; + row <= rect->screen_y_row + rect->img_end_row - + rect->img_start_row - 1; ++row) { + gr_update_next_redraw_time( + row, img->next_redraw); + } + } + + // Load the frame. + Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw, + rect->ch); + + // If the image couldn't be loaded, display the bounding box. + if (!pixmap) { + gr_showrect(buf, rect); + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + return; + } + + // The coordinates and size (in pixels) of the src rectangle inside the + // box of cells (not the pixmap). + int src_x = rect->img_start_col * rect->cw; + int src_y = rect->img_start_row * rect->ch; + int src_w = (rect->img_end_col - rect->img_start_col) * rect->cw; + int src_h = (rect->img_end_row - rect->img_start_row) * rect->ch; + // The coordinates of the dst rectangle inside the window. The size is + // the same as the src size in the box (src_w, src_h). + int window_x = rect->screen_x_pix; + int window_y = rect->screen_y_pix; + + Display *disp = imlib_context_get_display(); + Visual *vis = imlib_context_get_visual(); + + // Create an xrender picture for the window. + XRenderPictFormat *win_format = + XRenderFindVisualFormat(disp, vis); + Picture window_pic = + XRenderCreatePicture(disp, buf, win_format, 0, NULL); + + // If needed, invert the image pixmap. Note that this naive approach of + // inverting the pixmap is not entirely correct, because the pixmap is + // premultiplied. But the result is good enough to visually indicate + // selection. + if (rect->reverse) { + unsigned pixmap_w = placement->pixmap_transformation.pixmap_w; + unsigned pixmap_h = placement->pixmap_transformation.pixmap_h; + Pixmap invpixmap = + XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32); + XGCValues gcv = {.function = GXcopyInverted}; + GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv); + XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w, + pixmap_h, 0, 0); + XFreeGC(disp, gc); + pixmap = invpixmap; + } + + // Create a picture for the image pixmap. + XRenderPictFormat *pic_format = + XRenderFindStandardFormat(disp, PictStandardARGB32); + // We use RepeatPad to avoid bilinear filtering halo. + XRenderPictureAttributes attrs = {0}; + attrs.repeat = RepeatPad; + Picture pixmap_pic = XRenderCreatePicture(disp, pixmap, pic_format, + CPRepeat, &attrs); + + // Since the pixmap may be of different size than the destination box of + // cells, we must apply a transformation to it. + // The XTransform structure describes a matrix to transform the + // destination picture coordinates (i.e. in the box) to the source + // coordinates (i.e. in the pixmap). We apply only scaling (translation + // will be applied to the src coordinates directly): + // + // pixmap_x = picture_x * pixmap_w / dst_w + // pixmap_y = picture_y * pixmap_h / dst_h + // + // Where dst_w, dst_h, pixmap_w, pixmap_h are from the placement's + // pixmap_transformation structure. + PixmapTransformation *tr = &placement->pixmap_transformation; + double xs = (double)tr->pixmap_w / MAX(tr->dst_w, 1); + double ys = (double)tr->pixmap_h / MAX(tr->dst_h, 1); + // clang-format off + XTransform xform = {{ + { XDoubleToFixed(xs), XDoubleToFixed( 0), XDoubleToFixed( 0) }, + { XDoubleToFixed( 0), XDoubleToFixed(ys), XDoubleToFixed( 0) }, + { XDoubleToFixed( 0), XDoubleToFixed( 0), XDoubleToFixed( 1) } + }}; + // clang-format on + XRenderSetPictureTransform(disp, pixmap_pic, &xform); + XRenderSetPictureFilter(disp, pixmap_pic, FilterBilinear, NULL, 0); + + // Do the translation: modify the src coordinates to make them + // coordinates into the picture rather than into the box. + src_x -= tr->dst_x; + src_y -= tr->dst_y; + // At this point src_x, src_y, src_w, src_h are coordinates of the + // source rectangle inside the picture (scaled pixmap). + + // Now do clipping. If src coordinates are negative, adjust the dst + // coordinates (into the window) and the width/height. We do clipping + // instead of using the values as is to avoid rendering the pad area + // outside the pixmap. + if (src_x < 0) { + window_x += -src_x; + src_w -= -src_x; + src_x = 0; + } + if (src_y < 0) { + window_y += -src_y; + src_h -= -src_y; + src_y = 0; + } + + // Adjust width and height if the src rectangle exceeds the picture. + src_w = MIN(src_w, tr->dst_w - src_x); + src_h = MIN(src_h, tr->dst_h - src_y); + + // Composite the image onto the window. In the reverse mode we ignore + // the alpha channel of the image because the naive inversion above + // seems to invert the alpha channel as well. + int pictop = rect->reverse ? PictOpSrc : PictOpOver; + if (src_w > 0 && src_h > 0) + XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, src_x, + src_y, src_x, src_y, window_x, window_y, src_w, + src_h); + + // Free resources + XRenderFreePicture(disp, pixmap_pic); + XRenderFreePicture(disp, window_pic); + if (rect->reverse) + XFreePixmap(disp, pixmap); + + // In debug mode always draw bounding boxes and print info. + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) { + gr_showrect(buf, rect); + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); + } +} + +/// Removes the given image rectangle. +static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); } + +/// Returns the bottom coordinate of the rect. +static int gr_getrectbottom(ImageRect *rect) { + return rect->screen_y_pix + + (rect->img_end_row - rect->img_start_row) * rect->ch; +} + +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch) { + current_cw = cw; + current_ch = ch; + debug_loaded_files_counter = 0; + debug_loaded_pixmaps_counter = 0; + drawing_start_time = gr_now_ms(); + imlib_context_set_drawable(buf); +} + +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf) { + // Draw and then delete all known image rectangles. + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (!rect->image_id) + continue; + gr_drawimagerect(buf, rect); + gr_freerect(rect); + } + + // Compute the delay until the next redraw as the minimum of the next + // redraw delays for all rows. + Milliseconds drawing_end_time = gr_now_ms(); + graphics_next_redraw_delay = INT_MAX; + for (int row = 0; row < kv_size(next_redraw_times); ++row) { + Milliseconds row_next_redraw = kv_A(next_redraw_times, row); + if (row_next_redraw > 0) { + int delay = MAX(graphics_animation_min_delay, + row_next_redraw - drawing_end_time); + graphics_next_redraw_delay = + MIN(graphics_next_redraw_delay, delay); + } + } + + // In debug mode display additional info. + if (graphics_debug_mode) { + int milliseconds = drawing_end_time - drawing_start_time; + + Display *disp = imlib_context_get_display(); + GC gc = XCreateGC(disp, buf, 0, NULL); + const char *debug_mode_str = + graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES + ? "(boxes shown) " + : ""; + int redraw_delay = graphics_next_redraw_delay == INT_MAX + ? -1 + : graphics_next_redraw_delay; + char info[MAX_INFO_LEN]; + snprintf(info, MAX_INFO_LEN, + "%sRender time: %d ms ram %ld K disk %ld K count " + "%d cell %dx%d delay %d", + debug_mode_str, milliseconds, images_ram_size / 1024, + images_disk_size / 1024, kh_size(images), current_cw, + current_ch, redraw_delay); + XSetForeground(disp, gc, 0xFF000000); + XFillRectangle(disp, buf, gc, 0, 0, 600, 16); + XSetForeground(disp, gc, 0xFFFFFFFF); + XDrawString(disp, buf, gc, 0, 14, info, strlen(info)); + XFreeGC(disp, gc); + + if (milliseconds > 0) { + fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n", + info, debug_loaded_files_counter, + debug_loaded_pixmaps_counter); + } + } + + // Check the limits in case we have used too much ram for placements. + gr_check_limits(); +} + +// Add an image rectangle to the list of rectangles to draw. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse) { + current_cw = cw; + current_ch = ch; + + ImageRect new_rect; + new_rect.image_id = image_id; + new_rect.placement_id = placement_id; + new_rect.img_start_col = img_start_col; + new_rect.img_end_col = img_end_col; + new_rect.img_start_row = img_start_row; + new_rect.img_end_row = img_end_row; + new_rect.screen_y_row = y_row; + new_rect.screen_x_pix = x_pix; + new_rect.screen_y_pix = y_pix; + new_rect.ch = ch; + new_rect.cw = cw; + new_rect.reverse = reverse; + + // Display some red text in debug mode. + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) + gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? "); + + // If it's the empty image (image_id=0) or an empty rectangle, do + // nothing. + if (image_id == 0 || img_end_col - img_start_col <= 0 || + img_end_row - img_start_row <= 0) + return; + // Try to find a rect to merge with. + ImageRect *free_rect = NULL; + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (rect->image_id == 0) { + if (!free_rect) + free_rect = rect; + continue; + } + if (rect->image_id != image_id || + rect->placement_id != placement_id || rect->cw != cw || + rect->ch != ch || rect->reverse != reverse) + continue; + // We only support the case when the new stripe is added to the + // bottom of an existing rectangle and they are perfectly + // aligned. + if (rect->img_end_row == img_start_row && + gr_getrectbottom(rect) == y_pix) { + if (rect->img_start_col == img_start_col && + rect->img_end_col == img_end_col && + rect->screen_x_pix == x_pix) { + rect->img_end_row = img_end_row; + return; + } + } + } + // If we haven't merged the new rect with any existing rect, and there + // is no free rect, we have to render one of the existing rects. + if (!free_rect) { + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { + ImageRect *rect = &image_rects[i]; + if (!free_rect || gr_getrectbottom(free_rect) > + gr_getrectbottom(rect)) + free_rect = rect; + } + gr_drawimagerect(buf, free_rect); + gr_freerect(free_rect); + } + // Start a new rectangle in `free_rect`. + *free_rect = new_rect; +} + +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows) { + if (rows < kv_size(next_redraw_times)) + kv_size(next_redraw_times) = rows; + if (rows * 2 < kv_max(next_redraw_times)) + kv_resize(Milliseconds, next_redraw_times, rows); + for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) { + if (dirty[i]) { + kv_A(next_redraw_times, i) = 0; + continue; + } + Milliseconds next_update = kv_A(next_redraw_times, i); + if (next_update > 0 && next_update <= drawing_start_time) { + dirty[i] = 1; + kv_A(next_redraw_times, i) = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Command parsing and handling. +//////////////////////////////////////////////////////////////////////////////// + +/// A parsed kitty graphics protocol command. +typedef struct { + /// The command itself, without the 'G'. + char *command; + /// The payload (after ';'). + char *payload; + /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'. + char action; + /// 'q=', 1 to suppress OK response, 2 to suppress errors too. + int quiet; + /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with + /// imlib2. If 'f=0', will try to load with imlib2, then fallback to + /// 32-bit pixel data. + int format; + /// 'o=', may be 'z' for RFC 1950 ZLIB. + int compression; + /// 't=', may be 'f', 't' or 'd'. + char transmission_medium; + /// 'd=' + char delete_specifier; + /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'. + /// When 'a=f', this is the size of the frame rectangle when composed on + /// top of another frame. + int frame_pix_width, frame_pix_height; + /// 'x=', 'y=' - top-left corner of the source rectangle. + int src_pix_x, src_pix_y; + /// 'w=', 'h=' - width and height of the source rectangle. + int src_pix_width, src_pix_height; + /// 'r=', 'c=' + int rows, columns; + /// 'i=' + uint32_t image_id; + /// 'I=' + uint32_t image_number; + /// 'p=' + uint32_t placement_id; + /// 'm=', may be 0 or 1. + int more; + /// True if turns out that this command is a continuation of a data + /// transmission and not the first one for this image. Populated by + /// `gr_handle_transmit_command`. + char is_direct_transmission_continuation; + /// 'S=', used to check the size of uploaded data. + int size; + /// The offset of the frame image data in the shared memory ('O='). + unsigned offset; + /// 'U=', whether it's a virtual placement for Unicode placeholders. + int virtual; + /// 'C=', if true, do not move the cursor when displaying this placement + /// (non-virtual placements only). + char do_not_move_cursor; + // --------------------------------------------------------------------- + // Animation-related fields. Their keys often overlap with keys of other + // commands, so these make sense only if the action is 'a=f' (frame + // transmission) or 'a=a' (animation control). + // + // 'x=' and 'y=', the relative position of the frame image when it's + // composed on top of another frame. + int frame_dst_pix_x, frame_dst_pix_y; + /// 'X=', 'X=1' to replace colors instead of alpha blending on top of + /// the background color or frame. + char replace_instead_of_blending; + /// 'Y=', the background color in the 0xRRGGBBAA format (still + /// transmitted as a decimal number). + uint32_t background_color; + /// (Only for 'a=f'). 'c=', the 1-based index of the background frame. + int background_frame; + /// (Only for 'a=a'). 'c=', sets the index of the current frame. + int current_frame; + /// 'r=', the 1-based index of the frame to edit. + int edit_frame; + /// 'z=', the duration of the frame. Zero if not specified, negative if + /// the frame is gapless (i.e. skipped). + int gap; + /// (Only for 'a=a'). 's=', if non-zero, sets the state of the + /// animation, 1 to stop, 2 to run in loading mode, 3 to loop. + int animation_state; + /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the + /// animation will loop. 1 to loop infinitely, N to loop N-1 times. + int loops; +} GraphicsCommand; + +/// Replaces all non-printed characters in `str` with '?' and truncates the +/// string to `max_size`, maybe inserting ellipsis at the end. +static void sanitize_str(char *str, size_t max_size) { + assert(max_size >= 4); + for (size_t i = 0; i < max_size; ++i) { + unsigned c = str[i]; + if (c == '\0') + return; + if (c >= 128 || !isprint(c)) + str[i] = '?'; + } + str[max_size - 1] = '\0'; + str[max_size - 2] = '.'; + str[max_size - 3] = '.'; + str[max_size - 4] = '.'; +} + +/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be +/// careful. +static const char *sanitized_filename(const char *str) { + static char buf[MAX_FILENAME_SIZE]; + strncpy(buf, str, sizeof(buf)); + sanitize_str(buf, sizeof(buf)); + return buf; +} + +/// Creates a response to the current command in `graphics_command_result`. +static void gr_createresponse(uint32_t image_id, uint32_t image_number, + uint32_t placement_id, const char *msg) { + if (!image_id && !image_number && !placement_id) { + // Nobody expects the response in this case, so just print it to + // stderr. + fprintf(stderr, + "error: No image id or image number or placement_id, " + "but still there is a response: %s\n", + msg); + return; + } + char *buf = graphics_command_result.response; + size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN; + size_t written; + written = snprintf(buf, maxlen, "\033_G"); + buf += written; + maxlen -= written; + if (image_id) { + written = snprintf(buf, maxlen, "i=%u,", image_id); + buf += written; + maxlen -= written; + } + if (image_number) { + written = snprintf(buf, maxlen, "I=%u,", image_number); + buf += written; + maxlen -= written; + } + if (placement_id) { + written = snprintf(buf, maxlen, "p=%u,", placement_id); + buf += written; + maxlen -= written; + } + buf[-1] = ';'; + written = snprintf(buf, maxlen, "%s\033\\", msg); + buf += written; + maxlen -= written; + buf[-2] = '\033'; + buf[-1] = '\\'; +} + +/// Creates the 'OK' response to the current command, unless suppressed or a +/// non-final data transmission. +static void gr_reportsuccess_cmd(GraphicsCommand *cmd) { + if (cmd->quiet < 1 && !cmd->more) + gr_createresponse(cmd->image_id, cmd->image_number, + cmd->placement_id, "OK"); +} + +/// Creates the 'OK' response to the current command (unless suppressed). +static void gr_reportsuccess_frame(ImageFrame *frame) { + uint32_t id = frame->image->query_id ? frame->image->query_id + : frame->image->image_id; + if (frame->quiet < 1) + gr_createresponse(id, frame->image->image_number, + frame->image->initial_placement_id, "OK"); +} + +/// Creates an error response to the current command (unless suppressed). +static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) { + char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; + graphics_command_result.error = 1; + va_list args; + va_start(args, format); + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); + va_end(args); + + fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command); + if (cmd->quiet < 2) + gr_createresponse(cmd->image_id, cmd->image_number, + cmd->placement_id, errmsg); +} + +/// Creates an error response to the current command (unless suppressed). +static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) { + char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; + graphics_command_result.error = 1; + va_list args; + va_start(args, format); + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); + va_end(args); + + if (!frame) { + fprintf(stderr, "%s\n", errmsg); + gr_createresponse(0, 0, 0, errmsg); + } else { + uint32_t id = frame->image->query_id ? frame->image->query_id + : frame->image->image_id; + fprintf(stderr, "%s id=%u\n", errmsg, id); + if (frame->quiet < 2) + gr_createresponse(id, frame->image->image_number, + frame->image->initial_placement_id, + errmsg); + } +} + +/// Loads an image and creates a success/failure response. Returns `frame`, or +/// NULL if it's a query action and the image was deleted. +static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) { + gr_load_imlib_object(frame); + if (!frame->imlib_object) { + gr_reporterror_frame(frame, "EBADF: could not load image"); + } else { + gr_reportsuccess_frame(frame); + } + // If it was a query action, discard the image. + if (frame->image->query_id) { + gr_delete_image(frame->image); + return NULL; + } + return frame; +} + +/// Creates an appropriate uploading failure response to the current command. +static void gr_reportuploaderror(ImageFrame *frame) { + switch (frame->uploading_failure) { + case 0: + return; + case ERROR_CANNOT_OPEN_CACHED_FILE: + gr_reporterror_frame(frame, + "EIO: could not create a file for image"); + break; + case ERROR_OVER_SIZE_LIMIT: + gr_reporterror_frame( + frame, + "EFBIG: the size of the uploaded image exceeded " + "the image size limit %u", + graphics_max_single_image_file_size); + break; + case ERROR_UNEXPECTED_SIZE: + gr_reporterror_frame(frame, + "EINVAL: the size of the uploaded image %u " + "doesn't match the expected size %u", + frame->disk_size, frame->expected_size); + break; + }; +} + +/// Displays a non-virtual placement. This functions records the information in +/// `graphics_command_result`, the placeholder itself is created by the terminal +/// after handling the current command in the graphics module. +static void gr_display_nonvirtual_placement(ImagePlacement *placement) { + if (placement->virtual) + return; + if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS) + return; + // Infer the placement size if needed. + gr_infer_placement_size_maybe(placement); + // Populate the information about the placeholder which will be created + // by the terminal. + graphics_command_result.create_placeholder = 1; + graphics_command_result.placeholder.image_id = placement->image->image_id; + graphics_command_result.placeholder.placement_id = placement->placement_id; + graphics_command_result.placeholder.columns = placement->cols; + graphics_command_result.placeholder.rows = placement->rows; + graphics_command_result.placeholder.do_not_move_cursor = + placement->do_not_move_cursor; + placement->text_underneath = + calloc(placement->rows * placement->cols, sizeof(Glyph)); + graphics_command_result.placeholder.text_underneath = + placement->text_underneath; + GR_LOG("Creating a placeholder for %u/%u %d x %d\n", + placement->image->image_id, placement->placement_id, + placement->cols, placement->rows); +} + +/// Marks the rows that are occupied by the image as dirty. +static void gr_schedule_image_redraw(Image *img) { + if (!img) + return; + gr_schedule_image_redraw_by_id(img->image_id); +} + +/// Closes the file currently being uploaded. This doesn't necessarily finish +/// the upload since the file may be reopened. +static void gr_close_current_upload_file() { + Image *img = gr_find_image(current_upload_image_id); + ImageFrame *frame = gr_get_frame(img, current_upload_frame_index); + gr_close_disk_cache_file(frame); +} + +/// Sets the current image and frame being uploaded. Closes the previous upload +/// file if it's changed. If `frame` is NULL, clears the current upload +/// image/frame. +static void gr_set_current_upload_frame(ImageFrame *frame) { + if (frame) { + if (current_upload_image_id != frame->image->image_id || + current_upload_frame_index != frame->index) { + gr_close_current_upload_file(); + } + current_upload_image_id = frame->image->image_id; + current_upload_frame_index = frame->index; + GR_LOG("Set current_upload_image_id = %u, " + "current_upload_frame_index = %u\n", + current_upload_image_id, current_upload_frame_index); + } else { + gr_close_current_upload_file(); + current_upload_image_id = 0; + current_upload_frame_index = 0; + GR_LOG("Set current_upload_image_id = 0\n"); + } +} + +/// Returns whether direct transmission continuation is allowed for the given +/// command and frame. +static int gr_transmission_continuation_is_allowed(GraphicsCommand *cmd, + ImageFrame *frame) { + if (!frame || frame->status != STATUS_UPLOADING) + return 0; + + // If it's the same image and frame as the current upload, allow it. + if (current_upload_image_id == frame->image->image_id && + current_upload_frame_index == frame->index) + return 1; + + // Otherwise it's a continuation of an interrupted upload. The kitty + // graphics protocol doesn't allow interleaving of direct transmission + // with other commands, so interrupted uploads must be aborted. However, + // we still allow it as an extension, because it's useful for + // protocol-unaware multiplexer. We check that there are no + // contradictions, and the time since the last upload activity is small. + + if (cmd->size && cmd->size != frame->expected_size) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of expected size mismatch\n"); + return 0; + } + if (cmd->format && cmd->format != frame->format) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of format mismatch\n"); + return 0; + } + if (cmd->compression && cmd->compression != frame->compression) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of compression mismatch\n"); + return 0; + } + if ((cmd->frame_pix_width && + cmd->frame_pix_width != frame->data_pix_width) || + (cmd->frame_pix_height && + cmd->frame_pix_height != frame->data_pix_height) || + (cmd->background_color && + cmd->background_color != frame->background_color) || + (cmd->background_frame && + cmd->background_frame != frame->background_frame_index) || + (cmd->gap && cmd->gap != frame->gap) || + (cmd->replace_instead_of_blending && + cmd->replace_instead_of_blending != !frame->blend)) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of frame parameters mismatch\n"); + return 0; + } + + Milliseconds now = gr_now_ms(); + if (now - frame->atime > graphics_direct_transmission_timeout_ms) { + fprintf(stderr, "warning: Not resuming interrupted upload " + "because of time out\n"); + return 0; + } + + return 1; +} + +/// Appends `data` to the on-disk cache file of the frame `frame`. Creates the +/// file if it doesn't exist. Updates `frame->disk_size` and the total disk +/// size. Returns 1 on success and 0 on failure. +static int gr_append_raw_data_to_file(ImageFrame *frame, const char *data, + size_t data_size) { + // If there is no open file corresponding to the image, create it. + if (!frame->open_file) { + gr_make_sure_tmpdir_exists(); + char filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); + FILE *file = fopen(filename, frame->disk_size ? "a" : "w"); + if (!file) + return 0; + frame->open_file = file; + } + + // Write data to the file and update disk size variables. + fwrite(data, 1, data_size, frame->open_file); + frame->disk_size += data_size; + frame->image->total_disk_size += data_size; + images_disk_size += data_size; + gr_touch_frame(frame); + return 1; +} + +/// Appends data from `payload` to the frame `frame` when using direct +/// transmission. Note that we report errors only for the final command +/// (`!more`) to avoid spamming the client. If the frame is not specified, use +/// the image id and frame index we are currently uploading. +static void gr_append_data(ImageFrame *frame, const char *payload, int more) { + gr_set_current_upload_frame(frame); + + if (frame->status != STATUS_UPLOADING) { + if (!more) + gr_reportuploaderror(frame); + gr_set_current_upload_frame(NULL); + return; + } + + // Decode the data. + size_t data_size = 0; + char *data = gr_base64dec(payload, &data_size); + + GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size, + frame->disk_size + data_size); + + // Do not append this data if the image exceeds the size limit. + if (frame->disk_size + data_size > + graphics_max_single_image_file_size || + frame->expected_size > graphics_max_single_image_file_size) { + free(data); + gr_delete_imagefile(frame); + frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; + if (!more) + gr_reportuploaderror(frame); + gr_set_current_upload_frame(NULL); + return; + } + + // Append the data to the file. + if (!gr_append_raw_data_to_file(frame, data, data_size)) { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; + if (!more) + gr_reportuploaderror(frame); + gr_set_current_upload_frame(NULL); + return; + } + free(data); + + if (!more) { + gr_set_current_upload_frame(NULL); + frame->status = STATUS_UPLOADING_SUCCESS; + uint32_t placement_id = frame->image->default_placement; + if (frame->expected_size && + frame->expected_size != frame->disk_size) { + // Report failure if the uploaded image size doesn't + // match the expected size. + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + gr_reportuploaderror(frame); + } else { + // Make sure to redraw all existing image instances. + gr_schedule_image_redraw(frame->image); + // Try to load the image into ram and report the result. + frame = gr_loadimage_and_report(frame); + // If there is a non-virtual image placement, we may + // need to display it. + if (frame && frame->index == 1) { + Image *img = frame->image; + ImagePlacement *placement = NULL; + kh_foreach_value(img->placements, placement, { + gr_display_nonvirtual_placement(placement); + }); + } + } + } + + // Check whether we need to delete old images. + gr_check_limits(); +} + +/// Finds the image either by id or by number specified in the command. +static Image *gr_find_image_for_command(GraphicsCommand *cmd) { + if (cmd->image_id) + return gr_find_image(cmd->image_id); + Image *img = NULL; + // If the image number is not specified, we can't find the image, unless + // it's a put command, in which case we will try the last image. + if (cmd->image_number == 0 && cmd->action == 'p') + img = gr_find_image(last_image_id); + else + img = gr_find_image_by_number(cmd->image_number); + return img; +} + +/// Creates a new image or a new frame in an existing image (depending on the +/// command's action) and initializes its parameters from the command. +static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) { + if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 && + cmd->compression != 0) { + gr_reporterror_cmd(cmd, "EINVAL: compression is supported only " + "for raw pixel data (f=32 or f=24)"); + // Even though we report an error, we still create an image. + } + + Image *img = NULL; + if (cmd->action == 'f') { + // If it's a frame transmission action, there must be an + // existing image. + img = gr_find_image_for_command(cmd); + if (img) { + cmd->image_id = img->image_id; + } else { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return NULL; + } + } else { + // Otherwise create a new image object. If the action is `q`, + // we'll use random id instead of the one specified in the + // command. + uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id; + img = gr_new_image(image_id); + if (!img) + return NULL; + if (cmd->action == 'q') + img->query_id = cmd->image_id; + else if (!cmd->image_id) + cmd->image_id = img->image_id; + // Set the image number. + img->image_number = cmd->image_number; + } + + ImageFrame *frame = gr_append_new_frame(img); + // Initialize the frame. + frame->expected_size = cmd->size; + // The default format is 32. + frame->format = cmd->format ? cmd->format : 32; + frame->compression = cmd->compression; + frame->background_color = cmd->background_color; + frame->background_frame_index = cmd->background_frame; + frame->gap = cmd->gap; + img->total_duration += frame->gap; + frame->blend = !cmd->replace_instead_of_blending; + frame->data_pix_width = cmd->frame_pix_width; + frame->data_pix_height = cmd->frame_pix_height; + if (cmd->action == 'f') { + frame->x = cmd->frame_dst_pix_x; + frame->y = cmd->frame_dst_pix_y; + } + // If the expected size is not specified, we can infer it from the pixel + // width and height if the format is 24 or 32 and there is no + // compression. This is required for the shared memory transmission. + if (!frame->expected_size && !frame->compression && + (frame->format == 24 || frame->format == 32)) { + frame->expected_size = frame->data_pix_width * + frame->data_pix_height * + (frame->format / 8); + } + // We save the quietness information in the frame because for direct + // transmission subsequent transmission command won't contain this info. + frame->quiet = cmd->quiet; + return frame; +} + +/// Removes a file if it actually looks like a temporary file. +static void gr_delete_tmp_file(const char *filename) { + if (strstr(filename, "tty-graphics-protocol") == NULL) + return; + if (strstr(filename, "/tmp/") != filename) { + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir || !tmpdir[0] || + strstr(filename, tmpdir) != filename) + return; + } + unlink(filename); +} + +/// Copy the image file `frame->original_filename` to the cache directory. This +/// is done when the image is transmitted via file transfer, or when we have +/// evicted the image from the disk cache and need to restore it. +/// If `cmd` is not NULL, it's used to report errors, otherwise errors are only +/// printed to stderr. +static void gr_copy_imagefile(ImageFrame *frame, GraphicsCommand *cmd) { + GR_LOG("Copying image %s\n", + sanitized_filename(frame->original_filename)); + // Stat the file and check that it's a regular file and not too big. + struct stat st; + int stat_res = stat(frame->original_filename, &st); + + const char *stat_error = NULL; + if (stat_res) + stat_error = strerror(errno); + else if (!S_ISREG(st.st_mode)) + stat_error = "Not a regular file"; + else if (st.st_size == 0) + stat_error = "The size of the file is zero"; + else if (st.st_size > graphics_max_single_image_file_size) + stat_error = "The file is too large"; + if (stat_error) { + fprintf(stderr, "Could not load the file %s: %s\n", + sanitized_filename(frame->original_filename), + stat_error); + if (cmd) + gr_reporterror_cmd(cmd, "EBADF: %s", stat_error); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + return; + } + + // Check the expected size if specified. + if (frame->expected_size && frame->expected_size != st.st_size) { + fprintf(stderr, + "Could not load, the size doesn't match: %s expected " + "%u vs actual %ld\n", + sanitized_filename(frame->original_filename), + frame->expected_size, st.st_size); + // The file has unexpected size. + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + if (cmd) + gr_reportuploaderror(frame); + return; + } + + // If we know the original modification time, we are trying to restore + // the evicted image file. Check that the modification time matches. + if (frame->original_file_mtime && + frame->original_file_mtime != st.st_mtime) { + fprintf(stderr, "Could not load, the mtime doesn't match: %s\n", + sanitized_filename(frame->original_filename)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_MTIME_MISMATCH; + if (cmd) + gr_reportuploaderror(frame); + return; + } + + frame->original_file_mtime = st.st_mtime; + + gr_make_sure_tmpdir_exists(); + // Build the filename for the cached copy of the file. + char cache_filename[MAX_FILENAME_SIZE]; + gr_get_frame_filename(frame, cache_filename, MAX_FILENAME_SIZE); + // We will create a symlink to the original file, and + // then copy the file to the temporary cache dir. We do + // this symlink trick mostly to be able to use cp for + // copying, and avoid escaping file name characters when + // calling system at the same time. + char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0}; + strcat(tmp_filename_symlink, cache_filename); + strcat(tmp_filename_symlink, ".sym"); + char command[MAX_FILENAME_SIZE + 256]; + size_t len = snprintf(command, MAX_FILENAME_SIZE + 255, "cp '%s' '%s'", + tmp_filename_symlink, cache_filename); + + if (len > MAX_FILENAME_SIZE + 255 || + symlink(frame->original_filename, tmp_filename_symlink) || + system(command) != 0) { + fprintf(stderr, + "Could not copy the image " + "%s (symlink %s) to %s", + sanitized_filename(frame->original_filename), + tmp_filename_symlink, cache_filename); + if (cmd) + gr_reporterror_cmd(cmd, "EBADF: could not copy the " + "image to the cache dir"); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_COPY_FILE; + // Delete the symlink. + unlink(tmp_filename_symlink); + return; + } + + // Delete the symlink. + unlink(tmp_filename_symlink); + // Set the status and update disk size variables. + frame->status = STATUS_UPLOADING_SUCCESS; + frame->disk_size = st.st_size; + frame->image->total_disk_size += st.st_size; + images_disk_size += frame->disk_size; +} + +/// Tries to restore the image file for `frame` if the original file is still +/// available. +static void gr_try_restore_imagefile(ImageFrame *frame) { + if (frame->disk_size != 0) + return; + if (gr_is_original_file_still_available(frame)) + gr_copy_imagefile(frame, NULL); +} + +/// Handles a data transmission command. +static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) { + // The default is direct transmission. + if (!cmd->transmission_medium) + cmd->transmission_medium = 'd'; + + // If neither id, nor image number is specified, and the transmission + // medium is 'd' (or unspecified), and there is an active direct upload, + // this is a continuation of the upload. + if (current_upload_image_id != 0 && cmd->image_id == 0 && + cmd->image_number == 0 && cmd->transmission_medium == 'd') { + cmd->image_id = current_upload_image_id; + GR_LOG("No images id is specified, continuing uploading %u\n", + cmd->image_id); + } + + ImageFrame *frame = NULL; + if (cmd->transmission_medium == 'f' || + cmd->transmission_medium == 't') { + // File transmission. + // Create a new image or a new frame of an existing image. + frame = gr_new_image_or_frame_from_command(cmd); + if (!frame) + return NULL; + last_image_id = frame->image->image_id; + // Decode the filename. + frame->original_filename = gr_base64dec(cmd->payload, NULL); + // Copy the file to the cache directory. + gr_copy_imagefile(frame, cmd); + if (frame->status == STATUS_UPLOADING_SUCCESS) { + // Everything seems fine, try to load and redraw + // existing instances. + gr_schedule_image_redraw(frame->image); + frame = gr_loadimage_and_report(frame); + } + // Delete the original file if it's temporary. + if (cmd->transmission_medium == 't') + gr_delete_tmp_file(frame->original_filename); + gr_check_limits(); + } else if (cmd->transmission_medium == 'd') { + // Direct transmission (default if 't' is not specified). + frame = gr_get_last_frame(gr_find_image_for_command(cmd)); + if (gr_transmission_continuation_is_allowed(cmd, frame)) { + // This is a continuation of the previous transmission. + cmd->is_direct_transmission_continuation = 1; + cmd->image_id = frame->image->image_id; + gr_append_data(frame, cmd->payload, cmd->more); + return frame; + } + // Otherwise create a new image or frame structure. + frame = gr_new_image_or_frame_from_command(cmd); + if (!frame) + return NULL; + last_image_id = frame->image->image_id; + frame->status = STATUS_UPLOADING; + // Start appending data. + gr_append_data(frame, cmd->payload, cmd->more); + } else if (cmd->transmission_medium == 's') { + // Shared memory transmission. + // Create a new image or a new frame of an existing image. + frame = gr_new_image_or_frame_from_command(cmd); + if (!frame) + return NULL; + last_image_id = frame->image->image_id; + // Check that we know the size. + if (!frame->expected_size) { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_UNEXPECTED_SIZE; + gr_reporterror_cmd( + cmd, "EINVAL: the size of the image is not " + "specified and cannot be inferred"); + return frame; + } + // Check the data size limit. + if (frame->expected_size > graphics_max_single_image_file_size) { + frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; + gr_reportuploaderror(frame); + return frame; + } + // Decode the filename. + char *original_filename = gr_base64dec(cmd->payload, NULL); + GR_LOG("Loading image from shared memory %s\n", + sanitized_filename(original_filename)); + // Open the shared memory object. + int fd = shm_open(original_filename, O_RDONLY, 0); + if (fd == -1) { + gr_reporterror_cmd(cmd, "EBADF: shm_open: %s", + strerror(errno)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; + fprintf(stderr, "shm_open failed for %s\n", + sanitized_filename(original_filename)); + shm_unlink(original_filename); + free(original_filename); + return frame; + } + shm_unlink(original_filename); + free(original_filename); + // The offset we pass to mmap must be a multiple of the page + // size. If it's not, adjust it and the size. + size_t page_size = sysconf(_SC_PAGESIZE); + if (page_size == -1) + page_size = 1; + size_t offset = cmd->offset - (cmd->offset % page_size); + size_t size = frame->expected_size + (cmd->offset - offset); + // Map the shared memory object. + void *data = + mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset); + if (data == MAP_FAILED) { + gr_reporterror_cmd(cmd, "EBADF: mmap: %s", + strerror(errno)); + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; + fprintf(stderr, + "mmap failed for size = %ld, offset = %ld\n", + size, offset); + close(fd); + return frame; + } + close(fd); + // Append the data to the cache file. + if (gr_append_raw_data_to_file(frame, + data + (cmd->offset - offset), + frame->expected_size)) { + frame->status = STATUS_UPLOADING_SUCCESS; + } else { + frame->status = STATUS_UPLOADING_ERROR; + frame->uploading_failure = + ERROR_CANNOT_OPEN_CACHED_FILE; + gr_reportuploaderror(frame); + } + // Close the cache file. + gr_close_disk_cache_file(frame); + // Unmap the data + if (munmap(data, size) != 0) + fprintf(stderr, "munmap failed: %s\n", strerror(errno)); + // Try to load and redraw existing instances. + gr_schedule_image_redraw(frame->image); + frame = gr_loadimage_and_report(frame); + gr_check_limits(); + } else { + gr_reporterror_cmd( + cmd, + "EINVAL: transmission medium '%c' is not supported", + cmd->transmission_medium); + return NULL; + } + + return frame; +} + +/// Handles the 'put' command by creating a placement. +static void gr_handle_put_command(GraphicsCommand *cmd) { + if (cmd->image_id == 0 && cmd->image_number == 0) { + gr_reporterror_cmd(cmd, + "EINVAL: neither image id nor image number " + "are specified or both are zero"); + return; + } + + // Find the image with the id or number. + Image *img = gr_find_image_for_command(cmd); + if (img) { + cmd->image_id = img->image_id; + } else { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return; + } + + // Create a placement. If a placement with the same id already exists, + // it will be deleted. If the id is zero, a random id will be generated. + ImagePlacement *placement = gr_new_placement(img, cmd->placement_id); + placement->virtual = cmd->virtual; + placement->src_pix_x = cmd->src_pix_x; + placement->src_pix_y = cmd->src_pix_y; + placement->src_pix_width = cmd->src_pix_width; + placement->src_pix_height = cmd->src_pix_height; + placement->cols = cmd->columns; + placement->rows = cmd->rows; + placement->do_not_move_cursor = cmd->do_not_move_cursor; + + if (placement->virtual) { + placement->scale_mode = SCALE_MODE_CONTAIN; + } else if (placement->cols && placement->rows) { + // For classic placements the default is to stretch the image if + // both cols and rows are specified. + placement->scale_mode = SCALE_MODE_FILL; + } else if (placement->cols || placement->rows) { + // But if only one of them is specified, the default is to + // contain. + placement->scale_mode = SCALE_MODE_CONTAIN; + } else { + // If none of them are specified, the default is to use the + // original size. + placement->scale_mode = SCALE_MODE_NONE; + } + + // Display the placement unless it's virtual. + gr_display_nonvirtual_placement(placement); + + // Report success. + gr_reportsuccess_cmd(cmd); +} + +/// Information about what to delete. +typedef struct DeletionData { + uint32_t image_id; + uint32_t placement_id; + /// Visible placement found during screen traversal that need to be + /// deleted. Each placement must occur only once in this vector. + ImagePlacementVec placements_to_delete; +} DeletionData; + +/// The callback called for each cell to perform deletion. +static int gr_deletion_callback(void *data, Glyph *gp) { + DeletionData *del_data = data; + // Leave unicode placeholders alone. + if (!tgetisclassicplaceholder(gp)) + return 0; + uint32_t image_id = tgetimgid(gp); + uint32_t placement_id = tgetimgplacementid(gp); + if (del_data->image_id && del_data->image_id != image_id) + return 0; + if (del_data->placement_id && del_data->placement_id != placement_id) + return 0; + + ImagePlacement *placement = NULL; + + // Record the placement to delete. We will actually delete it later. + for (int i = 0; i < kv_size(del_data->placements_to_delete); ++i) { + ImagePlacement *cand = kv_A(del_data->placements_to_delete, i); + if (cand->image->image_id == image_id && + cand->placement_id == placement_id) { + placement = cand; + break; + } + } + if (!placement) { + placement = gr_find_image_and_placement(image_id, placement_id); + if (placement) { + kv_push(ImagePlacement *, + del_data->placements_to_delete, placement); + } + } + + // Restore the text underneath the placement if possible. + if (placement && placement->text_underneath) { + int row = tgetimgrow(gp) - 1; + int col = tgetimgcol(gp) - 1; + if (col >= 0 && row >= 0 && row < placement->rows && + col < placement->cols) { + *gp = placement->text_underneath[row * placement->cols + + col]; + return 1; + } + } + + // Otherwise just erase the cell. + gp->mode = 0; + gp->u = ' '; + return 1; +} + +/// Handles the delete command. +static void gr_handle_delete_command(GraphicsCommand *cmd) { + DeletionData del_data = {0}; + char delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0; + char d = tolower(cmd->delete_specifier); + + if (d == 'n') { + d = 'i'; + Image *img = gr_find_image_by_number(cmd->image_number); + if (!img) + return; + del_data.image_id = img->image_id; + } + + kv_init(del_data.placements_to_delete); + + if (!d || d == 'a') { + // Delete all visible placements. + gr_for_each_image_cell(gr_deletion_callback, &del_data); + } else if (d == 'i') { + // Delete the specified image by image id and maybe placement + // id. + if (!del_data.image_id) + del_data.image_id = cmd->image_id; + if (!del_data.image_id) { + fprintf(stderr, + "ERROR: image id is not specified in the " + "delete command\n"); + kv_destroy(del_data.placements_to_delete); + return; + } + del_data.placement_id = cmd->placement_id; + gr_for_each_image_cell(gr_deletion_callback, &del_data); + } else { + fprintf(stderr, + "WARNING: unsupported value of the d key: '%c'. The " + "command is ignored.\n", + cmd->delete_specifier); + } + + // Delete the placements we have collected and maybe images too. + for (int i = 0; i < kv_size(del_data.placements_to_delete); ++i) { + ImagePlacement *placement = + kv_A(del_data.placements_to_delete, i); + // Delete the text underneath the placement and set it to NULL + // to avoid erasing it from the screen again. + free(placement->text_underneath); + placement->text_underneath = NULL; + Image *img = placement->image; + gr_delete_placement(placement); + // Delete the image if image deletion is requested (uppercase + // delete specifier) and there are no more placements. + if (delete_image_if_no_ref && kh_size(img->placements) == 0) + gr_delete_image(img); + } + + // NOTE: It's not very clear whether we should delete the image + // even if there are no _visible_ placements to delete. We do + // this because otherwise there is no way to delete an image + // with virtual placements in one command. + if (d == 'i' && !del_data.placement_id && delete_image_if_no_ref) + gr_delete_image(gr_find_image(cmd->image_id)); + + kv_destroy(del_data.placements_to_delete); +} + +/// Clears the cells occupied by the placement. This is normally done when +/// implicitly deleting a classic placement. +static void gr_erase_placement(ImagePlacement *placement) { + DeletionData del_data = {0}; + del_data.image_id = placement->image->image_id; + del_data.placement_id = placement->placement_id; + kv_init(del_data.placements_to_delete); + gr_for_each_image_cell(gr_deletion_callback, &del_data); + // Delete the text underneath the placement and set it to NULL + // to avoid erasing it from the screen again. + free(placement->text_underneath); + placement->text_underneath = NULL; + kv_destroy(del_data.placements_to_delete); +} + +static void gr_handle_animation_control_command(GraphicsCommand *cmd) { + if (cmd->image_id == 0 && cmd->image_number == 0) { + gr_reporterror_cmd(cmd, + "EINVAL: neither image id nor image number " + "are specified or both are zero"); + return; + } + + // Find the image with the id or number. + Image *img = gr_find_image_for_command(cmd); + if (img) { + cmd->image_id = img->image_id; + } else { + gr_reporterror_cmd(cmd, "ENOENT: image not found"); + return; + } + + // Find the frame to edit, if requested. + ImageFrame *frame = NULL; + if (cmd->edit_frame) + frame = gr_get_frame(img, cmd->edit_frame); + if (cmd->edit_frame || cmd->gap) { + if (!frame) { + gr_reporterror_cmd(cmd, "ENOENT: frame %d not found", + cmd->edit_frame); + return; + } + if (cmd->gap) { + img->total_duration -= frame->gap; + frame->gap = cmd->gap; + img->total_duration += frame->gap; + } + } + + // Set animation-related parameters of the image. + if (cmd->current_frame) + img->current_frame = cmd->current_frame; + if (cmd->animation_state) { + if (cmd->animation_state == 1) { + img->animation_state = ANIMATION_STATE_STOPPED; + } else if (cmd->animation_state == 2) { + img->animation_state = ANIMATION_STATE_LOADING; + } else if (cmd->animation_state == 3) { + img->animation_state = ANIMATION_STATE_LOOPING; + } else { + gr_reporterror_cmd( + cmd, "EINVAL: invalid animation state: %d", + cmd->animation_state); + } + } + // TODO: Set the number of loops to cmd->loops + + // Make sure we redraw all instances of the image. + gr_schedule_image_redraw(img); +} + +/// Handles a command. +static void gr_handle_command(GraphicsCommand *cmd) { + if (!cmd->image_id && !cmd->image_number) { + // If there is no image id or image number, nobody expects a + // response, so set quiet to 2. + cmd->quiet = 2; + } + + int was_transmission = 0; + ImageFrame *frame = NULL; + + switch (cmd->action) { + case 0: + // If no action is specified, it is data transmission. + case 't': + case 'q': + case 'f': + was_transmission = 1; + // Transmit data. 'q' means query, which is basically the same + // as transmit, but the image is discarded, and the id is fake. + // 'f' appends a frame to an existing image. + gr_handle_transmit_command(cmd); + break; + case 'p': + // Display (put) the image. + gr_handle_put_command(cmd); + break; + case 'T': + was_transmission = 1; + // Transmit and display. + frame = gr_handle_transmit_command(cmd); + if (frame && !cmd->is_direct_transmission_continuation) { + gr_handle_put_command(cmd); + if (cmd->placement_id) + frame->image->initial_placement_id = + cmd->placement_id; + } + break; + case 'd': + gr_handle_delete_command(cmd); + break; + case 'a': + gr_handle_animation_control_command(cmd); + break; + default: + gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c", + cmd->action); + break; + } + + if (!was_transmission || + (cmd->transmission_medium && cmd->transmission_medium != 'd')) { + // If it wasn't a transmission command, or if the transmission + // wasn't direct, clear the current upload frame and close the + // file. (If it was a direct transmission, the current upload + // was handled inside `gr_append_data`.) + gr_set_current_upload_frame(NULL); + } +} + +/// A partially parsed key-value pair. +typedef struct KeyAndValue { + char *key_start; + char *val_start; + unsigned key_len, val_len; +} KeyAndValue; + +/// Parses the value of a key and assigns it to the appropriate field of `cmd`. +static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) { + char *key_start = kv->key_start; + char *key_end = key_start + kv->key_len; + char *value_start = kv->val_start; + char *value_end = value_start + kv->val_len; + // Currently all keys are one-character. + if (key_end - key_start != 1) { + gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s", + key_end - key_start, key_start); + return; + } + long num = 0; + if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' || + *key_start == 'o') { + // Some keys have one-character values. + if (value_end - value_start != 1) { + gr_reporterror_cmd( + cmd, + "EINVAL: value of 'a', 't' or 'd' must be a " + "single char: %s", + key_start); + return; + } + } else { + // All the other keys have integer values. + char *num_end = NULL; + num = strtol(value_start, &num_end, 10); + if (num_end != value_end) { + gr_reporterror_cmd( + cmd, "EINVAL: could not parse number value: %s", + key_start); + return; + } + } + switch (*key_start) { + case 'a': + cmd->action = *value_start; + break; + case 't': + cmd->transmission_medium = *value_start; + break; + case 'd': + cmd->delete_specifier = *value_start; + break; + case 'q': + cmd->quiet = num; + break; + case 'f': + cmd->format = num; + if (num != 0 && num != 24 && num != 32 && num != 100) { + gr_reporterror_cmd( + cmd, + "EINVAL: unsupported format specification: %s", + key_start); + } + break; + case 'o': + cmd->compression = *value_start; + if (cmd->compression != 'z') { + gr_reporterror_cmd(cmd, + "EINVAL: unsupported compression " + "specification: %s", + key_start); + } + break; + case 's': + if (cmd->action == 'a') + cmd->animation_state = num; + else + cmd->frame_pix_width = num; + break; + case 'v': + if (cmd->action == 'a') + cmd->loops = num; + else + cmd->frame_pix_height = num; + break; + case 'i': + cmd->image_id = num; + break; + case 'I': + cmd->image_number = num; + break; + case 'p': + cmd->placement_id = num; + break; + case 'x': + cmd->src_pix_x = num; + cmd->frame_dst_pix_x = num; + break; + case 'y': + if (cmd->action == 'f') + cmd->frame_dst_pix_y = num; + else + cmd->src_pix_y = num; + break; + case 'w': + cmd->src_pix_width = num; + break; + case 'h': + cmd->src_pix_height = num; + break; + case 'c': + if (cmd->action == 'f') + cmd->background_frame = num; + else if (cmd->action == 'a') + cmd->current_frame = num; + else + cmd->columns = num; + break; + case 'r': + if (cmd->action == 'f' || cmd->action == 'a') + cmd->edit_frame = num; + else + cmd->rows = num; + break; + case 'm': + cmd->more = num; + break; + case 'S': + cmd->size = num; + break; + case 'O': + cmd->offset = num; + break; + case 'U': + cmd->virtual = num; + break; + case 'X': + if (cmd->action == 'f') + cmd->replace_instead_of_blending = num; + else + break; /*ignore*/ + break; + case 'Y': + if (cmd->action == 'f') + cmd->background_color = num; + else + break; /*ignore*/ + break; + case 'z': + if (cmd->action == 'f' || cmd->action == 'a') + cmd->gap = num; + else + break; /*ignore*/ + break; + case 'C': + cmd->do_not_move_cursor = num; + break; + default: + gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s", + key_start); + return; + } +} + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters. Returns 1 on success. +int gr_parse_command(char *buf, size_t len) { + if (buf[0] != 'G') + return 0; + + Milliseconds command_start_time = gr_now_ms(); + debug_loaded_files_counter = 0; + debug_loaded_pixmaps_counter = 0; + global_command_counter++; + GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); + + memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); + + // Eat the 'G'. + ++buf; + --len; + + GraphicsCommand cmd = {.command = buf}; + // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to + // parse the payload. + char state = 'k'; + // An array of partially parsed key-value pairs. + KeyAndValue key_vals[32]; + unsigned key_vals_count = 0; + char *key_start = buf; + char *key_end = NULL; + char *val_start = NULL; + char *val_end = NULL; + char *c = buf; + while (c - buf < len + 1) { + if (state == 'k') { + switch (*c) { + case ',': + case ';': + case '\0': + state = *c == ',' ? 'k' : 'p'; + key_end = c; + gr_reporterror_cmd( + &cmd, "EINVAL: key without value: %s ", + key_start); + break; + case '=': + key_end = c; + state = 'v'; + val_start = c + 1; + break; + default: + break; + } + } else if (state == 'v') { + switch (*c) { + case ',': + case ';': + case '\0': + state = *c == ',' ? 'k' : 'p'; + val_end = c; + if (key_vals_count >= + sizeof(key_vals) / sizeof(*key_vals)) { + gr_reporterror_cmd(&cmd, + "EINVAL: too many " + "key-value pairs"); + break; + } + key_vals[key_vals_count].key_start = key_start; + key_vals[key_vals_count].val_start = val_start; + key_vals[key_vals_count].key_len = + key_end - key_start; + key_vals[key_vals_count].val_len = + val_end - val_start; + ++key_vals_count; + key_start = c + 1; + break; + default: + break; + } + } else if (state == 'p') { + cmd.payload = c; + // break out of the loop, we don't check the payload + break; + } + ++c; + } + + // Set the action key ('a=') first because we need it to disambiguate + // some keys. Also set 'i=' and 'I=' for better error reporting. + for (unsigned i = 0; i < key_vals_count; ++i) { + if (key_vals[i].key_len == 1) { + char *start = key_vals[i].key_start; + if (*start == 'a' || *start == 'i' || *start == 'I') { + gr_set_keyvalue(&cmd, &key_vals[i]); + break; + } + } + } + // Set the rest of the keys. + for (unsigned i = 0; i < key_vals_count; ++i) + gr_set_keyvalue(&cmd, &key_vals[i]); + + if (!cmd.payload) + cmd.payload = buf + len; + + if (cmd.payload && cmd.payload[0]) + GR_LOG(" payload size: %ld\n", strlen(cmd.payload)); + + if (!graphics_command_result.error) + gr_handle_command(&cmd); + + if (graphics_debug_mode) { + fprintf(stderr, "Response: "); + for (const char *resp = graphics_command_result.response; + *resp != '\0'; ++resp) { + if (isprint(*resp)) + fprintf(stderr, "%c", *resp); + else + fprintf(stderr, "(0x%x)", *resp); + } + fprintf(stderr, "\n"); + } + + // Make sure that we suppress response if needed. Usually cmd.quiet is + // taken into account when creating the response, but it's not very + // reliable in the current implementation. + if (cmd.quiet) { + if (!graphics_command_result.error || cmd.quiet >= 2) + graphics_command_result.response[0] = '\0'; + } + + Milliseconds command_end_time = gr_now_ms(); + GR_LOG("Command %lu took %ld ms (loaded %d files, %d pixmaps)\n\n", + global_command_counter, command_end_time - command_start_time, + debug_loaded_files_counter, debug_loaded_pixmaps_counter); + + return 1; +} + +//////////////////////////////////////////////////////////////////////////////// +// base64 decoding part is basically copied from st.c +//////////////////////////////////////////////////////////////////////////////// + +static const char gr_base64_digits[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +static char gr_base64_getc(const char **src) { + while (**src && !isprint(**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char *gr_base64dec(const char *src, size_t *size) { + size_t in_len = strlen(src); + char *result, *dst; + + result = dst = malloc((in_len + 3) / 4 * 3 + 1); + while (*src) { + int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; + + if (a == -1 || b == -1) + break; + + *dst++ = (a << 2) | ((b & 0x30) >> 4); + if (c == -1) + break; + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + if (d == -1) + break; + *dst++ = ((c & 0x03) << 6) | d; + } + *dst = '\0'; + if (size) { + *size = dst - result; + } + return result; +} diff --git a/suckless/st/graphics.h b/suckless/st/graphics.h new file mode 100644 index 0000000..f9a649a --- /dev/null +++ b/suckless/st/graphics.h @@ -0,0 +1,112 @@ + +#include +#include +#include + +/// Initialize the graphics module. +void gr_init(Display *disp, Visual *vis, Colormap cm); +/// Deinitialize the graphics module. +void gr_deinit(); + +/// Add an image rectangle to a list if rectangles to draw. This function may +/// actually draw some rectangles, or it may wait till more rectangles are +/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. +/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the +/// part of the image to draw (row/col indices are zero-based, ends are +/// excluded). +/// - `x_col` and `y_row` are the coordinates of the top-left corner of the +/// image in the terminal grid. +/// - `x_pix` and `y_pix` are the same but in pixels. +/// - `reverse` indicates whether colors should be inverted. +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, + int img_start_col, int img_end_col, int img_start_row, + int img_end_row, int x_col, int y_row, int x_pix, + int y_pix, int cw, int ch, int reverse); +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. +void gr_start_drawing(Drawable buf, int cw, int ch); +/// Finish image drawing. This functions will draw all the rectangles left to +/// draw. +void gr_finish_drawing(Drawable buf); +/// Mark rows containing animations as dirty if it's time to redraw them. Must +/// be called right after `gr_start_drawing`. +void gr_mark_dirty_animations(int *dirty, int rows); + +/// Parse and execute a graphics command. `buf` must start with 'G' and contain +/// at least `len + 1` characters (including '\0'). Returns 1 on success. +/// Additional informations is returned through `graphics_command_result`. +int gr_parse_command(char *buf, size_t len); + +/// Executes `command` with the name of the file corresponding to `image_id` as +/// the argument. Executes xmessage with an error message on failure. +void gr_preview_image(uint32_t image_id, const char *command); + +/// Executes ` -e less ` where is the name of a temporary file +/// containing the information about an image and placement, and is +/// specified with `st_executable`. +void gr_show_image_info(uint32_t image_id, uint32_t placement_id, + uint32_t imgcol, uint32_t imgrow, + char is_classic_placeholder, int32_t diacritic_count, + char *st_executable); + +/// Dumps the internal state (images and placements) to stderr. +void gr_dump_state(); + +/// Unloads images to reduce RAM usage. +void gr_unload_images_to_reduce_ram(); + +/// Executes `callback` for each image cell. The callback should return 1 if it +/// changed the glyph. This function is implemented in `st.c`. +void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp), + void *data); + +/// Marks all the rows containing the image with `image_id` as dirty. +void gr_schedule_image_redraw_by_id(uint32_t image_id); + +/// Returns a pointer to the glyph under the classic placement with `image_id` +/// and `placement_id` at `col` and `row` (1-based). May return NULL if the +/// underneath text is unknown. +Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, + int col, int row); + +typedef enum { + GRAPHICS_DEBUG_NONE = 0, + GRAPHICS_DEBUG_LOG = 1, + GRAPHICS_DEBUG_LOG_AND_BOXES = 2, +} GraphicsDebugMode; + +/// Print additional information, draw bounding bounding boxes, etc. +extern GraphicsDebugMode graphics_debug_mode; + +/// Whether to display images or just draw bounding boxes. +extern char graphics_display_images; + +/// The time in milliseconds until the next redraw to update animations. +/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`. +extern int graphics_next_redraw_delay; + +#define MAX_GRAPHICS_RESPONSE_LEN 256 + +/// A structure representing the result of a graphics command. +typedef struct { + /// Indicates if the terminal needs to be redrawn. + char redraw; + /// The response of the command that should be sent back to the client + /// (may be empty if the quiet flag is set). + char response[MAX_GRAPHICS_RESPONSE_LEN]; + /// Whether there was an error executing this command (not very useful, + /// the response must be sent back anyway). + char error; + /// Whether the terminal has to create a placeholder for a non-virtual + /// placement. + char create_placeholder; + /// The placeholder that needs to be created. + struct { + uint32_t rows, columns; + uint32_t image_id, placement_id; + char do_not_move_cursor; + Glyph *text_underneath; + } placeholder; +} GraphicsCommandResult; + +/// The result of a graphics command. +extern GraphicsCommandResult graphics_command_result; diff --git a/st/hb.c b/suckless/st/hb.c similarity index 100% rename from st/hb.c rename to suckless/st/hb.c diff --git a/st/hb.h b/suckless/st/hb.h similarity index 100% rename from st/hb.h rename to suckless/st/hb.h diff --git a/suckless/st/icat-mini.sh b/suckless/st/icat-mini.sh new file mode 100755 index 0000000..8f240e9 --- /dev/null +++ b/suckless/st/icat-mini.sh @@ -0,0 +1,875 @@ +#!/bin/sh + +# vim: shiftwidth=4 + +script_name="$(basename "$0")" + +short_help="Usage: $script_name [OPTIONS] + +This is a script to display images in the terminal using the kitty graphics +protocol with Unicode placeholders. It is very basic, please use something else +if you have alternatives. + +Options: + -h Show this help. + -s SCALE The scale of the image, may be floating point. + -c N, --cols N The number of columns. + -r N, --rows N The number of rows. + --max-cols N The maximum number of columns. + --max-rows N The maximum number of rows. + --cell-size WxH The cell size in pixels. + -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. + --speed SPEED The multiplier for the animation speed (float). +" + +# Exit the script on keyboard interrupt +trap "echo 'icat-mini was interrupted' >&2; exit 1" INT + +cols="" +rows="" +file="" +command_tty="" +response_tty="" +uploading_method="auto" +cell_size="" +scale=1 +max_cols="" +max_rows="" +speed="" + +# Parse the command line. +while [ $# -gt 0 ]; do + case "$1" in + -c|--columns|--cols) + cols="$2" + shift 2 + ;; + -r|--rows|-l|--lines) + rows="$2" + shift 2 + ;; + -s|--scale) + scale="$2" + shift 2 + ;; + -h|--help) + echo "$short_help" + exit 0 + ;; + -m|--upload-method|--uploading-method) + uploading_method="$2" + shift 2 + ;; + --cell-size) + cell_size="$2" + shift 2 + ;; + --max-cols) + max_cols="$2" + shift 2 + ;; + --max-rows) + max_rows="$2" + shift 2 + ;; + --speed) + speed="$2" + shift 2 + ;; + --) + file="$2" + shift 2 + ;; + -*) + echo "Unknown option: $1" >&2 + exit 1 + ;; + *) + if [ -n "$file" ]; then + echo "Multiple image files are not supported: $file and $1" >&2 + exit 1 + fi + file="$1" + shift + ;; + esac +done + +file="$(realpath "$file")" + +##################################################################### +# Detect imagemagick +##################################################################### + +# If there is the 'magick' command, use it instead of separate 'convert' and +# 'identify' commands. +if command -v magick > /dev/null; then + identify="magick identify" + convert="magick" +else + identify="identify" + convert="convert" +fi + +##################################################################### +# Detect tmux +##################################################################### + +# Check if we are inside tmux. +inside_tmux="" +if [ -n "$TMUX" ]; then + inside_tmux=1 +fi + +if [ -z "$command_tty" ] && [ -n "$inside_tmux" ]; then + # Get the pty of the current tmux pane. + command_tty="$(tmux display-message -t "$TMUX_PANE" -p "#{pane_tty}")" + if [ ! -e "$command_tty" ]; then + command_tty="" + fi +fi + +##################################################################### +# Adjust the terminal state +##################################################################### + +if [ -z "$command_tty" ]; then + command_tty="/dev/tty" +fi +if [ -z "$response_tty" ]; then + response_tty="/dev/tty" +fi + +stty_orig="$(stty -g < "$response_tty")" +stty -echo < "$response_tty" +# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some +# horrible issues otherwise. +stty susp undef < "$response_tty" +stty -icanon < "$response_tty" + +restore_echo() { + [ -n "$stty_orig" ] || return + stty $stty_orig < "$response_tty" +} + +trap restore_echo EXIT TERM + +##################################################################### +# Compute the number of rows and columns +##################################################################### + +is_pos_int() { + if [ -z "$1" ]; then + return 1 # false + fi + if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then + if [ "$1" -gt 0 ]; then + return 0 # true + fi + fi + return 1 # false +} + +if [ -n "$cols" ] || [ -n "$rows" ]; then + if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then + echo "You can't specify both max-cols/rows and cols/rows" >&2 + exit 1 + fi +fi + +# Get the max number of cols and rows. +[ -n "$max_cols" ] || max_cols="$(tput cols)" +[ -n "$max_rows" ] || max_rows="$(tput lines)" +if [ "$max_rows" -gt 255 ]; then + max_rows=255 +fi + +python_ioctl_command="import array, fcntl, termios +buf = array.array('H', [0, 0, 0, 0]) +fcntl.ioctl(0, termios.TIOCGWINSZ, buf) +print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" + +# Get the cell size in pixels if either cols or rows are not specified. +if [ -z "$cols" ] || [ -z "$rows" ]; then + cell_width="" + cell_height="" + # If the cell size is specified, use it. + if [ -n "$cell_size" ]; then + cell_width="${cell_size%x*}" + cell_height="${cell_size#*x}" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + echo "Invalid cell size: $cell_size" >&2 + exit 1 + fi + fi + # Otherwise try to use TIOCGWINSZ ioctl via python. + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then + cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$command_tty" 2> /dev/null)" + cell_width="${cell_size_ioctl% *}" + cell_height="${cell_size_ioctl#* }" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + cell_width="" + cell_height="" + fi + fi + # If it didn't work, try to use csi XTWINOPS. + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then + if [ -n "$inside_tmux" ]; then + printf '\ePtmux;\e\e[16t\e\\' >> "$command_tty" + else + printf '\e[16t' >> "$command_tty" + fi + # The expected response will look like ^[[6;;t + term_response="" + while true; do + char=$(dd bs=1 count=1 <"$response_tty" 2>/dev/null) + if [ "$char" = "t" ]; then + break + fi + term_response="$term_response$char" + done + cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" + cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then + cell_width=8 + cell_height=16 + fi + fi +fi + +# Compute a formula with bc and round to the nearest integer. +bc_round() { + LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" +} + +# Compute the number of rows and columns of the image. +if [ -z "$cols" ] || [ -z "$rows" ]; then + # Get the size of the image and its resolution. If it's an animation, use + # the first frame. + format_output="$($identify -format '%w %h\n' "$file" | head -1)" + img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" + img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" + if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then + echo "Couldn't get image size from identify: $format_output" >&2 + echo >&2 + exit 1 + fi + opt_cols_expr="(${scale}*${img_width}/${cell_width})" + opt_rows_expr="(${scale}*${img_height}/${cell_height})" + if [ -z "$cols" ] && [ -z "$rows" ]; then + # If columns and rows are not specified, compute the optimal values + # using the information about rows and columns per inch. + cols="$(bc_round "$opt_cols_expr")" + rows="$(bc_round "$opt_rows_expr")" + # Make sure that automatically computed rows and columns are within some + # sane limits + if [ "$cols" -gt "$max_cols" ]; then + rows="$(bc_round "$rows * $max_cols / $cols")" + cols="$max_cols" + fi + if [ "$rows" -gt "$max_rows" ]; then + cols="$(bc_round "$cols * $max_rows / $rows")" + rows="$max_rows" + fi + elif [ -z "$cols" ]; then + # If only one dimension is specified, compute the other one to match the + # aspect ratio as close as possible. + cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" + elif [ -z "$rows" ]; then + rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" + fi + + if [ "$cols" -lt 1 ]; then + cols=1 + fi + if [ "$rows" -lt 1 ]; then + rows=1 + fi +fi + +##################################################################### +# Generate an image id +##################################################################### + +image_id="" +while [ -z "$image_id" ]; do + image_id="$(shuf -i 16777217-4294967295 -n 1)" + # Check that the id requires 24-bit fg colors. + if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then + image_id="" + fi +done + +##################################################################### +# Uploading the image +##################################################################### + +# Choose the uploading method +if [ "$uploading_method" = "auto" ]; then + if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then + uploading_method="direct" + else + uploading_method="file" + fi +fi + +# Functions to emit the start and the end of a graphics command. +if [ -n "$inside_tmux" ]; then + # If we are in tmux we have to wrap the command in Ptmux. + graphics_command_start='\ePtmux;\e\e_G' + graphics_command_end='\e\e\\\e\\' +else + graphics_command_start='\e_G' + graphics_command_end='\e\\' +fi + +# Send a graphics command with the correct start and end +gr_command() { + printf "${graphics_command_start}%s${graphics_command_end}" "$1" >> "$command_tty" +} + +# Compute the size of a data chunk for direct transmission. +if [ "$uploading_method" = "direct" ]; then + # Get the value of PIPE_BUF. + pipe_buf="$(getconf PIPE_BUF "$command_tty" 2> /dev/null)" + if is_pos_int "$pipe_buf"; then + # Make sure it's between 512 and 4096. + if [ "$(expr "$pipe_buf" \< 512)" -eq 1 ]; then + pipe_buf=512 + elif [ "$(expr "$pipe_buf" \> 4096)" -eq 1 ]; then + pipe_buf=4096 + fi + else + pipe_buf=512 + fi + + # The size of each graphics command shouldn't be more than PIPE_BUF, so we + # set the size of an encoded chunk to be PIPE_BUF - 128 to leave some space + # for the command. + chunk_size="$(expr "$pipe_buf" - 128)" +fi + +# Check if the image format is supported. +is_format_supported() { + arg_format="$1" + if [ "$arg_format" = "PNG" ]; then + return 0 + elif [ "$arg_format" = "JPEG" ]; then + if [ -z "$inside_tmux" ]; then + actual_term="$TERM" + else + # Get the actual current terminal name from tmux. + actual_term="$(tmux display-message -p "#{client_termname}")" + fi + # st is known to support JPEG. + case "$actual_term" in + st | *-st | st-* | *-st-*) + return 0 + ;; + esac + return 1 + else + return 1 + fi +} + +# Send an uploading command. Usage: gr_upload +# Where is a part of command that specifies the action, it will be +# repeated for every chunk (if the method is direct), and is the rest +# of the command that specifies the image parameters. and +# must not include the transmission method or ';'. +# Example: +# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" +gr_upload() { + arg_action="$1" + arg_command="$2" + arg_file="$3" + if [ "$uploading_method" = "file" ]; then + # base64-encode the filename + encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) + # If the file name contains 'tty-graphics-protocol', assume it's + # temporary and use t=t. + medium="t=f" + case "$arg_file" in + *tty-graphics-protocol*) + medium="t=t" + ;; + *) + medium="t=f" + ;; + esac + gr_command "${arg_action},${arg_command},${medium};${encoded_filename}" + fi + if [ "$uploading_method" = "direct" ]; then + # Create a temporary directory to store the chunked image. + chunkdir="$(mktemp -d)" + if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then + echo "Can't create a temp dir" >&2 + exit 1 + fi + # base64-encode the file and split it into chunks. The size of each + # graphics command shouldn't be more than 4096, so we set the size of an + # encoded chunk to be 3968, slightly less than that. + chunk_size=3968 + cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" + + # Issue a command indicating that we want to start data transmission for + # a new image. + gr_command "${arg_action},${arg_command},t=d,m=1" + + # Transmit chunks. + for chunk in "$chunkdir/chunk_"*; do + gr_command "${arg_action},i=${image_id},m=1;$(cat "$chunk")" + rm "$chunk" + done + + # Tell the terminal that we are done. + gr_command "${arg_action},i=$image_id,m=0" + + # Remove the temporary directory. + rmdir "$chunkdir" + fi +} + +delayed_frame_dir_cleanup() { + arg_frame_dir="$1" + sleep 2 + if [ -n "$arg_frame_dir" ]; then + for frame in "$arg_frame_dir"/frame_*.png; do + rm "$frame" + done + rmdir "$arg_frame_dir" + fi +} + +upload_image_and_print_placeholder() { + # Check if the file is an animation. + format_output=$($identify -format '%n %m\n' "$file" | head -n 1) + frame_count="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" + image_format="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" + + if [ "$frame_count" -gt 1 ]; then + # The file is an animation, decompose into frames and upload each frame. + frame_dir="$(mktemp -d)" + frame_dir="$HOME/temp/frames${frame_dir}" + mkdir -p "$frame_dir" + if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then + echo "Can't create a temp dir for frames" >&2 + exit 1 + fi + + # Decompose the animation into separate frames. + $convert "$file" -coalesce "$frame_dir/frame_%06d.png" + + # Get all frame delays at once, in centiseconds, as a space-separated + # string. + delays=$($identify -format "%T " "$file") + + frame_number=1 + for frame in "$frame_dir"/frame_*.png; do + # Read the delay for the current frame and convert it from + # centiseconds to milliseconds. + delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") + delay=$((delay * 10)) + # If the delay is 0, set it to 100ms. + if [ "$delay" -eq 0 ]; then + delay=100 + fi + + if [ -n "$speed" ]; then + delay=$(bc_round "$delay / $speed") + fi + + if [ "$frame_number" -eq 1 ]; then + # Abort the previous transmission, just in case. + gr_command "q=2,a=t,i=${image_id},m=0" + # Upload the first frame with a=T + gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" + # Set the delay for the first frame and also play the animation + # in loading mode (s=2). + gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" + # Print the placeholder after the first frame to reduce the wait + # time. + print_placeholder + else + # Upload subsequent frames with a=f + gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" + fi + + frame_number=$((frame_number + 1)) + done + + # Play the animation in loop mode (s=3). + gr_command "a=a,v=1,s=3,i=${image_id}" + + # Remove the temporary directory, but do it in the background with a + # delay to avoid removing files before they are loaded by the terminal. + delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & + elif is_format_supported "$image_format"; then + # The file is not an animation and has a supported format, upload it + # directly. + gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" + # Print the placeholder + print_placeholder + else + # The format is not supported, try to convert it to png. + temp_file="$(mktemp --tmpdir "icat-mini-tty-graphics-protocol-XXXXX.png")" + if ! $convert "$file" "$temp_file"; then + echo "Failed to convert the image to PNG" >&2 + exit 1 + fi + # Upload the converted image. + gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$temp_file" + # Print the placeholder + print_placeholder + fi +} + +##################################################################### +# Printing the image placeholder +##################################################################### + +print_placeholder() { + # Each line starts with the escape sequence to set the foreground color to + # the image id. + blue="$(expr "$image_id" % 256 )" + green="$(expr \( "$image_id" / 256 \) % 256 )" + red="$(expr \( "$image_id" / 65536 \) % 256 )" + line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" + line_end="$(printf "\e[39;m")" + + id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" + eval "id_diacritic=\$d${id4th}" + + # Reset the brush state, mostly to reset the underline color. + printf "\e[0m" + + # Fill the output with characters representing the image + for y in $(seq 0 "$(expr "$rows" - 1)"); do + eval "row_diacritic=\$d${y}" + line="$line_start" + for x in $(seq 0 "$(expr "$cols" - 1)"); do + eval "col_diacritic=\$d${x}" + # Note that when $x is out of bounds, the column diacritic will + # be empty, meaning that the column should be guessed by the + # terminal. + if [ "$x" -ge "$num_diacritics" ]; then + line="${line}${placeholder}${row_diacritic}" + else + line="${line}${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" + fi + done + line="${line}${line_end}" + printf "%s\n" "$line" + done + + printf "\e[0m" +} + +d0="̅" +d1="̍" +d2="̎" +d3="̐" +d4="̒" +d5="̽" +d6="̾" +d7="̿" +d8="͆" +d9="͊" +d10="͋" +d11="͌" +d12="͐" +d13="͑" +d14="͒" +d15="͗" +d16="͛" +d17="ͣ" +d18="ͤ" +d19="ͥ" +d20="ͦ" +d21="ͧ" +d22="ͨ" +d23="ͩ" +d24="ͪ" +d25="ͫ" +d26="ͬ" +d27="ͭ" +d28="ͮ" +d29="ͯ" +d30="҃" +d31="҄" +d32="҅" +d33="҆" +d34="҇" +d35="֒" +d36="֓" +d37="֔" +d38="֕" +d39="֗" +d40="֘" +d41="֙" +d42="֜" +d43="֝" +d44="֞" +d45="֟" +d46="֠" +d47="֡" +d48="֨" +d49="֩" +d50="֫" +d51="֬" +d52="֯" +d53="ׄ" +d54="ؐ" +d55="ؑ" +d56="ؒ" +d57="ؓ" +d58="ؔ" +d59="ؕ" +d60="ؖ" +d61="ؗ" +d62="ٗ" +d63="٘" +d64="ٙ" +d65="ٚ" +d66="ٛ" +d67="ٝ" +d68="ٞ" +d69="ۖ" +d70="ۗ" +d71="ۘ" +d72="ۙ" +d73="ۚ" +d74="ۛ" +d75="ۜ" +d76="۟" +d77="۠" +d78="ۡ" +d79="ۢ" +d80="ۤ" +d81="ۧ" +d82="ۨ" +d83="۫" +d84="۬" +d85="ܰ" +d86="ܲ" +d87="ܳ" +d88="ܵ" +d89="ܶ" +d90="ܺ" +d91="ܽ" +d92="ܿ" +d93="݀" +d94="݁" +d95="݃" +d96="݅" +d97="݇" +d98="݉" +d99="݊" +d100="߫" +d101="߬" +d102="߭" +d103="߮" +d104="߯" +d105="߰" +d106="߱" +d107="߳" +d108="ࠖ" +d109="ࠗ" +d110="࠘" +d111="࠙" +d112="ࠛ" +d113="ࠜ" +d114="ࠝ" +d115="ࠞ" +d116="ࠟ" +d117="ࠠ" +d118="ࠡ" +d119="ࠢ" +d120="ࠣ" +d121="ࠥ" +d122="ࠦ" +d123="ࠧ" +d124="ࠩ" +d125="ࠪ" +d126="ࠫ" +d127="ࠬ" +d128="࠭" +d129="॑" +d130="॓" +d131="॔" +d132="ྂ" +d133="ྃ" +d134="྆" +d135="྇" +d136="፝" +d137="፞" +d138="፟" +d139="៝" +d140="᤺" +d141="ᨗ" +d142="᩵" +d143="᩶" +d144="᩷" +d145="᩸" +d146="᩹" +d147="᩺" +d148="᩻" +d149="᩼" +d150="᭫" +d151="᭭" +d152="᭮" +d153="᭯" +d154="᭰" +d155="᭱" +d156="᭲" +d157="᭳" +d158="᳐" +d159="᳑" +d160="᳒" +d161="᳚" +d162="᳛" +d163="᳠" +d164="᷀" +d165="᷁" +d166="᷃" +d167="᷄" +d168="᷅" +d169="᷆" +d170="᷇" +d171="᷈" +d172="᷉" +d173="᷋" +d174="᷌" +d175="᷑" +d176="᷒" +d177="ᷓ" +d178="ᷔ" +d179="ᷕ" +d180="ᷖ" +d181="ᷗ" +d182="ᷘ" +d183="ᷙ" +d184="ᷚ" +d185="ᷛ" +d186="ᷜ" +d187="ᷝ" +d188="ᷞ" +d189="ᷟ" +d190="ᷠ" +d191="ᷡ" +d192="ᷢ" +d193="ᷣ" +d194="ᷤ" +d195="ᷥ" +d196="ᷦ" +d197="᷾" +d198="⃐" +d199="⃑" +d200="⃔" +d201="⃕" +d202="⃖" +d203="⃗" +d204="⃛" +d205="⃜" +d206="⃡" +d207="⃧" +d208="⃩" +d209="⃰" +d210="⳯" +d211="⳰" +d212="⳱" +d213="ⷠ" +d214="ⷡ" +d215="ⷢ" +d216="ⷣ" +d217="ⷤ" +d218="ⷥ" +d219="ⷦ" +d220="ⷧ" +d221="ⷨ" +d222="ⷩ" +d223="ⷪ" +d224="ⷫ" +d225="ⷬ" +d226="ⷭ" +d227="ⷮ" +d228="ⷯ" +d229="ⷰ" +d230="ⷱ" +d231="ⷲ" +d232="ⷳ" +d233="ⷴ" +d234="ⷵ" +d235="ⷶ" +d236="ⷷ" +d237="ⷸ" +d238="ⷹ" +d239="ⷺ" +d240="ⷻ" +d241="ⷼ" +d242="ⷽ" +d243="ⷾ" +d244="ⷿ" +d245="꙯" +d246="꙼" +d247="꙽" +d248="꛰" +d249="꛱" +d250="꣠" +d251="꣡" +d252="꣢" +d253="꣣" +d254="꣤" +d255="꣥" +d256="꣦" +d257="꣧" +d258="꣨" +d259="꣩" +d260="꣪" +d261="꣫" +d262="꣬" +d263="꣭" +d264="꣮" +d265="꣯" +d266="꣰" +d267="꣱" +d268="ꪰ" +d269="ꪲ" +d270="ꪳ" +d271="ꪷ" +d272="ꪸ" +d273="ꪾ" +d274="꪿" +d275="꫁" +d276="︠" +d277="︡" +d278="︢" +d279="︣" +d280="︤" +d281="︥" +d282="︦" +d283="𐨏" +d284="𐨸" +d285="𝆅" +d286="𝆆" +d287="𝆇" +d288="𝆈" +d289="𝆉" +d290="𝆪" +d291="𝆫" +d292="𝆬" +d293="𝆭" +d294="𝉂" +d295="𝉃" +d296="𝉄" + +num_diacritics="297" + +placeholder="􎻮" + +##################################################################### +# Upload the image and print the placeholder +##################################################################### + +upload_image_and_print_placeholder diff --git a/suckless/st/khash.h b/suckless/st/khash.h new file mode 100644 index 0000000..f75f347 --- /dev/null +++ b/suckless/st/khash.h @@ -0,0 +1,627 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include +#include +#include + +/* compiler specific configuration */ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash set containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ diff --git a/suckless/st/kvec.h b/suckless/st/kvec.h new file mode 100644 index 0000000..10f1c5b --- /dev/null +++ b/suckless/st/kvec.h @@ -0,0 +1,90 @@ +/* The MIT License + + Copyright (c) 2008, by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "kvec.h" +int main() { + kvec_t(int) array; + kv_init(array); + kv_push(int, array, 10); // append + kv_a(int, array, 20) = 5; // dynamic + kv_A(array, 20) = 4; // static + kv_destroy(array); + return 0; +} +*/ + +/* + 2008-09-22 (0.1.0): + + * The initial version. + +*/ + +#ifndef AC_KVEC_H +#define AC_KVEC_H + +#include + +#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) + +#define kvec_t(type) struct { size_t n, m; type *a; } +#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) +#define kv_destroy(v) free((v).a) +#define kv_A(v, i) ((v).a[(i)]) +#define kv_pop(v) ((v).a[--(v).n]) +#define kv_size(v) ((v).n) +#define kv_max(v) ((v).m) + +#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) + +#define kv_copy(type, v1, v0) do { \ + if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ + (v1).n = (v0).n; \ + memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ + } while (0) \ + +#define kv_push(type, v, x) do { \ + if ((v).n == (v).m) { \ + (v).m = (v).m? (v).m<<1 : 2; \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ + } \ + (v).a[(v).n++] = (x); \ + } while (0) + +#define kv_pushp(type, v) ((((v).n == (v).m)? \ + ((v).m = ((v).m? (v).m<<1 : 2), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : 0), ((v).a + ((v).n++))) + +#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ + ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ + : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ + : 0), (v).a[(i)]) + +#endif diff --git a/st/patches/st-alpha-20240814-a0274bc.diff b/suckless/st/patches/st-alpha-20240814-a0274bc.diff similarity index 100% rename from st/patches/st-alpha-20240814-a0274bc.diff rename to suckless/st/patches/st-alpha-20240814-a0274bc.diff diff --git a/suckless/st/patches/st-glyph-wide-support-graphics.diff b/suckless/st/patches/st-glyph-wide-support-graphics.diff new file mode 100644 index 0000000..cd46ced --- /dev/null +++ b/suckless/st/patches/st-glyph-wide-support-graphics.diff @@ -0,0 +1,199 @@ +From 8eac1cd750e01d76d56a0b43ae67b12e2185bae4 Mon Sep 17 00:00:00 2001 +From: wael <40663@protonmail.com> +Date: Mon, 11 Apr 2022 17:14:06 +0300 +Subject: [PATCH] [PATCH] glyph wide support + +https://st.suckless.org/patches/glyph_wide_support/ +https://st.suckless.org/patches/glyph_wide_support/st-glyph-wide-support-boxdraw-20220411-ef05519.diff +--- + st.h | 6 ++++ + x.c | 108 +++++++++++++++++++++++++++++++++-------------------------- + 2 files changed, 66 insertions(+), 48 deletions(-) + +diff --git a/st.h b/st.h +index 0525b83..1eb7db7 100644 +--- a/st.h ++++ b/st.h +@@ -43,6 +43,12 @@ enum glyph_attribute { + ATTR_IMAGE = 1 << 14, + }; + ++enum drawing_mode { ++ DRAW_NONE = 0, ++ DRAW_BG = 1 << 0, ++ DRAW_FG = 1 << 1, ++}; ++ + enum selection_mode { + SEL_IDLE = 0, + SEL_EMPTY = 1, +diff --git a/x.c b/x.c +index ec1fa47..f83b98e 100644 +--- a/x.c ++++ b/x.c +@@ -154,7 +154,7 @@ typedef struct { + + 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 xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int); + static void xdrawglyph(Glyph, int, int); + static void xdrawimages(Glyph, Line, int x1, int y1, int x2); + static void xdrawoneimagecell(Glyph, int x, int y); +@@ -1524,7 +1524,7 @@ xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) + } + + void +-xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) ++xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int dmode) + { + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); + int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, +@@ -1616,24 +1616,29 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + fg = bg; + + /* Intelligent cleaning up of the borders. */ +- if (x == 0) { +- xclear(0, (y == 0)? 0 : winy, 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 (dmode & DRAW_BG) { ++ if (x == 0) { ++ xclear(0, (y == 0)? 0 : winy, win.hborderpx, ++ winy + win.ch + ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); ++ } ++ if (winx + width >= win.hborderpx + win.tw) { ++ xclear(winx + width, (y == 0)? 0 : winy, win.w, ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); ++ } ++ if (y == 0) ++ xclear(winx, 0, winx + width, win.vborderpx); ++ if (winy + win.ch >= win.vborderpx + win.th) ++ xclear(winx, winy + win.ch, winx + width, win.h); ++ ++ /* Clean up the region we want to draw to. */ ++ XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + } +- if (y == 0) +- xclear(winx, 0, winx + width, win.vborderpx); +- if (winy + win.ch >= win.vborderpx + win.th) +- xclear(winx, winy + win.ch, winx + width, win.h); + +- /* Clean up the region we want to draw to. */ +- XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); ++ if (!(dmode & DRAW_FG)) ++ return; + +- /* Set the clip region because Xft is sometimes dirty. */ ++ /* Set the clip region only for the underline. */ + r.x = 0; + r.y = 0; + r.height = win.ch; +@@ -1695,6 +1700,9 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + } + } + ++ /* Reset clip to none. */ ++ XftDrawSetClip(xw.draw, 0); ++ + if (base.mode & ATTR_BOXDRAW) { + drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len); + } else { +@@ -1707,9 +1715,6 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, + width, thick); + } +- +- /* Reset clip to none. */ +- XftDrawSetClip(xw.draw, 0); + } + + void +@@ -1719,7 +1724,7 @@ xdrawglyph(Glyph g, int x, int y) + XftGlyphFontSpec spec; + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); +- xdrawglyphfontspecs(&spec, g, numspecs, x, y); ++ xdrawglyphfontspecs(&spec, g, numspecs, x, y, DRAW_BG | DRAW_FG); + if (g.mode & ATTR_IMAGE) { + gr_start_drawing(xw.buf, win.cw, win.ch); + xdrawoneimagecell(g, x, y); +@@ -1997,36 +2002,43 @@ xstartdraw(void) + void + xdrawline(Line line, int x1, int y1, int x2) + { +- int i, x, ox, numspecs; ++ int i, x, ox, numspecs, numspecs_cached; + 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; ++ XftGlyphFontSpec *specs; ++ ++ numspecs_cached = xmakeglyphfontspecs(xw.specbuf, &line[x1], x2 - x1, x1, y1); ++ ++ /* Draw line in 2 passes: background and foreground. This way wide ++ * glyphs won't get truncated (#223) */ ++ for (int dmode = DRAW_BG; dmode <= DRAW_FG; dmode <<= 1) { ++ specs = xw.specbuf; ++ numspecs = numspecs_cached; ++ 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, dmode); ++ if (base.mode & ATTR_IMAGE && dmode == DRAW_FG) ++ xdrawimages(base, line, ox, y1, x); ++ specs += i; ++ numspecs -= i; ++ i = 0; ++ } ++ if (i == 0) { ++ ox = x; ++ base = new; ++ } ++ i++; + } +- i++; ++ if (i > 0) ++ xdrawglyphfontspecs(specs, base, i, ox, y1, dmode); ++ if (i > 0 && base.mode & ATTR_IMAGE && dmode == DRAW_FG) ++ xdrawimages(base, line, ox, y1, x); + } +- if (i > 0) +- xdrawglyphfontspecs(specs, base, i, ox, y1); +- if (i > 0 && base.mode & ATTR_IMAGE) +- xdrawimages(base, line, ox, y1, x); + } + + void +-- +2.55.0 + diff --git a/suckless/st/patches/st-kitty-graphics-20251230-0.9.3.diff b/suckless/st/patches/st-kitty-graphics-20251230-0.9.3.diff new file mode 100644 index 0000000..d53bb30 --- /dev/null +++ b/suckless/st/patches/st-kitty-graphics-20251230-0.9.3.diff @@ -0,0 +1,8033 @@ +From 16663a03b6d7522d171b9717151040f9fad67f9f Mon Sep 17 00:00:00 2001 +From: Sergei Grechanik +Date: Sat, 10 Jan 2026 16:09:05 -0800 +Subject: [PATCH] Kitty graphics protocol support eb8d6cb 2025-12-30 + +This patch implements the kitty graphics protocol in st. +See https://github.com/sergei-grechanik/st-graphics +Created by squashing the graphics branch, the most recent +commit is eb8d6cbc964a864e6391c4288aebe4ebe74bb5c7 (2025-12-30). + +Squashed on top of 6e970474743d57a5d8b054c41fd3bff2bc895742 (0.9.3) + +Note that the following files were excluded from the squash: + .clang-format + README.md + generate-rowcolumn-helpers.py + rowcolumn-diacritics.txt + rowcolumn_diacritics.sh + +Main changes since 2025-02-22: + * Restore evicted images from original files when available. + * Fix an issue that caused too many files to be opened during + concurrent direct uploads. + * Optimize scaled pixmap generation (don't store pad pixels; upscale + with XRender). + +Main changes since 2024-09-22: + * Restore text under classic placements on deletion. + * Support uploading via shared memory (t=s). + * Bug fixes. +--- + Makefile | 7 +- + config.def.h | 46 +- + config.mk | 5 +- + graphics.c | 4393 ++++++++++++++++++++++++++++++++ + graphics.h | 112 + + icat-mini.sh | 875 +++++++ + khash.h | 627 +++++ + kvec.h | 90 + + rowcolumn_diacritics_helpers.c | 391 +++ + st.c | 303 ++- + st.h | 84 +- + st.info | 6 + + win.h | 3 + + x.c | 411 ++- + 14 files changed, 7295 insertions(+), 58 deletions(-) + create mode 100644 graphics.c + create mode 100644 graphics.h + create mode 100755 icat-mini.sh + create mode 100644 khash.h + create mode 100644 kvec.h + create mode 100644 rowcolumn_diacritics_helpers.c + +diff --git a/Makefile b/Makefile +index 15db421..413e7ab 100644 +--- a/Makefile ++++ b/Makefile +@@ -4,7 +4,7 @@ + + include config.mk + +-SRC = st.c x.c ++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c + OBJ = $(SRC:.c=.o) + + all: st +@@ -15,8 +15,9 @@ config.h: + .c.o: + $(CC) $(STCFLAGS) -c $< + +-st.o: config.h st.h win.h +-x.o: arg.h config.h st.h win.h ++st.o: config.h st.h win.h graphics.h ++x.o: arg.h config.h st.h win.h graphics.h ++graphics.c: graphics.h khash.h kvec.h st.h + + $(OBJ): config.h config.mk + +diff --git a/config.def.h b/config.def.h +index 2cd740a..4aadbbc 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -8,6 +8,13 @@ + static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; + static int borderpx = 2; + ++/* How to align the content in the window when the size of the terminal ++ * doesn't perfectly match the size of the window. The values are percentages. ++ * 50 means center, 0 means flush left/top, 100 means flush right/bottom. ++ */ ++static int anysize_halign = 50; ++static int anysize_valign = 50; ++ + /* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e +@@ -23,7 +30,8 @@ char *scroll = NULL; + char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + + /* identification sequence returned in DA and DECID */ +-char *vtiden = "\033[?6c"; ++/* By default, use the same one as kitty. */ ++char *vtiden = "\033[?62c"; + + /* Kerning / character bounding-box multipliers */ + static float cwscale = 1.0; +@@ -163,6 +171,28 @@ static unsigned int mousebg = 0; + */ + static unsigned int defaultattr = 11; + ++/* ++ * Graphics configuration ++ */ ++ ++/// The template for the cache directory. ++const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX"; ++/// The max size of a single image file, in bytes. ++unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024; ++/// The max size of the cache, in bytes. ++unsigned graphics_total_file_cache_size = 300 * 1024 * 1024; ++/// The max ram size of an image or placement, in bytes. ++unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024; ++/// The max total size of all images loaded into RAM. ++unsigned graphics_max_total_ram_size = 300 * 1024 * 1024; ++/// The max total number of image placements and images. ++unsigned graphics_max_total_placements = 4096; ++/// The ratio by which limits can be exceeded. This is to reduce the frequency ++/// of image removal. ++double graphics_excess_tolerance_ratio = 0.05; ++/// The minimum delay between redraws caused by animations, in milliseconds. ++unsigned graphics_animation_min_delay = 20; ++ + /* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other +@@ -170,12 +200,18 @@ static unsigned int defaultattr = 11; + */ + static uint forcemousemod = ShiftMask; + ++/* Internal keyboard shortcuts. */ ++#define MODKEY Mod1Mask ++#define TERMMOD (ControlMask|ShiftMask) ++ + /* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ + static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ ++ { TERMMOD, Button3, previewimage, {.s = "feh"} }, ++ { TERMMOD, Button2, showimageinfo, {}, 1 }, + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, +@@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = { + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, + }; + +-/* Internal keyboard shortcuts. */ +-#define MODKEY Mod1Mask +-#define TERMMOD (ControlMask|ShiftMask) +- + static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, +@@ -201,6 +233,10 @@ static Shortcut shortcuts[] = { + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, ++ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} }, ++ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} }, ++ { TERMMOD, XK_F7, unloadimages, {.i = 0} }, ++ { TERMMOD, XK_F8, toggleimages, {.i = 0} }, + }; + + /* +diff --git a/config.mk b/config.mk +index 2fc854e..7b22243 100644 +--- a/config.mk ++++ b/config.mk +@@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config + + # includes and libs + INCS = -I$(X11INC) \ ++ `$(PKG_CONFIG) --cflags imlib2` \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +-LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ ++LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \ ++ `$(PKG_CONFIG) --libs imlib2` \ ++ `$(PKG_CONFIG) --libs zlib` \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +diff --git a/graphics.c b/graphics.c +new file mode 100644 +index 0000000..801f5c9 +--- /dev/null ++++ b/graphics.c +@@ -0,0 +1,4393 @@ ++/* The MIT License ++ ++ Copyright (c) 2021-2024 Sergei Grechanik ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// This file implements a subset of the kitty graphics protocol. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++// A workaround for mac os to enable mkdtemp. ++#ifdef __APPLE__ ++#define _DARWIN_C_SOURCE ++#endif ++ ++#define _POSIX_C_SOURCE 200809L ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "khash.h" ++#include "kvec.h" ++ ++#include "st.h" ++#include "graphics.h" ++ ++extern char **environ; ++ ++#define MAX_FILENAME_SIZE 256 ++#define MAX_INFO_LEN 256 ++#define MAX_IMAGE_RECTS 20 ++ ++/// The type used in this file to represent time. Used both for time differences ++/// and absolute times (as milliseconds since an arbitrary point in time, see ++/// `initialization_time`). ++typedef int64_t Milliseconds; ++ ++enum ScaleMode { ++ SCALE_MODE_UNSET = 0, ++ /// Stretch or shrink the image to fill the box, ignoring aspect ratio. ++ SCALE_MODE_FILL = 1, ++ /// Preserve aspect ratio and fit to width or to height so that the ++ /// whole image is visible. ++ SCALE_MODE_CONTAIN = 2, ++ /// Do not scale. The image may be cropped if the box is too small. ++ SCALE_MODE_NONE = 3, ++ /// Do not scale, unless the box is too small, in which case the image ++ /// will be shrunk like with `SCALE_MODE_CONTAIN`. ++ SCALE_MODE_NONE_OR_CONTAIN = 4, ++}; ++ ++enum AnimationState { ++ ANIMATION_STATE_UNSET = 0, ++ /// The animation is stopped. Display the current frame, but don't ++ /// advance to the next one. ++ ANIMATION_STATE_STOPPED = 1, ++ /// Run the animation to then end, then wait for the next frame. ++ ANIMATION_STATE_LOADING = 2, ++ /// Run the animation in a loop. ++ ANIMATION_STATE_LOOPING = 3, ++}; ++ ++/// The status of an image. Each image uploaded to the terminal is cached on ++/// disk, then it is loaded to ram when needed. ++enum ImageStatus { ++ STATUS_UNINITIALIZED = 0, ++ STATUS_UPLOADING = 1, ++ STATUS_UPLOADING_ERROR = 2, ++ STATUS_UPLOADING_SUCCESS = 3, ++ STATUS_RAM_LOADING_ERROR = 4, ++ STATUS_RAM_LOADING_SUCCESS = 6, ++}; ++ ++const char *image_status_strings[6] = { ++ "STATUS_UNINITIALIZED", ++ "STATUS_UPLOADING", ++ "STATUS_UPLOADING_ERROR", ++ "STATUS_UPLOADING_SUCCESS", ++ "STATUS_RAM_LOADING_ERROR", ++ "STATUS_RAM_LOADING_SUCCESS", ++}; ++ ++enum ImageUploadingFailure { ++ ERROR_OVER_SIZE_LIMIT = 1, ++ ERROR_CANNOT_OPEN_CACHED_FILE = 2, ++ ERROR_UNEXPECTED_SIZE = 3, ++ ERROR_CANNOT_COPY_FILE = 4, ++ ERROR_CANNOT_OPEN_SHM = 5, ++ ERROR_MTIME_MISMATCH = 3, ++}; ++ ++const char *image_uploading_failure_strings[7] = { ++ "NO_ERROR", ++ "ERROR_OVER_SIZE_LIMIT", ++ "ERROR_CANNOT_OPEN_CACHED_FILE", ++ "ERROR_UNEXPECTED_SIZE", ++ "ERROR_CANNOT_COPY_FILE", ++ "ERROR_CANNOT_OPEN_SHM", ++ "ERROR_MTIME_MISMATCH", ++}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// ++// We use the following structures to represent images and placements: ++// ++// - Image: this is the main structure representing an image, usually created ++// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id, ++// specified by 'i='). An image may have multiple frames (ImageFrame) and ++// placements (ImagePlacement). ++// ++// - ImageFrame: represents a single frame of an image, usually created by ++// the action 'a=f' (and the first frame is created with the image itself). ++// Each frame has an index and also: ++// - a file containing the frame data (considered to be "on disk", although ++// it's probably in tmpfs), ++// - an imlib object containing the fully composed frame (i.e. the frame ++// data from the file composed onto the background frame or color). It is ++// not ready for display yet, because it needs to be scaled and uploaded ++// to the X server. ++// ++// - ImagePlacement: represents a placement of an image, created by 'a=p' and ++// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also ++// each placement has an array of pixmaps: one for each frame of the image. ++// Each pixmap is a scaled and uploaded image ready to be displayed. ++// ++// Images are store in the `images` hash table, mapping image ids to Image ++// objects (allocated on the heap). ++// ++// Placements are stored in the `placements` hash table of each Image object, ++// mapping placement ids to ImagePlacement objects (also allocated on the heap). ++// ++// ImageFrames are stored in the `first_frame` field and in the ++// `frames_beyond_the_first` array of each Image object. They are stored by ++// value, so ImageFrame pointer may be invalidated when frames are ++// added/deleted, be careful. ++// ++//////////////////////////////////////////////////////////////////////////////// ++ ++struct Image; ++struct ImageFrame; ++struct ImagePlacement; ++ ++KHASH_MAP_INIT_INT(id2image, struct Image *) ++KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *) ++ ++/// A transformation to apply to a pixmap before drawing it. ++typedef struct PixmapTransformation { ++ /// The width and height of the pixmap. ++ int pixmap_w, pixmap_h; ++ /// The width and height of the transformed pixmap. ++ int dst_w, dst_h; ++ /// The offset relative to the top-left corner of the box of cells where ++ /// the transformed pixmap is drawn. ++ int dst_x, dst_y; ++} PixmapTransformation; ++ ++typedef struct ImageFrame { ++ /// The image this frame belongs to. ++ struct Image *image; ++ /// The 1-based index of the frame. Zero if the frame isn't initialized. ++ int index; ++ /// The last time when the frame was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The background color of the frame in the 0xRRGGBBAA format. ++ uint32_t background_color; ++ /// The index of the background frame. Zero to use the color instead. ++ int background_frame_index; ++ /// The duration of the frame in milliseconds. ++ int gap; ++ /// The expected size of the frame image file (specified with 'S='), ++ /// used to check if uploading succeeded. ++ unsigned expected_size; ++ /// Format specification (see the `f=` key). ++ int format; ++ /// Pixel width and height of the non-composed (on-disk) frame data. May ++ /// differ from the image (i.e. first frame) dimensions. ++ int data_pix_width, data_pix_height; ++ /// The offset of the frame relative to the first frame. ++ int x, y; ++ /// Compression mode (see the `o=` key). ++ char compression; ++ /// The status (see `ImageStatus`). ++ char status; ++ /// Whether loading into ram is in progress. This is used to avoid ++ /// cyclic dependencies between frames. ++ char ram_loading_in_progress; ++ /// The reason of uploading failure (see `ImageUploadingFailure`). ++ char uploading_failure; ++ /// Whether failures and successes should be reported ('q='). ++ char quiet; ++ /// Whether to blend the frame with the background or replace it. ++ char blend; ++ /// The original file name used with file transmission. Malloced. ++ char *original_filename; ++ /// The modification time of the original file used with file ++ /// transmission. ++ time_t original_file_mtime; ++ /// The file corresponding to the on-disk cache, used when uploading. ++ FILE *open_file; ++ /// The size of the corresponding file cached on disk. ++ unsigned disk_size; ++ /// The imlib object containing the fully composed frame. It's not ++ /// scaled for screen display yet. ++ Imlib_Image imlib_object; ++} ImageFrame; ++ ++typedef struct Image { ++ /// The client id (the one specified with 'i='). Must be nonzero. ++ uint32_t image_id; ++ /// The client id specified in the query command (`a=q`). This one must ++ /// be used to create the response if it's non-zero. ++ uint32_t query_id; ++ /// The number specified in the transmission command (`I=`). If ++ /// non-zero, it may be used to identify the image instead of the ++ /// image_id, and it also should be mentioned in responses. ++ uint32_t image_number; ++ /// The last time when the image was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The total duration of the animation in milliseconds. ++ int total_duration; ++ /// The total size of cached image files for all frames. ++ int total_disk_size; ++ /// The global index of the creation command. Used to decide which image ++ /// is newer if they have the same image number. ++ uint64_t global_command_index; ++ /// The 1-based index of the currently displayed frame. ++ int current_frame; ++ /// The state of the animation, see `AnimationState`. ++ char animation_state; ++ /// The absolute time that is assumed to be the start of the current ++ /// frame (in ms since initialization). ++ Milliseconds current_frame_time; ++ /// The absolute time of the last redraw (in ms since initialization). ++ /// Used to check whether it's the first time we draw the image in the ++ /// current redraw cycle. ++ Milliseconds last_redraw; ++ /// The absolute time of the next redraw (in ms since initialization). ++ /// 0 means no redraw is scheduled. ++ Milliseconds next_redraw; ++ /// The unscaled pixel width and height of the image. Usually inherited ++ /// from the first frame. ++ int pix_width, pix_height; ++ /// The first frame. ++ ImageFrame first_frame; ++ /// The array of frames beyond the first one. ++ kvec_t(ImageFrame) frames_beyond_the_first; ++ /// Image placements. ++ khash_t(id2placement) *placements; ++ /// The default placement. ++ uint32_t default_placement; ++ /// The initial placement id, specified with the transmission command, ++ /// used to report success or failure. ++ uint32_t initial_placement_id; ++} Image; ++ ++typedef struct ImagePlacement { ++ /// The image this placement belongs to. ++ Image *image; ++ /// The id of the placement. Must be nonzero. ++ uint32_t placement_id; ++ /// The last time when the placement was displayed or otherwise touched. ++ Milliseconds atime; ++ /// The 1-based index of the protected pixmap. We protect a pixmap in ++ /// gr_load_pixmap to avoid unloading it right after it was loaded. ++ int protected_frame; ++ /// Whether the placement is used only for Unicode placeholders. ++ char virtual; ++ /// The scaling mode (see `ScaleMode`). ++ char scale_mode; ++ /// Height and width in cells. ++ uint16_t rows, cols; ++ /// Top-left corner of the source rectangle ('x=' and 'y='). ++ int src_pix_x, src_pix_y; ++ /// Height and width of the source rectangle (zero if full image). ++ int src_pix_width, src_pix_height; ++ /// The image appropriately scaled and uploaded to the X server. This ++ /// pixmap is premultiplied by alpha. ++ Pixmap first_pixmap; ++ /// The array of pixmaps beyond the first one. ++ kvec_t(Pixmap) pixmaps_beyond_the_first; ++ /// The dimensions of the cell used to scale the image. If cell ++ /// dimensions are changed (font change), the image will be rescaled. ++ uint16_t scaled_cw, scaled_ch; ++ /// The transformation to apply to the pixmap before drawing it. ++ PixmapTransformation pixmap_transformation; ++ /// If true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++ /// The text underneath this placement, valid only for classic ++ /// placements. On deletion, the text is restored. This is a malloced ++ /// array of rows*cols Glyphs. ++ Glyph *text_underneath; ++} ImagePlacement; ++ ++/// A rectangular piece of an image to be drawn. ++typedef struct { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// The position of the rectangle in pixels. ++ int screen_x_pix, screen_y_pix; ++ /// The starting row on the screen. ++ int screen_y_row; ++ /// The part of the whole image to be drawn, in cells. Starts are ++ /// zero-based, ends are exclusive. ++ int img_start_col, img_end_col, img_start_row, img_end_row; ++ /// The current cell width and height in pixels. ++ int cw, ch; ++ /// Whether colors should be inverted. ++ int reverse; ++} ImageRect; ++ ++/// Executes `code` for each frame of an image. Example: ++/// ++/// foreach_frame(image, frame, { ++/// printf("Frame %d\n", frame->index); ++/// }); ++/// ++#define foreach_frame(image, framevar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \ ++ ImageFrame *framevar = \ ++ __i == 0 ? &(image).first_frame \ ++ : &kv_A((image).frames_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++/// Executes `code` for each pixmap of a placement. Example: ++/// ++/// foreach_pixmap(placement, pixmap, { ++/// ... ++/// }); ++/// ++#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \ ++ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \ ++ Pixmap pixmapvar = \ ++ __i == 0 ? (placement).first_pixmap \ ++ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \ ++ code; \ ++ } } ++ ++ ++static Image *gr_find_image(uint32_t image_id); ++static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len); ++static void gr_delete_image(Image *img); ++static void gr_erase_placement(ImagePlacement *placement); ++static void gr_check_limits(); ++static void gr_try_restore_imagefile(ImageFrame *frame); ++static char *gr_base64dec(const char *src, size_t *size); ++static void sanitize_str(char *str, size_t max_len); ++static const char *sanitized_filename(const char *str); ++ ++/// The array of image rectangles to draw. It is reset each frame. ++static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}}; ++/// The known images (including the ones being uploaded). ++static khash_t(id2image) *images = NULL; ++/// The total number of placements in all images. ++static unsigned total_placement_count = 0; ++/// The total size of all image files stored in the on-disk cache. ++static int64_t images_disk_size = 0; ++/// The total size of all images and placements loaded into ram. ++static int64_t images_ram_size = 0; ++/// The id of the last loaded image. ++static uint32_t last_image_id = 0; ++/// Current cell width and heigh in pixels. ++static int current_cw = 0, current_ch = 0; ++/// The id of the currently uploaded image (when using direct uploading). ++static uint32_t current_upload_image_id = 0; ++/// The index of the frame currently being uploaded. ++static int current_upload_frame_index = 0; ++/// The time when the graphics module was initialized. ++static struct timespec initialization_time = {0}; ++/// The time when the current frame drawing started, used for debugging fps and ++/// to calculate the current frame for animations. ++static Milliseconds drawing_start_time; ++/// The global index of the current command. ++static uint64_t global_command_counter = 0; ++/// The next redraw times for each row of the terminal. Used for animations. ++/// 0 means no redraw is scheduled. ++static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL}; ++/// The number of files loaded in the current redraw cycle or command execution. ++static int debug_loaded_files_counter = 0; ++/// The number of pixmaps loaded in the current redraw cycle or command execution. ++static int debug_loaded_pixmaps_counter = 0; ++ ++/// The directory where the cache files are stored. ++static char cache_dir[MAX_FILENAME_SIZE - 16]; ++ ++/// The table used for color inversion. ++static unsigned char reverse_table[256]; ++ ++// Declared in the header. ++GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE; ++char graphics_display_images = 1; ++GraphicsCommandResult graphics_command_result = {0}; ++int graphics_next_redraw_delay = INT_MAX; ++ ++// Defined in config.h ++extern const char graphics_cache_dir_template[]; ++extern unsigned graphics_max_single_image_file_size; ++extern unsigned graphics_total_file_cache_size; ++extern unsigned graphics_max_single_image_ram_size; ++extern unsigned graphics_max_total_ram_size; ++extern unsigned graphics_max_total_placements; ++extern double graphics_excess_tolerance_ratio; ++extern unsigned graphics_animation_min_delay; ++ ++// Constants that are not important enough to expose in the config. ++ ++/// The time after which an interrupted (with another command) direct ++/// transmission cannot be resumed. ++static Milliseconds graphics_direct_transmission_timeout_ms = 2000; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic helpers. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define MIN(a, b) ((a) < (b) ? (a) : (b)) ++#define MAX(a, b) ((a) < (b) ? (b) : (a)) ++ ++/// Returns the difference between `end` and `start` in milliseconds. ++static int64_t gr_timediff_ms(const struct timespec *end, ++ const struct timespec *start) { ++ return (end->tv_sec - start->tv_sec) * 1000 + ++ (end->tv_nsec - start->tv_nsec) / 1000000; ++} ++ ++/// Returns the current time in milliseconds since the initialization. ++static Milliseconds gr_now_ms() { ++ struct timespec now; ++ clock_gettime(CLOCK_MONOTONIC, &now); ++ return gr_timediff_ms(&now, &initialization_time); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Logging. ++//////////////////////////////////////////////////////////////////////////////// ++ ++#define GR_LOG(...) \ ++ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0) ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Basic image management functions (create, delete, find, etc). ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns the 1-based index of the last frame. Note that you may want to use ++/// `gr_last_uploaded_frame_index` instead since the last frame may be not ++/// fully uploaded yet. ++static inline int gr_last_frame_index(Image *img) { ++ return kv_size(img->frames_beyond_the_first) + 1; ++} ++ ++/// Returns the frame with the given index. Returns NULL if the index is out of ++/// bounds. The index is 1-based. ++static ImageFrame *gr_get_frame(Image *img, int index) { ++ if (!img) ++ return NULL; ++ if (index == 1) ++ return &img->first_frame; ++ if (2 <= index && index <= gr_last_frame_index(img)) ++ return &kv_A(img->frames_beyond_the_first, index - 2); ++ return NULL; ++} ++ ++/// Returns the last frame of the image. Returns NULL if `img` is NULL. ++static ImageFrame *gr_get_last_frame(Image *img) { ++ if (!img) ++ return NULL; ++ return gr_get_frame(img, gr_last_frame_index(img)); ++} ++ ++/// Returns the 1-based index of the last frame or the second-to-last frame if ++/// the last frame is not fully uploaded yet. ++static inline int gr_last_uploaded_frame_index(Image *img) { ++ int last_index = gr_last_frame_index(img); ++ if (last_index > 1 && ++ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS) ++ return last_index - 1; ++ return last_index; ++} ++ ++/// Returns the pixmap for the frame with the given index. Returns 0 if the ++/// index is out of bounds. The index is 1-based. ++static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) { ++ if (index == 1) ++ return placement->first_pixmap; ++ if (2 <= index && ++ index <= kv_size(placement->pixmaps_beyond_the_first) + 1) ++ return kv_A(placement->pixmaps_beyond_the_first, index - 2); ++ return 0; ++} ++ ++/// Sets the pixmap for the frame with the given index. The index is 1-based. ++/// The array of pixmaps is resized if needed. ++static void gr_set_frame_pixmap(ImagePlacement *placement, int index, ++ Pixmap pixmap) { ++ if (index == 1) { ++ placement->first_pixmap = pixmap; ++ return; ++ } ++ // Resize the array if needed. ++ size_t old_size = kv_size(placement->pixmaps_beyond_the_first); ++ if (old_size < index - 1) { ++ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2); ++ for (size_t i = old_size; i < index - 1; i++) ++ kv_A(placement->pixmaps_beyond_the_first, i) = 0; ++ } ++ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap; ++} ++ ++/// Finds the image corresponding to the client id. Returns NULL if cannot find. ++static Image *gr_find_image(uint32_t image_id) { ++ khiter_t k = kh_get(id2image, images, image_id); ++ if (k == kh_end(images)) ++ return NULL; ++ Image *res = kh_value(images, k); ++ return res; ++} ++ ++/// Finds the newest image corresponding to the image number. Returns NULL if ++/// cannot find. ++static Image *gr_find_image_by_number(uint32_t image_number) { ++ if (image_number == 0) ++ return NULL; ++ Image *newest_img = NULL; ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ if (img->image_number == image_number && ++ (!newest_img || newest_img->global_command_index < ++ img->global_command_index)) ++ newest_img = img; ++ }); ++ if (!newest_img) ++ GR_LOG("Image number %u not found\n", image_number); ++ else ++ GR_LOG("Found image number %u, its id is %u\n", image_number, ++ img->image_id); ++ return newest_img; ++} ++ ++/// Finds the placement corresponding to the id. If the placement id is 0, ++/// returns some default placement. ++static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) { ++ if (!img) ++ return NULL; ++ if (placement_id == 0) { ++ // Try to get the default placement. ++ ImagePlacement *dflt = NULL; ++ if (img->default_placement != 0) ++ dflt = gr_find_placement(img, img->default_placement); ++ if (dflt) ++ return dflt; ++ // If there is no default placement, return the first one and ++ // set it as the default. ++ kh_foreach_value(img->placements, dflt, { ++ img->default_placement = dflt->placement_id; ++ return dflt; ++ }); ++ // If there are no placements, return NULL. ++ return NULL; ++ } ++ khiter_t k = kh_get(id2placement, img->placements, placement_id); ++ if (k == kh_end(img->placements)) ++ return NULL; ++ ImagePlacement *res = kh_value(img->placements, k); ++ return res; ++} ++ ++/// Finds the placement by image id and placement id. ++static ImagePlacement *gr_find_image_and_placement(uint32_t image_id, ++ uint32_t placement_id) { ++ return gr_find_placement(gr_find_image(image_id), placement_id); ++} ++ ++/// Returns a pointer to the glyph under the classic placement with `image_id` ++/// and `placement_id` at `col` and `row` (1-based). May return NULL if the ++/// underneath text is unknown. ++Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, ++ int col, int row) { ++ ImagePlacement *placement = ++ gr_find_image_and_placement(image_id, placement_id); ++ if (!placement || !placement->text_underneath) ++ return NULL; ++ col--; ++ row--; ++ if (col < 0 || col >= placement->cols || row < 0 || ++ row >= placement->rows) ++ return NULL; ++ return &placement->text_underneath[row * placement->cols + col]; ++} ++ ++/// Writes the name of the on-disk cache file to `out`. `max_len` should be the ++/// size of `out`. The name will be something like ++/// "/tmp/st-images-xxx/img-ID-FRAME". ++static void gr_get_frame_filename(ImageFrame *frame, char *out, ++ size_t max_len) { ++ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir, ++ frame->image->image_id, frame->index); ++} ++ ++/// Returns the (estimation) of the RAM size used by the frame right now. ++static unsigned gr_frame_current_ram_size(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return 0; ++ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by a single frame pixmap. ++static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) { ++ return (unsigned)placement->pixmap_transformation.pixmap_w * ++ placement->pixmap_transformation.pixmap_h * 4; ++} ++ ++/// Returns the (estimation) of the RAM size used by the placemenet right now. ++static unsigned gr_placement_current_ram_size(ImagePlacement *placement) { ++ unsigned single_frame_size = ++ gr_placement_single_frame_ram_size(placement); ++ unsigned result = 0; ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ result += single_frame_size; ++ }); ++ return result; ++} ++ ++/// Unload the frame from RAM (i.e. delete the corresponding imlib object). ++/// If the on-disk file of the frame is preserved, it can be reloaded later. ++static void gr_unload_frame(ImageFrame *frame) { ++ if (!frame->imlib_object) ++ return; ++ ++ unsigned frame_ram_size = gr_frame_current_ram_size(frame); ++ images_ram_size -= frame_ram_size; ++ ++ imlib_context_set_image(frame->imlib_object); ++ imlib_free_image_and_decache(); ++ frame->imlib_object = NULL; ++ ++ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_ram_size / 1024, ++ frame_ram_size / 1024); ++} ++ ++/// Unload all frames of the image. ++static void gr_unload_all_frames(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_unload_frame(frame); ++ }); ++} ++ ++/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps). ++/// If the on-disk files or imlib objects of the corresponding image are ++/// preserved, the placement can be reloaded later. ++static void gr_unload_placement(ImagePlacement *placement) { ++ unsigned placement_ram_size = gr_placement_current_ram_size(placement); ++ images_ram_size -= placement_ram_size; ++ ++ Display *disp = imlib_context_get_display(); ++ foreach_pixmap(*placement, pixmap, { ++ if (pixmap) ++ XFreePixmap(disp, pixmap); ++ }); ++ ++ placement->first_pixmap = 0; ++ placement->pixmaps_beyond_the_first.n = 0; ++ placement->scaled_ch = placement->scaled_cw = 0; ++ ++ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, images_ram_size / 1024, ++ placement_ram_size / 1024); ++} ++ ++/// Unload a single pixmap of the placement from RAM. ++static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) { ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (!pixmap) ++ return; ++ ++ Display *disp = imlib_context_get_display(); ++ XFreePixmap(disp, pixmap); ++ gr_set_frame_pixmap(placement, frameidx, 0); ++ images_ram_size -= gr_placement_single_frame_ram_size(placement); ++ ++ GR_LOG("After unloading pixmap %ld of " ++ "placement %u/%u (atime %ld ms ago) " ++ "frame %u (atime %ld ms ago) " ++ "ram: %ld KiB (- %u KiB)\n", ++ pixmap, placement->image->image_id, placement->placement_id, ++ drawing_start_time - placement->atime, frameidx, ++ drawing_start_time - ++ gr_get_frame(placement->image, frameidx)->atime, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024); ++} ++ ++/// Closes the on-disk cache file of the frame `frame`. ++static void gr_close_disk_cache_file(ImageFrame *frame) { ++ if (frame && frame->open_file) { ++ fclose(frame->open_file); ++ frame->open_file = NULL; ++ } ++} ++ ++/// Deletes the on-disk cache file corresponding to the frame. The in-ram image ++/// object (if it exists) is not deleted, placements are not unloaded either. ++static void gr_delete_imagefile(ImageFrame *frame) { ++ // It may still be being loaded. Close the file in this case. ++ gr_close_disk_cache_file(frame); ++ ++ if (frame->disk_size == 0) ++ return; ++ ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ remove(filename); ++ ++ unsigned disk_size = frame->disk_size; ++ images_disk_size -= disk_size; ++ frame->image->total_disk_size -= disk_size; ++ frame->disk_size = 0; ++ ++ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) " ++ "disk: %ld KiB (- %u KiB)\n", ++ frame->image->image_id, frame->index, ++ drawing_start_time - frame->atime, images_disk_size / 1024, ++ disk_size / 1024); ++} ++ ++/// Deletes all on-disk cache files of the image (for each frame). ++static void gr_delete_imagefiles(Image *img) { ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ }); ++} ++ ++/// Deletes the given placement: unloads, frees the object, erases it from the ++/// screen in the classic case, but doesn't change the `placements` hash table. ++static void gr_delete_placement_keep_id(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id, ++ placement->placement_id); ++ // Erase the placement from the screen if it's classic and there is some ++ // saved text underneath. ++ if (placement->text_underneath && !placement->virtual) ++ gr_erase_placement(placement); ++ gr_unload_placement(placement); ++ kv_destroy(placement->pixmaps_beyond_the_first); ++ free(placement->text_underneath); ++ free(placement); ++ total_placement_count--; ++} ++ ++/// Deletes all placements of `img`. ++static void gr_delete_all_placements(Image *img) { ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_delete_placement_keep_id(placement); ++ }); ++ kh_clear(id2placement, img->placements); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// but doesn't change the `images` hash table. ++static void gr_delete_image_keep_id(Image *img) { ++ if (!img) ++ return; ++ GR_LOG("Deleting image %u\n", img->image_id); ++ foreach_frame(*img, frame, { ++ gr_delete_imagefile(frame); ++ gr_unload_frame(frame); ++ if (frame->original_filename) ++ free(frame->original_filename); ++ }); ++ kv_destroy(img->frames_beyond_the_first); ++ gr_delete_all_placements(img); ++ kh_destroy(id2placement, img->placements); ++ free(img); ++} ++ ++/// Deletes the given image: unloads, deletes the file, frees the Image object, ++/// and also removes it from `images`. ++static void gr_delete_image(Image *img) { ++ if (!img) ++ return; ++ uint32_t id = img->image_id; ++ gr_delete_image_keep_id(img); ++ khiter_t k = kh_get(id2image, images, id); ++ kh_del(id2image, images, k); ++} ++ ++/// Deletes the given placement: unloads, frees the object, erases from the ++/// screen (in the classic case), and also removes it from `placements`. ++static void gr_delete_placement(ImagePlacement *placement) { ++ if (!placement) ++ return; ++ uint32_t id = placement->placement_id; ++ Image *img = placement->image; ++ gr_delete_placement_keep_id(placement); ++ khiter_t k = kh_get(id2placement, img->placements, id); ++ kh_del(id2placement, img->placements, k); ++} ++ ++/// Deletes all images and clears `images`. ++static void gr_delete_all_images() { ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ gr_delete_image_keep_id(img); ++ }); ++ kh_clear(id2image, images); ++} ++ ++/// Update the atime of the image. ++static void gr_touch_image(Image *img) { ++ img->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the frame. ++static void gr_touch_frame(ImageFrame *frame) { ++ frame->image->atime = frame->atime = gr_now_ms(); ++} ++ ++/// Update the atime of the placement. Touches the images too. ++static void gr_touch_placement(ImagePlacement *placement) { ++ placement->image->atime = placement->atime = gr_now_ms(); ++} ++ ++/// Creates a new image with the given id. If an image with that id already ++/// exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static Image *gr_new_image(uint32_t id) { ++ if (id == 0) { ++ do { ++ id = rand(); ++ // Avoid IDs that don't need full 32 bits. ++ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 || ++ gr_find_image(id)); ++ GR_LOG("Generated random image id %u\n", id); ++ } ++ Image *img = gr_find_image(id); ++ gr_delete_image_keep_id(img); ++ GR_LOG("Creating image %u\n", id); ++ img = malloc(sizeof(Image)); ++ memset(img, 0, sizeof(Image)); ++ img->placements = kh_init(id2placement); ++ int ret; ++ khiter_t k = kh_put(id2image, images, id, &ret); ++ kh_value(images, k) = img; ++ img->image_id = id; ++ gr_touch_image(img); ++ img->global_command_index = global_command_counter; ++ return img; ++} ++ ++/// Creates a new frame at the end of the frame array. It may be the first frame ++/// if there are no frames yet. ++static ImageFrame *gr_append_new_frame(Image *img) { ++ ImageFrame *frame = NULL; ++ if (img->first_frame.index == 0 && ++ kv_size(img->frames_beyond_the_first) == 0) { ++ frame = &img->first_frame; ++ frame->index = 1; ++ } else { ++ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first); ++ memset(frame, 0, sizeof(ImageFrame)); ++ frame->index = kv_size(img->frames_beyond_the_first) + 1; ++ } ++ frame->image = img; ++ gr_touch_frame(frame); ++ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id); ++ return frame; ++} ++ ++/// Creates a new placement with the given id. If a placement with that id ++/// already exists, it is deleted first. If the provided id is 0, generates a ++/// random id. ++static ImagePlacement *gr_new_placement(Image *img, uint32_t id) { ++ if (id == 0) { ++ do { ++ // Currently we support only 24-bit IDs. ++ id = rand() & 0xFFFFFF; ++ // Avoid IDs that need only one byte. ++ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id)); ++ } ++ ImagePlacement *placement = gr_find_placement(img, id); ++ gr_delete_placement_keep_id(placement); ++ GR_LOG("Creating placement %u/%u\n", img->image_id, id); ++ placement = malloc(sizeof(ImagePlacement)); ++ memset(placement, 0, sizeof(ImagePlacement)); ++ total_placement_count++; ++ int ret; ++ khiter_t k = kh_put(id2placement, img->placements, id, &ret); ++ kh_value(img->placements, k) = placement; ++ placement->image = img; ++ placement->placement_id = id; ++ gr_touch_placement(placement); ++ if (img->default_placement == 0) ++ img->default_placement = id; ++ return placement; ++} ++ ++static int64_t ceil_div(int64_t a, int64_t b) { ++ return (a + b - 1) / b; ++} ++ ++/// Computes the best number of rows and columns for a placement if it's not ++/// specified, and also adjusts the source rectangle size. ++static void gr_infer_placement_size_maybe(ImagePlacement *placement) { ++ // The size of the image. ++ int image_pix_width = placement->image->pix_width; ++ int image_pix_height = placement->image->pix_height; ++ // Negative values are not allowed. Quietly set them to 0. ++ if (placement->src_pix_x < 0) ++ placement->src_pix_x = 0; ++ if (placement->src_pix_y < 0) ++ placement->src_pix_y = 0; ++ if (placement->src_pix_width < 0) ++ placement->src_pix_width = 0; ++ if (placement->src_pix_height < 0) ++ placement->src_pix_height = 0; ++ // If the source rectangle is outside the image, truncate it. ++ if (placement->src_pix_x > image_pix_width) ++ placement->src_pix_x = image_pix_width; ++ if (placement->src_pix_y > image_pix_height) ++ placement->src_pix_y = image_pix_height; ++ // If the source rectangle is not specified, use the whole image. If ++ // it's partially outside the image, truncate it. ++ if (placement->src_pix_width == 0 || ++ placement->src_pix_x + placement->src_pix_width > image_pix_width) ++ placement->src_pix_width = ++ image_pix_width - placement->src_pix_x; ++ if (placement->src_pix_height == 0 || ++ placement->src_pix_y + placement->src_pix_height > image_pix_height) ++ placement->src_pix_height = ++ image_pix_height - placement->src_pix_y; ++ ++ if (placement->cols != 0 && placement->rows != 0) ++ return; ++ if (placement->src_pix_width == 0 || placement->src_pix_height == 0) ++ return; ++ if (current_cw == 0 || current_ch == 0) ++ return; ++ ++ // If no size is specified, use the image size. ++ if (placement->cols == 0 && placement->rows == 0) { ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ return; ++ } ++ ++ // Some applications specify only one of the dimensions. ++ if (placement->scale_mode == SCALE_MODE_CONTAIN) { ++ // If we preserve aspect ratio and fit to width/height, the most ++ // logical thing is to find the minimum size of the ++ // non-specified dimension that allows the image to fit the ++ // specified dimension. ++ if (placement->cols == 0) { ++ placement->cols = ceil_div( ++ placement->src_pix_width * placement->rows * ++ current_ch, ++ placement->src_pix_height * current_cw); ++ return; ++ } ++ if (placement->rows == 0) { ++ placement->rows = ++ ceil_div(placement->src_pix_height * ++ placement->cols * current_cw, ++ placement->src_pix_width * current_ch); ++ return; ++ } ++ } else { ++ // Otherwise we stretch the image or preserve the original size. ++ // In both cases we compute the best number of columns from the ++ // pixel size and cell size. ++ // TODO: In the case of stretching it's not the most logical ++ // thing to do, may need to revisit in the future. ++ // Currently we switch to SCALE_MODE_CONTAIN when only one ++ // of the dimensions is specified, so this case shouldn't ++ // happen in practice. ++ if (!placement->cols) ++ placement->cols = ++ ceil_div(placement->src_pix_width, current_cw); ++ if (!placement->rows) ++ placement->rows = ++ ceil_div(placement->src_pix_height, current_ch); ++ } ++} ++ ++/// Adjusts the current frame index if enough time has passed since the display ++/// of the current frame. Also computes the time of the next redraw of this ++/// image (`img->next_redraw`). The current time is passed as an argument so ++/// that all animations are in sync. ++static void gr_update_frame_index(Image *img, Milliseconds now) { ++ if (img->current_frame == 0) { ++ img->current_frame_time = now; ++ img->current_frame = 1; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ // If the animation is stopped, show the current frame. ++ if (!img->animation_state || ++ img->animation_state == ANIMATION_STATE_STOPPED || ++ img->animation_state == ANIMATION_STATE_UNSET) { ++ // The next redraw is never (unless the state is changed). ++ img->next_redraw = 0; ++ return; ++ } ++ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img); ++ // If we are loading and we reached the last frame, show the last frame. ++ if (img->animation_state == ANIMATION_STATE_LOADING && ++ img->current_frame == last_uploaded_frame_index) { ++ // The next redraw is never (unless the state is changed or ++ // frames are added). ++ img->next_redraw = 0; ++ return; ++ } ++ ++ // Check how many milliseconds passed since the current frame was shown. ++ int passed_ms = now - img->current_frame_time; ++ // If the animation is looping and too much time has passes, we can ++ // make a shortcut. ++ if (img->animation_state == ANIMATION_STATE_LOOPING && ++ img->total_duration > 0 && passed_ms >= img->total_duration) { ++ passed_ms %= img->total_duration; ++ img->current_frame_time = now - passed_ms; ++ } ++ // Find the next frame. ++ int original_frame_index = img->current_frame; ++ while (1) { ++ ImageFrame *frame = gr_get_frame(img, img->current_frame); ++ if (!frame) { ++ // The frame doesn't exist, go to the first frame. ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX(1, img->first_frame.gap); ++ return; ++ } ++ if (frame->gap >= 0 && passed_ms < frame->gap) { ++ // Not enough time has passed, we are still in the same ++ // frame, and it's not a gapless frame. ++ img->next_redraw = ++ img->current_frame_time + MAX(1, frame->gap); ++ return; ++ } ++ // Otherwise go to the next frame. ++ passed_ms -= MAX(0, frame->gap); ++ if (img->current_frame >= last_uploaded_frame_index) { ++ // It's the last frame, if the animation is loading, ++ // remain on it. ++ if (img->animation_state == ANIMATION_STATE_LOADING) { ++ img->next_redraw = 0; ++ return; ++ } ++ // Otherwise the animation is looping. ++ img->current_frame = 1; ++ // TODO: Support finite number of loops. ++ } else { ++ img->current_frame++; ++ } ++ // Make sure we don't get stuck in an infinite loop. ++ if (img->current_frame == original_frame_index) { ++ // We looped through all frames, but haven't reached the ++ // next frame yet. This may happen if too much time has ++ // passed since the last redraw or all the frames are ++ // gapless. Just move on to the next frame. ++ img->current_frame++; ++ if (img->current_frame > ++ last_uploaded_frame_index) ++ img->current_frame = 1; ++ img->current_frame_time = now; ++ img->next_redraw = now + MAX( ++ 1, gr_get_frame(img, img->current_frame)->gap); ++ return; ++ } ++ // Adjust the start time of the frame. The next redraw time will ++ // be set in the next iteration. ++ img->current_frame_time += MAX(0, frame->gap); ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Unloading and deleting images to save resources. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns whether the original file of the frame is still available (exists, ++/// is a regular file, has the same size and mtime). ++static int gr_is_original_file_still_available(ImageFrame *frame) { ++ if (!frame->original_filename) ++ return 0; ++ struct stat st; ++ if (stat(frame->original_filename, &st) != 0) ++ return 0; ++ if (!S_ISREG(st.st_mode)) ++ return 0; ++ if (st.st_size == 0 || st.st_size > graphics_max_single_image_file_size) ++ return 0; ++ if (frame->expected_size && st.st_size != frame->expected_size) ++ return 0; ++ if (st.st_mtime != frame->original_file_mtime) ++ return 0; ++ return 1; ++} ++ ++/// A helper to compare frames by atime for qsort. ++static int gr_cmp_frames_by_atime(const void *a, const void *b) { ++ ImageFrame *frame_a = *(ImageFrame *const *)a; ++ ImageFrame *frame_b = *(ImageFrame *const *)b; ++ if (frame_a->atime == frame_b->atime) ++ return frame_a->image->global_command_index - ++ frame_b->image->global_command_index; ++ return frame_a->atime - frame_b->atime; ++} ++ ++/// A helper to compare images by atime for qsort. ++static int gr_cmp_images_by_atime(const void *a, const void *b) { ++ Image *img_a = *(Image *const *)a; ++ Image *img_b = *(Image *const *)b; ++ if (img_a->atime == img_b->atime) ++ return img_a->global_command_index - ++ img_b->global_command_index; ++ return img_a->atime - img_b->atime; ++} ++ ++/// A helper to compare placements by atime for qsort. ++static int gr_cmp_placements_by_atime(const void *a, const void *b) { ++ ImagePlacement *p_a = *(ImagePlacement **)a; ++ ImagePlacement *p_b = *(ImagePlacement **)b; ++ if (p_a->atime == p_b->atime) ++ return p_a->image->global_command_index - ++ p_b->image->global_command_index; ++ return p_a->atime - p_b->atime; ++} ++ ++typedef kvec_t(Image *) ImageVec; ++typedef kvec_t(ImagePlacement *) ImagePlacementVec; ++typedef kvec_t(ImageFrame *) ImageFrameVec; ++ ++/// Returns an array of pointers to all images sorted by atime. ++static ImageVec gr_get_images_sorted_by_atime() { ++ ImageVec vec; ++ kv_init(vec); ++ if (kh_size(images) == 0) ++ return vec; ++ kv_resize(Image *, vec, kh_size(images)); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { kv_push(Image *, vec, img); }); ++ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all placements sorted by atime. ++static ImagePlacementVec gr_get_placements_sorted_by_atime() { ++ ImagePlacementVec vec; ++ kv_init(vec); ++ if (total_placement_count == 0) ++ return vec; ++ kv_resize(ImagePlacement *, vec, total_placement_count); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ kv_push(ImagePlacement *, vec, placement); ++ }); ++ }); ++ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *), ++ gr_cmp_placements_by_atime); ++ return vec; ++} ++ ++/// Returns an array of pointers to all frames sorted by atime. ++static ImageFrameVec gr_get_frames_sorted_by_atime() { ++ ImageFrameVec frames; ++ kv_init(frames); ++ Image *img = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ kv_push(ImageFrame *, frames, frame); ++ }); ++ }); ++ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *), ++ gr_cmp_frames_by_atime); ++ return frames; ++} ++ ++/// An object that can be unloaded from RAM. ++typedef struct { ++ /// Some score, probably based on access time. The lower the score, the ++ /// more likely that the object should be unloaded. ++ int64_t score; ++ union { ++ ImagePlacement *placement; ++ ImageFrame *frame; ++ }; ++ /// If zero, the object is the imlib object of `frame`, if non-zero, ++ /// the object is a pixmap of `frameidx`-th frame of `placement`. ++ int frameidx; ++} UnloadableObject; ++ ++typedef kvec_t(UnloadableObject) UnloadableObjectVec; ++ ++/// A helper to compare unloadable objects by score for qsort. ++static int gr_cmp_unloadable_objects(const void *a, const void *b) { ++ UnloadableObject *obj_a = (UnloadableObject *)a; ++ UnloadableObject *obj_b = (UnloadableObject *)b; ++ return obj_a->score - obj_b->score; ++} ++ ++/// Unloads an unloadable object from RAM. ++static void gr_unload_object(UnloadableObject *obj) { ++ if (obj->frameidx) { ++ if (obj->placement->protected_frame == obj->frameidx) ++ return; ++ gr_unload_pixmap(obj->placement, obj->frameidx); ++ } else { ++ gr_unload_frame(obj->frame); ++ } ++} ++ ++/// Returns the recency threshold for an image. Frames that were accessed within ++/// this threshold from now are considered recent and may be handled ++/// differently because we may need them again very soon. ++static Milliseconds gr_recency_threshold(Image *img) { ++ return img->total_duration * 2 + 1000; ++} ++ ++/// Creates an unloadable object for the imlib object of a frame. ++static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now, ++ ImageFrame *frame) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = 0; ++ obj.frame = frame; ++ Milliseconds atime = frame->atime; ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent frame, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Randomize the score because it's not very clear in which ++ // order we want to unload them: reloading a frame may require ++ // reloading other frames. ++ obj.score = now + 1000 + rand() % 1000; ++ } ++ return obj; ++} ++ ++/// Creates an unloadable object for a pixmap. ++static UnloadableObject ++gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame, ++ ImagePlacement *placement) { ++ UnloadableObject obj = {0}; ++ obj.frameidx = frame->index; ++ obj.placement = placement; ++ obj.score = placement->atime; ++ // Since we don't store pixmap atimes, use the ++ // oldest atime of the frame and the placement. ++ Milliseconds atime = MIN(placement->atime, frame->atime); ++ obj.score = atime; ++ if (atime >= now - gr_recency_threshold(frame->image)) { ++ // This is a recent pixmap, probably from an active animation. ++ // Score it above `now` to prefer unloading non-active frames. ++ // Also assign higher scores to frames that are closer to the ++ // current frame (more likely to be used soon). ++ int num_frames = gr_last_frame_index(frame->image); ++ int dist = frame->index - frame->image->current_frame; ++ if (dist < 0) ++ dist += num_frames; ++ obj.score = ++ now + 1000 + (num_frames - dist) * 1000 / num_frames; ++ // If the pixmap is much larger than the imlib image, prefer to ++ // unload the pixmap by adding up to -1000 to the score. If the ++ // imlib image is larger, add up to +1000. ++ float imlib_size = gr_frame_current_ram_size(frame); ++ float pixmap_size = ++ gr_placement_single_frame_ram_size(placement); ++ obj.score += ++ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5); ++ } ++ return obj; ++} ++ ++/// Returns an array of unloadable objects sorted by score. ++static UnloadableObjectVec ++gr_get_unloadable_objects_sorted_by_score(Milliseconds now) { ++ UnloadableObjectVec objects; ++ kv_init(objects); ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ foreach_frame(*img, frame, { ++ if (frame->imlib_object) { ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_frame(now, ++ frame)); ++ } ++ int frameidx = frame->index; ++ kh_foreach_value(img->placements, placement, { ++ if (!gr_get_frame_pixmap(placement, frameidx)) ++ continue; ++ kv_push(UnloadableObject, objects, ++ gr_unloadable_object_for_pixmap( ++ now, frame, placement)); ++ }); ++ }); ++ }); ++ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject), ++ gr_cmp_unloadable_objects); ++ return objects; ++} ++ ++/// Returns the limit adjusted by the excess tolerance ratio. ++static inline unsigned apply_tolerance(unsigned limit) { ++ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio); ++} ++ ++/// Checks RAM and disk cache limits and deletes/unloads some images. ++static void gr_check_limits() { ++ Milliseconds now = gr_now_ms(); ++ ImageVec images_sorted = {0}; ++ ImagePlacementVec placements_sorted = {0}; ++ ImageFrameVec frames_sorted = {0}; ++ UnloadableObjectVec objects_sorted = {0}; ++ int images_begin = 0; ++ int placements_begin = 0; ++ char changed = 0; ++ // First reduce the number of images if there are too many. ++ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many images: %d\n", kh_size(images)); ++ changed = 1; ++ images_sorted = gr_get_images_sorted_by_atime(); ++ int to_delete = kv_size(images_sorted) - ++ graphics_max_total_placements; ++ for (; images_begin < to_delete; images_begin++) ++ gr_delete_image(images_sorted.a[images_begin]); ++ } ++ // Then reduce the number of placements if there are too many. ++ if (total_placement_count > ++ apply_tolerance(graphics_max_total_placements)) { ++ GR_LOG("Too many placements: %d\n", total_placement_count); ++ changed = 1; ++ placements_sorted = gr_get_placements_sorted_by_atime(); ++ int to_delete = kv_size(placements_sorted) - ++ graphics_max_total_placements; ++ for (; placements_begin < to_delete; placements_begin++) { ++ ImagePlacement *placement = ++ placements_sorted.a[placements_begin]; ++ if (placement->protected_frame) ++ break; ++ gr_delete_placement(placement); ++ } ++ } ++ // Then reduce the size of the image file cache. The files correspond to ++ // image frames. ++ if (images_disk_size > ++ apply_tolerance(graphics_total_file_cache_size)) { ++ GR_LOG("Too big disk cache: %ld KiB\n", ++ images_disk_size / 1024); ++ changed = 1; ++ frames_sorted = gr_get_frames_sorted_by_atime(); ++ for (int i = 0; i < kv_size(frames_sorted); i++) { ++ if (images_disk_size <= graphics_total_file_cache_size) ++ break; ++ gr_delete_imagefile(kv_A(frames_sorted, i)); ++ } ++ } ++ // Then unload images from RAM. ++ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) { ++ changed = 1; ++ int frames_begin = 0; ++ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024); ++ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now); ++ for (int i = 0; i < kv_size(objects_sorted); i++) { ++ if (images_ram_size <= graphics_max_total_ram_size) ++ break; ++ gr_unload_object(&kv_A(objects_sorted, i)); ++ } ++ } ++ if (changed) { ++ Milliseconds end = gr_now_ms(); ++ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB " ++ "img count: %d placement count: %d Took %ld ms\n", ++ images_ram_size / 1024, images_disk_size / 1024, ++ kh_size(images), total_placement_count, end - now); ++ } ++ kv_destroy(images_sorted); ++ kv_destroy(placements_sorted); ++ kv_destroy(frames_sorted); ++ kv_destroy(objects_sorted); ++} ++ ++/// Unloads all images by user request. ++void gr_unload_images_to_reduce_ram() { ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ kh_foreach_value(img->placements, placement, { ++ if (placement->protected_frame) ++ continue; ++ gr_unload_placement(placement); ++ }); ++ gr_unload_all_frames(img); ++ }); ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Image loading. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2 ++/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted ++/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout ++/// on little-endian architectures). ++static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format, ++ size_t num_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ if (format == 32) { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | ++ ((DATA32)from[byte_i + 3]) << 24; ++ } ++ } else { ++ for (unsigned i = 0; i < num_pixels; ++i) { ++ unsigned byte_i = i * pixel_size; ++ to[i] = ((DATA32)from[byte_i + 2]) | ++ ((DATA32)from[byte_i + 1]) << 8 | ++ ((DATA32)from[byte_i]) << 16 | 0xFF000000; ++ } ++ } ++} ++ ++/// Loads uncompressed RGB or RGBA image data from a file. ++static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file, ++ int format, ++ size_t total_pixels) { ++ unsigned char chunk[BUFSIZ]; ++ size_t pixel_size = format == 24 ? 3 : 4; ++ size_t chunk_size_pix = BUFSIZ / 4; ++ size_t chunk_size_bytes = chunk_size_pix * pixel_size; ++ size_t bytes = total_pixels * pixel_size; ++ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels; ++ chunk_start_pix += chunk_size_pix) { ++ size_t read_size = fread(chunk, 1, chunk_size_bytes, file); ++ size_t read_pixels = read_size / pixel_size; ++ if (chunk_start_pix + read_pixels > total_pixels) ++ read_pixels = total_pixels - chunk_start_pix; ++ gr_copy_pixels(data + chunk_start_pix, chunk, format, ++ read_pixels); ++ } ++} ++ ++#define COMPRESSED_CHUNK_SIZE BUFSIZ ++#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4) ++ ++/// Loads compressed RGB or RGBA image data from a file. ++static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file, ++ int format, size_t total_pixels) { ++ size_t pixel_size = format == 24 ? 3 : 4; ++ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE]; ++ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE]; ++ ++ z_stream strm; ++ strm.zalloc = Z_NULL; ++ strm.zfree = Z_NULL; ++ strm.opaque = Z_NULL; ++ strm.next_out = decompressed_chunk; ++ strm.avail_out = DECOMPRESSED_CHUNK_SIZE; ++ strm.avail_in = 0; ++ strm.next_in = Z_NULL; ++ int ret = inflateInit(&strm); ++ if (ret != Z_OK) ++ return 1; ++ ++ int error = 0; ++ int progress = 0; ++ size_t total_copied_pixels = 0; ++ while (1) { ++ // If we don't have enough data in the input buffer, try to read ++ // from the file. ++ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) { ++ // Move the existing data to the beginning. ++ memmove(compressed_chunk, strm.next_in, strm.avail_in); ++ strm.next_in = compressed_chunk; ++ // Read more data. ++ size_t bytes_read = fread( ++ compressed_chunk + strm.avail_in, 1, ++ COMPRESSED_CHUNK_SIZE - strm.avail_in, file); ++ strm.avail_in += bytes_read; ++ if (bytes_read != 0) ++ progress = 1; ++ } ++ ++ // Try to inflate the data. ++ int ret = inflate(&strm, Z_SYNC_FLUSH); ++ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) { ++ error = 1; ++ fprintf(stderr, ++ "error: could not decompress the image, error " ++ "%s\n", ++ ret == Z_MEM_ERROR ? "Z_MEM_ERROR" ++ : "Z_DATA_ERROR"); ++ break; ++ } ++ ++ // Copy the data from the output buffer to the image. ++ size_t full_pixels = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size; ++ // Make sure we don't overflow the image. ++ if (full_pixels > total_pixels - total_copied_pixels) ++ full_pixels = total_pixels - total_copied_pixels; ++ if (full_pixels > 0) { ++ // Copy pixels. ++ gr_copy_pixels(data, decompressed_chunk, format, ++ full_pixels); ++ data += full_pixels; ++ total_copied_pixels += full_pixels; ++ if (total_copied_pixels >= total_pixels) { ++ // We filled the whole image, there may be some ++ // data left, but we just truncate it. ++ break; ++ } ++ // Move the remaining data to the beginning. ++ size_t copied_bytes = full_pixels * pixel_size; ++ size_t leftover = ++ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) - ++ copied_bytes; ++ memmove(decompressed_chunk, ++ decompressed_chunk + copied_bytes, leftover); ++ strm.next_out -= copied_bytes; ++ strm.avail_out += copied_bytes; ++ progress = 1; ++ } ++ ++ // If we haven't made any progress, then we have reached the end ++ // of both the file and the inflated data. ++ if (!progress) ++ break; ++ progress = 0; ++ } ++ ++ inflateEnd(&strm); ++ return error; ++} ++ ++#undef COMPRESSED_CHUNK_SIZE ++#undef DECOMPRESSED_CHUNK_SIZE ++ ++/// Load the image from a file containing raw pixel data (RGB or RGBA), the data ++/// may be compressed. ++static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame, ++ const char *filename) { ++ size_t total_pixels = frame->data_pix_width * frame->data_pix_height; ++ if (total_pixels * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: image %u frame %u is too big too load: %zu > %u\n", ++ frame->image->image_id, frame->index, total_pixels * 4, ++ graphics_max_single_image_ram_size); ++ return NULL; ++ } ++ ++ FILE* file = fopen(filename, "rb"); ++ if (!file) { ++ fprintf(stderr, ++ "error: could not open image file: %s\n", ++ sanitized_filename(filename)); ++ return NULL; ++ } ++ ++ Imlib_Image image = imlib_create_image(frame->data_pix_width, ++ frame->data_pix_height); ++ if (!image) { ++ fprintf(stderr, ++ "error: could not create an image of size %d x %d\n", ++ frame->data_pix_width, frame->data_pix_height); ++ fclose(file); ++ return NULL; ++ } ++ ++ imlib_context_set_image(image); ++ imlib_image_set_has_alpha(1); ++ DATA32* data = imlib_image_get_data(); ++ ++ if (frame->compression == 0) { ++ gr_load_raw_pixel_data_uncompressed(data, file, frame->format, ++ total_pixels); ++ } else { ++ int ret = gr_load_raw_pixel_data_compressed( ++ data, file, frame->format, total_pixels); ++ if (ret != 0) { ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ fclose(file); ++ return NULL; ++ } ++ } ++ ++ fclose(file); ++ imlib_image_put_back_data(data); ++ return image; ++} ++ ++/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object ++/// is fully composed on top of the background frame. If the frame is already ++/// loaded, does nothing. Loading may fail, in which case the status of the ++/// frame will be set to STATUS_RAM_LOADING_ERROR. ++static void gr_load_imlib_object(ImageFrame *frame) { ++ if (frame->imlib_object) ++ return; ++ ++ // If the image is uninitialized or uploading has failed, or the file ++ // has been deleted, we cannot load the image. ++ if (frame->status < STATUS_UPLOADING_SUCCESS) ++ return; ++ if (frame->disk_size == 0) { ++ // In some cases the original image file may still be available, ++ // try to restore it. ++ gr_try_restore_imagefile(frame); ++ } ++ if (frame->disk_size == 0) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR && ++ frame->status >= STATUS_UPLOADING_SUCCESS) { ++ fprintf(stderr, ++ "error: cached image was deleted: %u frame %u\n", ++ frame->image->image_id, frame->index); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ // Prevent recursive dependences between frames. ++ if (frame->ram_loading_in_progress) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: recursive loading of image %u frame " ++ "%u\n", ++ frame->image->image_id, frame->index); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ frame->ram_loading_in_progress = 1; ++ ++ // Load the background frame if needed. Hopefully it's not recursive. ++ ImageFrame *bg_frame = NULL; ++ if (frame->background_frame_index) { ++ bg_frame = gr_get_frame(frame->image, ++ frame->background_frame_index); ++ if (!bg_frame) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: could not find background " ++ "frame %d for image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ } ++ return; ++ } ++ gr_load_imlib_object(bg_frame); ++ if (!bg_frame->imlib_object) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: could not load background " ++ "frame %d for image %u frame %d\n", ++ frame->background_frame_index, ++ frame->image->image_id, frame->index); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ } ++ ++ frame->ram_loading_in_progress = 0; ++ ++ // We exclude background frames from the time to load the frame. ++ Milliseconds loading_start = gr_now_ms(); ++ ++ // Load the frame data image. ++ Imlib_Image frame_data_image = NULL; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ GR_LOG("Loading image: %s\n", sanitized_filename(filename)); ++ if (frame->format == 100) ++ frame_data_image = imlib_load_image(filename); ++ if (frame->format == 32 || frame->format == 24) ++ frame_data_image = gr_load_raw_pixel_data(frame, filename); ++ debug_loaded_files_counter++; ++ ++ if (!frame_data_image) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, "error: could not load image: %s\n", ++ sanitized_filename(filename)); ++ } ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ imlib_context_set_image(frame_data_image); ++ int frame_data_width = imlib_image_get_width(); ++ int frame_data_height = imlib_image_get_height(); ++ ++ // Check that the size of the image we are loading does not exceed the ++ // limit. ++ if (frame_data_width * frame_data_height * 4 > ++ graphics_max_single_image_ram_size) { ++ if (frame->status != STATUS_RAM_LOADING_ERROR) { ++ fprintf(stderr, ++ "error: image %u frame %u is too big too load: " ++ "%d x %d * 4 = %d > %u\n", ++ frame->image->image_id, frame->index, ++ frame_data_width, frame_data_height, ++ frame_data_width * frame_data_height * 4, ++ graphics_max_single_image_ram_size); ++ } ++ imlib_free_image(); ++ frame->status = STATUS_RAM_LOADING_ERROR; ++ return; ++ } ++ ++ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width, ++ frame_data_height); ++ // If imlib loading succeeded, and it is the first frame, set the ++ // information about the original image size, unless it's already set. ++ if (frame->index == 1 && frame->image->pix_width == 0 && ++ frame->image->pix_height == 0) { ++ frame->image->pix_width = frame_data_width; ++ frame->image->pix_height = frame_data_height; ++ } ++ ++ int image_width = frame->image->pix_width; ++ int image_height = frame->image->pix_height; ++ ++ // Compose the image with the background color or frame. ++ if (frame->background_color != 0 || bg_frame || ++ image_width != frame_data_width || ++ image_height != frame_data_height) { ++ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n", ++ frame->background_color, frame->background_frame_index); ++ Imlib_Image composed_image = imlib_create_image( ++ image_width, image_height); ++ imlib_context_set_image(composed_image); ++ imlib_image_set_has_alpha(1); ++ imlib_context_set_anti_alias(0); ++ ++ // Start with the background frame or color. ++ imlib_context_set_blend(0); ++ if (bg_frame && bg_frame->imlib_object) { ++ imlib_blend_image_onto_image( ++ bg_frame->imlib_object, 1, 0, 0, ++ image_width, image_height, 0, 0, ++ image_width, image_height); ++ } else { ++ int r = (frame->background_color >> 24) & 0xFF; ++ int g = (frame->background_color >> 16) & 0xFF; ++ int b = (frame->background_color >> 8) & 0xFF; ++ int a = frame->background_color & 0xFF; ++ imlib_context_set_color(r, g, b, a); ++ imlib_image_fill_rectangle(0, 0, image_width, ++ image_height); ++ } ++ ++ // Blend the frame data image onto the background. ++ imlib_context_set_blend(1); ++ imlib_blend_image_onto_image( ++ frame_data_image, 1, 0, 0, frame->data_pix_width, ++ frame->data_pix_height, frame->x, frame->y, ++ frame->data_pix_width, frame->data_pix_height); ++ ++ // Free the frame data image. ++ imlib_context_set_image(frame_data_image); ++ imlib_free_image(); ++ ++ frame_data_image = composed_image; ++ } ++ ++ frame->imlib_object = frame_data_image; ++ ++ images_ram_size += gr_frame_current_ram_size(frame); ++ frame->status = STATUS_RAM_LOADING_SUCCESS; ++ ++ Milliseconds loading_end = gr_now_ms(); ++ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB) Took " ++ "%ld ms\n", ++ frame->image->image_id, frame->index, images_ram_size / 1024, ++ gr_frame_current_ram_size(frame) / 1024, ++ loading_end - loading_start); ++} ++ ++/// Premultiplies the alpha channel of the image data. The data is an array of ++/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB. ++static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) { ++ for (size_t i = 0; i < num_pixels; ++i) { ++ DATA32 pixel = data[i]; ++ unsigned char a = pixel >> 24; ++ if (a == 0) { ++ data[i] = 0; ++ } else if (a != 255) { ++ unsigned char b = (pixel & 0xFF) * a / 255; ++ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255; ++ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255; ++ data[i] = (a << 24) | (r << 16) | (g << 8) | b; ++ } ++ } ++} ++ ++/// Computes the pixmap transformation, which is essentially the destination ++/// rectangle for drawing an image placement in a box of size `cols*cw` x ++/// `rows*ch`. The destination rectangle depends on the parameters of the ++/// placement, like scaling mode. The computed transformation is stored in the ++/// placement. ++void gr_compute_pixmap_transformation(ImagePlacement *placement) { ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ ++ // The size of the box in which the image is placed. ++ int box_w = (int)placement->cols * placement->scaled_cw; ++ int box_h = (int)placement->rows * placement->scaled_ch; ++ ++ int src_w = placement->src_pix_width; ++ int src_h = placement->src_pix_height; ++ ++ // Whether the box is too small to use the true size of the image. ++ char box_too_small = box_w < src_w || box_h < src_h; ++ char mode = placement->scale_mode; ++ ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ ++ if (src_w <= 0 || src_h <= 0) { ++ tr->dst_x = tr->dst_y = tr->dst_w = tr->dst_h = 0; ++ } else if (mode == SCALE_MODE_FILL) { ++ tr->dst_x = tr->dst_y = 0; ++ tr->dst_w = box_w; ++ tr->dst_h = box_h; ++ } else if (mode == SCALE_MODE_NONE || ++ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) { ++ tr->dst_x = tr->dst_y = 0; ++ tr->dst_w = src_w; ++ tr->dst_h = src_h; ++ } else { ++ if (mode != SCALE_MODE_CONTAIN && ++ mode != SCALE_MODE_NONE_OR_CONTAIN) { ++ fprintf(stderr, ++ "warning: unknown scale mode %u, using " ++ "'contain' instead\n", ++ mode); ++ } ++ if (box_w * src_h > src_w * box_h) { ++ // If the box is wider than the original image, fit to ++ // height. ++ tr->dst_h = box_h; ++ tr->dst_y = 0; ++ tr->dst_w = src_w * box_h / src_h; ++ tr->dst_x = (box_w - tr->dst_w) / 2; ++ } else { ++ // Otherwise, fit to width. ++ tr->dst_w = box_w; ++ tr->dst_x = 0; ++ tr->dst_h = src_h * box_w / src_w; ++ tr->dst_y = (box_h - tr->dst_h) / 2; ++ } ++ } ++ ++ // Make sure that the size of the destination image is non-zero. ++ tr->dst_w = MAX(1, tr->dst_w); ++ tr->dst_h = MAX(1, tr->dst_h); ++ ++ // Normally we want the pixmap to be exactly the size of the destination ++ // rectangle. ++ tr->pixmap_w = tr->dst_w; ++ tr->pixmap_h = tr->dst_h; ++ // However, if the pixmap would be larger than the source image, use the ++ // source image size. The upscaling will be done by XRender then. ++ if (tr->pixmap_w * tr->pixmap_h > src_w * src_h) { ++ tr->pixmap_w = MAX(1, src_w); ++ tr->pixmap_h = MAX(1, src_h); ++ } ++ ++ // If the pixmap would be over the limit, scale it down. ++ if (tr->pixmap_w * tr->pixmap_h * 4 > ++ graphics_max_single_image_ram_size) { ++ double scale = sqrt((double)graphics_max_single_image_ram_size / ++ (tr->pixmap_w * tr->pixmap_h * 4)); ++ tr->pixmap_w = MAX(1, (int)(tr->pixmap_w * scale)); ++ tr->pixmap_h = MAX(1, (int)(tr->pixmap_h * scale)); ++ } ++} ++ ++/// Creates and returns a scaled imlib image for the image placement. The ++/// original imlib object must be loaded and the placement size must be inferred ++/// by the caller (by calling `gr_compute_pixmap_transformation`). The caller is ++/// responsible for freeing the returned image. ++Imlib_Image gr_create_scaled_image_object(ImagePlacement *placement, ++ ImageFrame *frame) { ++ // The source rectangle (inside the original image). ++ int src_x = placement->src_pix_x; ++ int src_y = placement->src_pix_y; ++ int src_w = placement->src_pix_width; ++ int src_h = placement->src_pix_height; ++ // The pixmap dimensions. ++ int pixmap_w = placement->pixmap_transformation.pixmap_w; ++ int pixmap_h = placement->pixmap_transformation.pixmap_h; ++ ++ if (pixmap_w * pixmap_h * 4 > graphics_max_single_image_ram_size) { ++ fprintf(stderr, ++ "error: placement %u/%u would be too big to load: %d x " ++ "%d x 4 > %u\n", ++ placement->image->image_id, placement->placement_id, ++ pixmap_w, pixmap_h, graphics_max_single_image_ram_size); ++ return 0; ++ } ++ ++ if (pixmap_w == 0 || pixmap_h == 0) ++ fprintf(stderr, "warning: image of zero size\n"); ++ ++ imlib_context_set_image(frame->imlib_object); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ ++ return imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, ++ pixmap_w, pixmap_h); ++} ++ ++/// Creates a pixmap for the frame of an image placement. The pixmap contains ++/// the image data correctly scaled and fit to the box defined by the number of ++/// rows/columns of the image placement and the provided cell dimensions in ++/// pixels. If the placement is already loaded, it will be reloaded only if the ++/// cell dimensions have changed. ++Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) { ++ Milliseconds loading_start = gr_now_ms(); ++ Image *img = placement->image; ++ ImageFrame *frame = gr_get_frame(img, frameidx); ++ ++ // Update the atime unconditionally. ++ gr_touch_placement(placement); ++ if (frame) ++ gr_touch_frame(frame); ++ ++ // If cw or ch are different, unload all the pixmaps and recompute the ++ // pixmap geometry and transformation (shared for all pixmaps). ++ if (placement->scaled_cw != cw || placement->scaled_ch != ch) { ++ gr_unload_placement(placement); ++ placement->scaled_cw = cw; ++ placement->scaled_ch = ch; ++ gr_compute_pixmap_transformation(placement); ++ } ++ ++ // If it's already loaded, do nothing. ++ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx); ++ if (pixmap) ++ return pixmap; ++ ++ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id, ++ placement->placement_id, frameidx); ++ ++ if (placement->pixmap_transformation.pixmap_w == 0 || ++ placement->pixmap_transformation.pixmap_h == 0) { ++ GR_LOG("Not loading because the pixmap size is zero\n"); ++ return 0; ++ } ++ ++ // Load the imlib object for the frame. ++ if (!frame) { ++ fprintf(stderr, ++ "error: could not find frame %u for image %u\n", ++ frameidx, img->image_id); ++ return 0; ++ } ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) ++ return 0; ++ ++ // Create the scaled image. This is temporary, we will upload to the X ++ // server, and then delete immediately. ++ Imlib_Image scaled_image = ++ gr_create_scaled_image_object(placement, frame); ++ if (!scaled_image) ++ return 0; ++ imlib_context_set_image(scaled_image); ++ int pixmap_w = imlib_image_get_width(); ++ int pixmap_h = imlib_image_get_height(); ++ ++ // XRender needs the alpha channel premultiplied. ++ DATA32 *data = imlib_image_get_data(); ++ gr_premultiply_alpha(data, pixmap_w * pixmap_h); ++ ++ // Upload the image to the X server. ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ Colormap cmap = imlib_context_get_colormap(); ++ Drawable drawable = imlib_context_get_drawable(); ++ if (!drawable) ++ drawable = DefaultRootWindow(disp); ++ pixmap = XCreatePixmap(disp, drawable, pixmap_w, pixmap_h, 32); ++ XVisualInfo visinfo = {0}; ++ Status visual_found = XMatchVisualInfo(disp, DefaultScreen(disp), 32, ++ TrueColor, &visinfo) || ++ XMatchVisualInfo(disp, DefaultScreen(disp), 24, ++ TrueColor, &visinfo); ++ if (!visual_found) { ++ fprintf(stderr, ++ "error: could not find 32-bit TrueColor visual\n"); ++ // Proceed anyway. ++ visinfo.visual = NULL; ++ } ++ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0, ++ (char *)data, pixmap_w, pixmap_h, 32, 0); ++ if (!ximage) { ++ fprintf(stderr, "error: could not create XImage\n"); ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ return 0; ++ } ++ GC gc = XCreateGC(disp, pixmap, 0, NULL); ++ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, pixmap_w, pixmap_h); ++ XFreeGC(disp, gc); ++ // XDestroyImage will free the data as well, but it is managed by imlib, ++ // so set it to NULL. ++ ximage->data = NULL; ++ XDestroyImage(ximage); ++ imlib_image_put_back_data(data); ++ imlib_free_image(); ++ ++ // Assign the pixmap to the frame and increase the ram size. ++ gr_set_frame_pixmap(placement, frameidx, pixmap); ++ images_ram_size += gr_placement_single_frame_ram_size(placement); ++ debug_loaded_pixmaps_counter++; ++ ++ Milliseconds loading_end = gr_now_ms(); ++ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u " ++ "KiB) Took %ld ms\n", ++ frame->image->image_id, placement->placement_id, frame->index, ++ images_ram_size / 1024, ++ gr_placement_single_frame_ram_size(placement) / 1024, ++ loading_end - loading_start); ++ ++ // Free up ram if needed, but keep the pixmap we've loaded no matter ++ // what. ++ placement->protected_frame = frameidx; ++ gr_check_limits(); ++ placement->protected_frame = 0; ++ ++ return pixmap; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Initialization and deinitialization. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Creates a temporary directory. ++static int gr_create_cache_dir() { ++ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir)); ++ if (!mkdtemp(cache_dir)) { ++ fprintf(stderr, ++ "error: could not create temporary dir from template " ++ "%s\n", ++ sanitized_filename(cache_dir)); ++ return 0; ++ } ++ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir); ++ return 1; ++} ++ ++/// Checks whether `tmp_dir` exists and recreates it if it doesn't. ++static void gr_make_sure_tmpdir_exists() { ++ struct stat st; ++ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode)) ++ return; ++ fprintf(stderr, ++ "error: %s is not a directory, will need to create a new " ++ "graphics cache directory\n", ++ sanitized_filename(cache_dir)); ++ gr_create_cache_dir(); ++} ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm) { ++ // Set the initialization time. ++ clock_gettime(CLOCK_MONOTONIC, &initialization_time); ++ ++ // Create the temporary dir. ++ if (!gr_create_cache_dir()) ++ abort(); ++ ++ // Initialize imlib. ++ imlib_context_set_display(disp); ++ imlib_context_set_visual(vis); ++ imlib_context_set_colormap(cm); ++ imlib_context_set_anti_alias(1); ++ imlib_context_set_blend(1); ++ // Imlib2 checks only the file name when caching, which is not enough ++ // for us since we reuse file names. Disable caching. ++ imlib_set_cache_size(0); ++ ++ // Prepare for color inversion. ++ for (size_t i = 0; i < 256; ++i) ++ reverse_table[i] = 255 - i; ++ ++ // Create data structures. ++ images = kh_init(id2image); ++ kv_init(next_redraw_times); ++ ++ atexit(gr_deinit); ++} ++ ++/// Deinitialize the graphics module. ++void gr_deinit() { ++ // Remove the cache dir. ++ remove(cache_dir); ++ kv_destroy(next_redraw_times); ++ if (images) { ++ // Delete all images. ++ gr_delete_all_images(); ++ // Destroy the data structures. ++ kh_destroy(id2image, images); ++ images = NULL; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Dumping, debugging, and image preview. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Returns a string containing a time difference in a human-readable format. ++/// Uses a static buffer, so be careful. ++static const char *gr_ago(Milliseconds diff) { ++ static char result[32]; ++ double seconds = (double)diff / 1000.0; ++ if (seconds < 1) ++ snprintf(result, sizeof(result), "%.2f sec ago", seconds); ++ else if (seconds < 60) ++ snprintf(result, sizeof(result), "%d sec ago", (int)seconds); ++ else if (seconds < 3600) ++ snprintf(result, sizeof(result), "%d min %d sec ago", ++ (int)(seconds / 60), (int)(seconds) % 60); ++ else { ++ snprintf(result, sizeof(result), "%d hr %d min %d sec ago", ++ (int)(seconds / 3600), (int)(seconds) % 3600 / 60, ++ (int)(seconds) % 60); ++ } ++ return result; ++} ++ ++/// Prints to `file` with an indentation of `ind` spaces. ++static void fprintf_ind(FILE *file, int ind, const char *format, ...) { ++ fprintf(file, "%*s", ind, ""); ++ va_list args; ++ va_start(args, format); ++ vfprintf(file, format, args); ++ va_end(args); ++} ++ ++/// Dumps the image info to `file` with an indentation of `ind` spaces. ++static void gr_dump_image_info(FILE *file, Image *img, int ind) { ++ if (!img) { ++ fprintf_ind(file, ind, "Image is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Image %u\n", img->image_id); ++ ind += 4; ++ fprintf_ind(file, ind, "number: %u\n", img->image_number); ++ fprintf_ind(file, ind, "global command index: %lu\n", ++ img->global_command_index); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime, ++ gr_ago(now - img->atime)); ++ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width, ++ img->pix_height); ++ fprintf_ind(file, ind, "cur frame start time: %ld %s\n", ++ img->current_frame_time, ++ gr_ago(now - img->current_frame_time)); ++ if (img->next_redraw) ++ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n", ++ img->next_redraw, img->next_redraw - now); ++ fprintf_ind(file, ind, "total disk size: %u KiB\n", ++ img->total_disk_size / 1024); ++ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration); ++ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img)); ++ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame); ++ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state); ++ fprintf_ind(file, ind, "default_placement: %u\n", ++ img->default_placement); ++} ++ ++/// Dumps the frame info to `file` with an indentation of `ind` spaces. ++static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) { ++ if (!frame) { ++ fprintf_ind(file, ind, "Frame is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Frame %d\n", frame->index); ++ ind += 4; ++ if (frame->index == 0) { ++ fprintf_ind(file, ind, "NOT INITIALIZED\n"); ++ return; ++ } ++ if (frame->original_filename) { ++ fprintf_ind(file, ind, "original filename (sanitized): %s\n", ++ sanitized_filename(frame->original_filename)); ++ fprintf_ind(file, ind, "original file %s\n", ++ gr_is_original_file_still_available(frame) ++ ? "is still available" ++ : "is NOT available anymore"); ++ } ++ if (frame->uploading_failure) ++ fprintf_ind(file, ind, "uploading failure: %s\n", ++ image_uploading_failure_strings ++ [frame->uploading_failure]); ++ fprintf_ind(file, ind, "gap: %d\n", frame->gap); ++ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime, ++ gr_ago(now - frame->atime)); ++ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width, ++ frame->data_pix_height); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (access(filename, F_OK) != -1) ++ fprintf_ind(file, ind, "file: %s\n", ++ sanitized_filename(filename)); ++ else ++ fprintf_ind(file, ind, "not on disk\n"); ++ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024); ++ if (frame->imlib_object) { ++ unsigned ram_size = gr_frame_current_ram_size(frame); ++ fprintf_ind(file, ind, ++ "loaded into ram, size: %d " ++ "KiB\n", ++ ram_size / 1024); ++ } else { ++ fprintf_ind(file, ind, "not loaded into ram\n"); ++ } ++} ++ ++/// Dumps the placement info to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_info(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) { ++ fprintf_ind(file, ind, "Placement is NULL\n"); ++ return; ++ } ++ Milliseconds now = gr_now_ms(); ++ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id); ++ ind += 4; ++ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime, ++ gr_ago(now - placement->atime)); ++ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode); ++ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols, ++ placement->rows); ++ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw, ++ placement->scaled_ch); ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ fprintf_ind(file, ind, "pixmap size: %ux%u\n", tr->pixmap_w, ++ tr->pixmap_h); ++ fprintf_ind(file, ind, "dst size: %ux%u offset: (%d, %d)\n", tr->dst_w, ++ tr->dst_h, tr->dst_x, tr->dst_y); ++ fprintf_ind(file, ind, "ram per frame: %u KiB\n", ++ gr_placement_single_frame_ram_size(placement) / 1024); ++ unsigned ram_size = gr_placement_current_ram_size(placement); ++ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024); ++} ++ ++/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces. ++static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement, ++ int ind) { ++ if (!placement) ++ return; ++ int frameidx = 1; ++ foreach_pixmap(*placement, pixmap, { ++ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx, ++ pixmap); ++ ++frameidx; ++ }); ++} ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state() { ++ FILE *file = stderr; ++ int ind = 0; ++ fprintf_ind(file, ind, "======= Graphics module state dump =======\n"); ++ fprintf_ind(file, ind, ++ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu " ++ "sizeof(ImagePlacement) = %lu\n", ++ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement)); ++ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images)); ++ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count); ++ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n", ++ images_ram_size / 1024); ++ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n", ++ images_disk_size / 1024); ++ ++ Milliseconds now = gr_now_ms(); ++ ++ int64_t images_ram_size_computed = 0; ++ int64_t images_disk_size_computed = 0; ++ ++ Image *img = NULL; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(images, img, { ++ fprintf_ind(file, ind, "----------------\n"); ++ gr_dump_image_info(file, img, 0); ++ int64_t total_disk_size_computed = 0; ++ int total_duration_computed = 0; ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ if (frame->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ total_duration_computed += frame->gap; ++ images_disk_size_computed += frame->disk_size; ++ total_disk_size_computed += frame->disk_size; ++ if (frame->imlib_object) ++ images_ram_size_computed += ++ gr_frame_current_ram_size(frame); ++ }); ++ if (img->total_disk_size != total_disk_size_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_disk_size is %u, but " ++ "computed value is %ld\n", ++ img->total_disk_size, total_disk_size_computed); ++ } ++ if (img->total_duration != total_duration_computed) { ++ fprintf_ind(file, ind, ++ " ERROR: total_duration is %d, but computed " ++ "value is %d\n", ++ img->total_duration, total_duration_computed); ++ } ++ kh_foreach_value(img->placements, placement, { ++ gr_dump_placement_info(file, placement, 4); ++ if (placement->image != img) ++ fprintf_ind(file, 8, ++ "ERROR: WRONG IMAGE POINTER\n"); ++ fprintf_ind(file, 8, ++ "Pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 12); ++ unsigned ram_size = ++ gr_placement_current_ram_size(placement); ++ images_ram_size_computed += ram_size; ++ }); ++ }); ++ if (images_ram_size != images_ram_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_ram_size is %ld, but computed value " ++ "is %ld\n", ++ images_ram_size, images_ram_size_computed); ++ } ++ if (images_disk_size != images_disk_size_computed) { ++ fprintf_ind(file, ind, ++ "ERROR: images_disk_size is %ld, but computed value " ++ "is %ld\n", ++ images_disk_size, images_disk_size_computed); ++ } ++ fprintf_ind(file, ind, "===========================================\n"); ++} ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++// TODO: Currently we do this for the first frame only. Not sure what to do with ++// animations. ++void gr_preview_image(uint32_t image_id, const char *exec) { ++ char command[256]; ++ size_t len; ++ Image *img = gr_find_image(image_id); ++ if (img) { ++ ImageFrame *frame = &img->first_frame; ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ if (frame->disk_size == 0) { ++ len = snprintf(command, 255, ++ "xmessage 'Image with id=%u is not " ++ "fully copied to %s'", ++ image_id, sanitized_filename(filename)); ++ } else { ++ len = snprintf(command, 255, "%s %s &", exec, ++ sanitized_filename(filename)); ++ } ++ } else { ++ len = snprintf(command, 255, ++ "xmessage 'Cannot find image with id=%u'", ++ image_id); ++ } ++ if (len > 255) { ++ fprintf(stderr, "error: command too long: %s\n", command); ++ snprintf(command, 255, "xmessage 'error: command too long'"); ++ } ++ if (system(command) != 0) { ++ fprintf(stderr, "error: could not execute command %s\n", ++ command); ++ } ++} ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable) { ++ char filename[MAX_FILENAME_SIZE]; ++ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id); ++ FILE *file = fopen(filename, "w"); ++ if (!file) { ++ perror("fopen"); ++ return; ++ } ++ // Basic information about the cell. ++ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id); ++ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id); ++ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow); ++ fprintf(file, "classic/unicode placeholder = %s\n", ++ is_classic_placeholder ? "classic" : "unicode"); ++ fprintf(file, "original diacritic count = %d\n", diacritic_count); ++ // Information about the image and the placement. ++ Image *img = gr_find_image(image_id); ++ ImagePlacement *placement = gr_find_placement(img, placement_id); ++ gr_dump_image_info(file, img, 0); ++ gr_dump_placement_info(file, placement, 0); ++ // The text underneath this particular cell. ++ if (placement && placement->text_underneath && imgcol >= 1 && ++ imgrow >= 1 && imgcol <= placement->cols && ++ imgrow <= placement->rows) { ++ fprintf(file, "Glyph underneath:\n"); ++ Glyph *glyph = ++ &placement->text_underneath[(imgrow - 1) * ++ placement->cols + ++ imgcol - 1]; ++ fprintf(file, " rune = 0x%08X\n", glyph->u); ++ fprintf(file, " bg = 0x%08X\n", glyph->bg); ++ fprintf(file, " fg = 0x%08X\n", glyph->fg); ++ fprintf(file, " decor = 0x%08X\n", glyph->decor); ++ fprintf(file, " mode = 0x%08X\n", glyph->mode); ++ } ++ if (img) { ++ fprintf(file, "Frames:\n"); ++ foreach_frame(*img, frame, { ++ gr_dump_frame_info(file, frame, 4); ++ }); ++ } ++ if (placement) { ++ fprintf(file, "Placement pixmaps:\n"); ++ gr_dump_placement_pixmaps(file, placement, 4); ++ } ++ fclose(file); ++ char *argv[] = {st_executable, "-e", "less", filename, NULL}; ++ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) { ++ perror("posix_spawnp"); ++ return; ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Appending and displaying image rectangles. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// Displays debug information in the rectangle using colors col1 and col2. ++static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2, ++ const char *message) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ char info[MAX_INFO_LEN]; ++ if (rect->placement_id) ++ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->placement_id, ++ rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ else ++ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message, ++ rect->image_id, rect->img_start_col, rect->img_end_col, ++ rect->img_start_row, rect->img_end_row); ++ XSetForeground(disp, gc, col1); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 4, ++ rect->screen_y_pix + h_pix - 3, info, strlen(info)); ++ XSetForeground(disp, gc, col2); ++ XDrawString(disp, buf, gc, rect->screen_x_pix + 2, ++ rect->screen_y_pix + h_pix - 5, info, strlen(info)); ++ XFreeGC(disp, gc); ++} ++ ++/// Draws a rectangle (bounding box) for debugging. ++static void gr_showrect(Drawable buf, ImageRect *rect) { ++ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ XSetForeground(disp, gc, 0xFF00FF00); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix, ++ w_pix - 1, h_pix - 1); ++ XSetForeground(disp, gc, 0xFFFF0000); ++ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1, ++ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3); ++ XFreeGC(disp, gc); ++} ++ ++/// Updates the next redraw time for the given row. Resizes the ++/// next_redraw_times array if needed. ++static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) { ++ if (next_redraw == 0) ++ return; ++ if (row >= kv_size(next_redraw_times)) { ++ size_t old_size = kv_size(next_redraw_times); ++ kv_a(Milliseconds, next_redraw_times, row); ++ for (size_t i = old_size; i <= row; ++i) ++ kv_A(next_redraw_times, i) = 0; ++ } ++ Milliseconds old_value = kv_A(next_redraw_times, row); ++ if (old_value == 0 || old_value > next_redraw) ++ kv_A(next_redraw_times, row) = next_redraw; ++} ++ ++/// Draws the given part of an image. ++static void gr_drawimagerect(Drawable buf, ImageRect *rect) { ++ ImagePlacement *placement = ++ gr_find_image_and_placement(rect->image_id, rect->placement_id); ++ // If the image does not exist or image display is switched off, draw ++ // the bounding box. ++ if (!placement || !graphics_display_images) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ Image *img = placement->image; ++ ++ if (img->last_redraw < drawing_start_time) { ++ // This is the first time we draw this image in this redraw ++ // cycle. Update the frame index we are going to display. Note ++ // that currently all image placements are synchronized. ++ int old_frame = img->current_frame; ++ gr_update_frame_index(img, drawing_start_time); ++ img->last_redraw = drawing_start_time; ++ } ++ ++ // Adjust next redraw times for the rows of this image rect. ++ if (img->next_redraw) { ++ for (int row = rect->screen_y_row; ++ row <= rect->screen_y_row + rect->img_end_row - ++ rect->img_start_row - 1; ++row) { ++ gr_update_next_redraw_time( ++ row, img->next_redraw); ++ } ++ } ++ ++ // Load the frame. ++ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw, ++ rect->ch); ++ ++ // If the image couldn't be loaded, display the bounding box. ++ if (!pixmap) { ++ gr_showrect(buf, rect); ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ return; ++ } ++ ++ // The coordinates and size (in pixels) of the src rectangle inside the ++ // box of cells (not the pixmap). ++ int src_x = rect->img_start_col * rect->cw; ++ int src_y = rect->img_start_row * rect->ch; ++ int src_w = (rect->img_end_col - rect->img_start_col) * rect->cw; ++ int src_h = (rect->img_end_row - rect->img_start_row) * rect->ch; ++ // The coordinates of the dst rectangle inside the window. The size is ++ // the same as the src size in the box (src_w, src_h). ++ int window_x = rect->screen_x_pix; ++ int window_y = rect->screen_y_pix; ++ ++ Display *disp = imlib_context_get_display(); ++ Visual *vis = imlib_context_get_visual(); ++ ++ // Create an xrender picture for the window. ++ XRenderPictFormat *win_format = ++ XRenderFindVisualFormat(disp, vis); ++ Picture window_pic = ++ XRenderCreatePicture(disp, buf, win_format, 0, NULL); ++ ++ // If needed, invert the image pixmap. Note that this naive approach of ++ // inverting the pixmap is not entirely correct, because the pixmap is ++ // premultiplied. But the result is good enough to visually indicate ++ // selection. ++ if (rect->reverse) { ++ unsigned pixmap_w = placement->pixmap_transformation.pixmap_w; ++ unsigned pixmap_h = placement->pixmap_transformation.pixmap_h; ++ Pixmap invpixmap = ++ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32); ++ XGCValues gcv = {.function = GXcopyInverted}; ++ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv); ++ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w, ++ pixmap_h, 0, 0); ++ XFreeGC(disp, gc); ++ pixmap = invpixmap; ++ } ++ ++ // Create a picture for the image pixmap. ++ XRenderPictFormat *pic_format = ++ XRenderFindStandardFormat(disp, PictStandardARGB32); ++ // We use RepeatPad to avoid bilinear filtering halo. ++ XRenderPictureAttributes attrs = {0}; ++ attrs.repeat = RepeatPad; ++ Picture pixmap_pic = XRenderCreatePicture(disp, pixmap, pic_format, ++ CPRepeat, &attrs); ++ ++ // Since the pixmap may be of different size than the destination box of ++ // cells, we must apply a transformation to it. ++ // The XTransform structure describes a matrix to transform the ++ // destination picture coordinates (i.e. in the box) to the source ++ // coordinates (i.e. in the pixmap). We apply only scaling (translation ++ // will be applied to the src coordinates directly): ++ // ++ // pixmap_x = picture_x * pixmap_w / dst_w ++ // pixmap_y = picture_y * pixmap_h / dst_h ++ // ++ // Where dst_w, dst_h, pixmap_w, pixmap_h are from the placement's ++ // pixmap_transformation structure. ++ PixmapTransformation *tr = &placement->pixmap_transformation; ++ double xs = (double)tr->pixmap_w / MAX(tr->dst_w, 1); ++ double ys = (double)tr->pixmap_h / MAX(tr->dst_h, 1); ++ // clang-format off ++ XTransform xform = {{ ++ { XDoubleToFixed(xs), XDoubleToFixed( 0), XDoubleToFixed( 0) }, ++ { XDoubleToFixed( 0), XDoubleToFixed(ys), XDoubleToFixed( 0) }, ++ { XDoubleToFixed( 0), XDoubleToFixed( 0), XDoubleToFixed( 1) } ++ }}; ++ // clang-format on ++ XRenderSetPictureTransform(disp, pixmap_pic, &xform); ++ XRenderSetPictureFilter(disp, pixmap_pic, FilterBilinear, NULL, 0); ++ ++ // Do the translation: modify the src coordinates to make them ++ // coordinates into the picture rather than into the box. ++ src_x -= tr->dst_x; ++ src_y -= tr->dst_y; ++ // At this point src_x, src_y, src_w, src_h are coordinates of the ++ // source rectangle inside the picture (scaled pixmap). ++ ++ // Now do clipping. If src coordinates are negative, adjust the dst ++ // coordinates (into the window) and the width/height. We do clipping ++ // instead of using the values as is to avoid rendering the pad area ++ // outside the pixmap. ++ if (src_x < 0) { ++ window_x += -src_x; ++ src_w -= -src_x; ++ src_x = 0; ++ } ++ if (src_y < 0) { ++ window_y += -src_y; ++ src_h -= -src_y; ++ src_y = 0; ++ } ++ ++ // Adjust width and height if the src rectangle exceeds the picture. ++ src_w = MIN(src_w, tr->dst_w - src_x); ++ src_h = MIN(src_h, tr->dst_h - src_y); ++ ++ // Composite the image onto the window. In the reverse mode we ignore ++ // the alpha channel of the image because the naive inversion above ++ // seems to invert the alpha channel as well. ++ int pictop = rect->reverse ? PictOpSrc : PictOpOver; ++ if (src_w > 0 && src_h > 0) ++ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic, src_x, ++ src_y, src_x, src_y, window_x, window_y, src_w, ++ src_h); ++ ++ // Free resources ++ XRenderFreePicture(disp, pixmap_pic); ++ XRenderFreePicture(disp, window_pic); ++ if (rect->reverse) ++ XFreePixmap(disp, pixmap); ++ ++ // In debug mode always draw bounding boxes and print info. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) { ++ gr_showrect(buf, rect); ++ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, ""); ++ } ++} ++ ++/// Removes the given image rectangle. ++static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); } ++ ++/// Returns the bottom coordinate of the rect. ++static int gr_getrectbottom(ImageRect *rect) { ++ return rect->screen_y_pix + ++ (rect->img_end_row - rect->img_start_row) * rect->ch; ++} ++ ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch) { ++ current_cw = cw; ++ current_ch = ch; ++ debug_loaded_files_counter = 0; ++ debug_loaded_pixmaps_counter = 0; ++ drawing_start_time = gr_now_ms(); ++ imlib_context_set_drawable(buf); ++} ++ ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf) { ++ // Draw and then delete all known image rectangles. ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!rect->image_id) ++ continue; ++ gr_drawimagerect(buf, rect); ++ gr_freerect(rect); ++ } ++ ++ // Compute the delay until the next redraw as the minimum of the next ++ // redraw delays for all rows. ++ Milliseconds drawing_end_time = gr_now_ms(); ++ graphics_next_redraw_delay = INT_MAX; ++ for (int row = 0; row < kv_size(next_redraw_times); ++row) { ++ Milliseconds row_next_redraw = kv_A(next_redraw_times, row); ++ if (row_next_redraw > 0) { ++ int delay = MAX(graphics_animation_min_delay, ++ row_next_redraw - drawing_end_time); ++ graphics_next_redraw_delay = ++ MIN(graphics_next_redraw_delay, delay); ++ } ++ } ++ ++ // In debug mode display additional info. ++ if (graphics_debug_mode) { ++ int milliseconds = drawing_end_time - drawing_start_time; ++ ++ Display *disp = imlib_context_get_display(); ++ GC gc = XCreateGC(disp, buf, 0, NULL); ++ const char *debug_mode_str = ++ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES ++ ? "(boxes shown) " ++ : ""; ++ int redraw_delay = graphics_next_redraw_delay == INT_MAX ++ ? -1 ++ : graphics_next_redraw_delay; ++ char info[MAX_INFO_LEN]; ++ snprintf(info, MAX_INFO_LEN, ++ "%sRender time: %d ms ram %ld K disk %ld K count " ++ "%d cell %dx%d delay %d", ++ debug_mode_str, milliseconds, images_ram_size / 1024, ++ images_disk_size / 1024, kh_size(images), current_cw, ++ current_ch, redraw_delay); ++ XSetForeground(disp, gc, 0xFF000000); ++ XFillRectangle(disp, buf, gc, 0, 0, 600, 16); ++ XSetForeground(disp, gc, 0xFFFFFFFF); ++ XDrawString(disp, buf, gc, 0, 14, info, strlen(info)); ++ XFreeGC(disp, gc); ++ ++ if (milliseconds > 0) { ++ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n", ++ info, debug_loaded_files_counter, ++ debug_loaded_pixmaps_counter); ++ } ++ } ++ ++ // Check the limits in case we have used too much ram for placements. ++ gr_check_limits(); ++} ++ ++// Add an image rectangle to the list of rectangles to draw. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse) { ++ current_cw = cw; ++ current_ch = ch; ++ ++ ImageRect new_rect; ++ new_rect.image_id = image_id; ++ new_rect.placement_id = placement_id; ++ new_rect.img_start_col = img_start_col; ++ new_rect.img_end_col = img_end_col; ++ new_rect.img_start_row = img_start_row; ++ new_rect.img_end_row = img_end_row; ++ new_rect.screen_y_row = y_row; ++ new_rect.screen_x_pix = x_pix; ++ new_rect.screen_y_pix = y_pix; ++ new_rect.ch = ch; ++ new_rect.cw = cw; ++ new_rect.reverse = reverse; ++ ++ // Display some red text in debug mode. ++ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) ++ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? "); ++ ++ // If it's the empty image (image_id=0) or an empty rectangle, do ++ // nothing. ++ if (image_id == 0 || img_end_col - img_start_col <= 0 || ++ img_end_row - img_start_row <= 0) ++ return; ++ // Try to find a rect to merge with. ++ ImageRect *free_rect = NULL; ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (rect->image_id == 0) { ++ if (!free_rect) ++ free_rect = rect; ++ continue; ++ } ++ if (rect->image_id != image_id || ++ rect->placement_id != placement_id || rect->cw != cw || ++ rect->ch != ch || rect->reverse != reverse) ++ continue; ++ // We only support the case when the new stripe is added to the ++ // bottom of an existing rectangle and they are perfectly ++ // aligned. ++ if (rect->img_end_row == img_start_row && ++ gr_getrectbottom(rect) == y_pix) { ++ if (rect->img_start_col == img_start_col && ++ rect->img_end_col == img_end_col && ++ rect->screen_x_pix == x_pix) { ++ rect->img_end_row = img_end_row; ++ return; ++ } ++ } ++ } ++ // If we haven't merged the new rect with any existing rect, and there ++ // is no free rect, we have to render one of the existing rects. ++ if (!free_rect) { ++ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) { ++ ImageRect *rect = &image_rects[i]; ++ if (!free_rect || gr_getrectbottom(free_rect) > ++ gr_getrectbottom(rect)) ++ free_rect = rect; ++ } ++ gr_drawimagerect(buf, free_rect); ++ gr_freerect(free_rect); ++ } ++ // Start a new rectangle in `free_rect`. ++ *free_rect = new_rect; ++} ++ ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows) { ++ if (rows < kv_size(next_redraw_times)) ++ kv_size(next_redraw_times) = rows; ++ if (rows * 2 < kv_max(next_redraw_times)) ++ kv_resize(Milliseconds, next_redraw_times, rows); ++ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) { ++ if (dirty[i]) { ++ kv_A(next_redraw_times, i) = 0; ++ continue; ++ } ++ Milliseconds next_update = kv_A(next_redraw_times, i); ++ if (next_update > 0 && next_update <= drawing_start_time) { ++ dirty[i] = 1; ++ kv_A(next_redraw_times, i) = 0; ++ } ++ } ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Command parsing and handling. ++//////////////////////////////////////////////////////////////////////////////// ++ ++/// A parsed kitty graphics protocol command. ++typedef struct { ++ /// The command itself, without the 'G'. ++ char *command; ++ /// The payload (after ';'). ++ char *payload; ++ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'. ++ char action; ++ /// 'q=', 1 to suppress OK response, 2 to suppress errors too. ++ int quiet; ++ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with ++ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to ++ /// 32-bit pixel data. ++ int format; ++ /// 'o=', may be 'z' for RFC 1950 ZLIB. ++ int compression; ++ /// 't=', may be 'f', 't' or 'd'. ++ char transmission_medium; ++ /// 'd=' ++ char delete_specifier; ++ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'. ++ /// When 'a=f', this is the size of the frame rectangle when composed on ++ /// top of another frame. ++ int frame_pix_width, frame_pix_height; ++ /// 'x=', 'y=' - top-left corner of the source rectangle. ++ int src_pix_x, src_pix_y; ++ /// 'w=', 'h=' - width and height of the source rectangle. ++ int src_pix_width, src_pix_height; ++ /// 'r=', 'c=' ++ int rows, columns; ++ /// 'i=' ++ uint32_t image_id; ++ /// 'I=' ++ uint32_t image_number; ++ /// 'p=' ++ uint32_t placement_id; ++ /// 'm=', may be 0 or 1. ++ int more; ++ /// True if turns out that this command is a continuation of a data ++ /// transmission and not the first one for this image. Populated by ++ /// `gr_handle_transmit_command`. ++ char is_direct_transmission_continuation; ++ /// 'S=', used to check the size of uploaded data. ++ int size; ++ /// The offset of the frame image data in the shared memory ('O='). ++ unsigned offset; ++ /// 'U=', whether it's a virtual placement for Unicode placeholders. ++ int virtual; ++ /// 'C=', if true, do not move the cursor when displaying this placement ++ /// (non-virtual placements only). ++ char do_not_move_cursor; ++ // --------------------------------------------------------------------- ++ // Animation-related fields. Their keys often overlap with keys of other ++ // commands, so these make sense only if the action is 'a=f' (frame ++ // transmission) or 'a=a' (animation control). ++ // ++ // 'x=' and 'y=', the relative position of the frame image when it's ++ // composed on top of another frame. ++ int frame_dst_pix_x, frame_dst_pix_y; ++ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of ++ /// the background color or frame. ++ char replace_instead_of_blending; ++ /// 'Y=', the background color in the 0xRRGGBBAA format (still ++ /// transmitted as a decimal number). ++ uint32_t background_color; ++ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame. ++ int background_frame; ++ /// (Only for 'a=a'). 'c=', sets the index of the current frame. ++ int current_frame; ++ /// 'r=', the 1-based index of the frame to edit. ++ int edit_frame; ++ /// 'z=', the duration of the frame. Zero if not specified, negative if ++ /// the frame is gapless (i.e. skipped). ++ int gap; ++ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the ++ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop. ++ int animation_state; ++ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the ++ /// animation will loop. 1 to loop infinitely, N to loop N-1 times. ++ int loops; ++} GraphicsCommand; ++ ++/// Replaces all non-printed characters in `str` with '?' and truncates the ++/// string to `max_size`, maybe inserting ellipsis at the end. ++static void sanitize_str(char *str, size_t max_size) { ++ assert(max_size >= 4); ++ for (size_t i = 0; i < max_size; ++i) { ++ unsigned c = str[i]; ++ if (c == '\0') ++ return; ++ if (c >= 128 || !isprint(c)) ++ str[i] = '?'; ++ } ++ str[max_size - 1] = '\0'; ++ str[max_size - 2] = '.'; ++ str[max_size - 3] = '.'; ++ str[max_size - 4] = '.'; ++} ++ ++/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be ++/// careful. ++static const char *sanitized_filename(const char *str) { ++ static char buf[MAX_FILENAME_SIZE]; ++ strncpy(buf, str, sizeof(buf)); ++ sanitize_str(buf, sizeof(buf)); ++ return buf; ++} ++ ++/// Creates a response to the current command in `graphics_command_result`. ++static void gr_createresponse(uint32_t image_id, uint32_t image_number, ++ uint32_t placement_id, const char *msg) { ++ if (!image_id && !image_number && !placement_id) { ++ // Nobody expects the response in this case, so just print it to ++ // stderr. ++ fprintf(stderr, ++ "error: No image id or image number or placement_id, " ++ "but still there is a response: %s\n", ++ msg); ++ return; ++ } ++ char *buf = graphics_command_result.response; ++ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN; ++ size_t written; ++ written = snprintf(buf, maxlen, "\033_G"); ++ buf += written; ++ maxlen -= written; ++ if (image_id) { ++ written = snprintf(buf, maxlen, "i=%u,", image_id); ++ buf += written; ++ maxlen -= written; ++ } ++ if (image_number) { ++ written = snprintf(buf, maxlen, "I=%u,", image_number); ++ buf += written; ++ maxlen -= written; ++ } ++ if (placement_id) { ++ written = snprintf(buf, maxlen, "p=%u,", placement_id); ++ buf += written; ++ maxlen -= written; ++ } ++ buf[-1] = ';'; ++ written = snprintf(buf, maxlen, "%s\033\\", msg); ++ buf += written; ++ maxlen -= written; ++ buf[-2] = '\033'; ++ buf[-1] = '\\'; ++} ++ ++/// Creates the 'OK' response to the current command, unless suppressed or a ++/// non-final data transmission. ++static void gr_reportsuccess_cmd(GraphicsCommand *cmd) { ++ if (cmd->quiet < 1 && !cmd->more) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, "OK"); ++} ++ ++/// Creates the 'OK' response to the current command (unless suppressed). ++static void gr_reportsuccess_frame(ImageFrame *frame) { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ if (frame->quiet < 1) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, "OK"); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command); ++ if (cmd->quiet < 2) ++ gr_createresponse(cmd->image_id, cmd->image_number, ++ cmd->placement_id, errmsg); ++} ++ ++/// Creates an error response to the current command (unless suppressed). ++static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) { ++ char errmsg[MAX_GRAPHICS_RESPONSE_LEN]; ++ graphics_command_result.error = 1; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args); ++ va_end(args); ++ ++ if (!frame) { ++ fprintf(stderr, "%s\n", errmsg); ++ gr_createresponse(0, 0, 0, errmsg); ++ } else { ++ uint32_t id = frame->image->query_id ? frame->image->query_id ++ : frame->image->image_id; ++ fprintf(stderr, "%s id=%u\n", errmsg, id); ++ if (frame->quiet < 2) ++ gr_createresponse(id, frame->image->image_number, ++ frame->image->initial_placement_id, ++ errmsg); ++ } ++} ++ ++/// Loads an image and creates a success/failure response. Returns `frame`, or ++/// NULL if it's a query action and the image was deleted. ++static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) { ++ gr_load_imlib_object(frame); ++ if (!frame->imlib_object) { ++ gr_reporterror_frame(frame, "EBADF: could not load image"); ++ } else { ++ gr_reportsuccess_frame(frame); ++ } ++ // If it was a query action, discard the image. ++ if (frame->image->query_id) { ++ gr_delete_image(frame->image); ++ return NULL; ++ } ++ return frame; ++} ++ ++/// Creates an appropriate uploading failure response to the current command. ++static void gr_reportuploaderror(ImageFrame *frame) { ++ switch (frame->uploading_failure) { ++ case 0: ++ return; ++ case ERROR_CANNOT_OPEN_CACHED_FILE: ++ gr_reporterror_frame(frame, ++ "EIO: could not create a file for image"); ++ break; ++ case ERROR_OVER_SIZE_LIMIT: ++ gr_reporterror_frame( ++ frame, ++ "EFBIG: the size of the uploaded image exceeded " ++ "the image size limit %u", ++ graphics_max_single_image_file_size); ++ break; ++ case ERROR_UNEXPECTED_SIZE: ++ gr_reporterror_frame(frame, ++ "EINVAL: the size of the uploaded image %u " ++ "doesn't match the expected size %u", ++ frame->disk_size, frame->expected_size); ++ break; ++ }; ++} ++ ++/// Displays a non-virtual placement. This functions records the information in ++/// `graphics_command_result`, the placeholder itself is created by the terminal ++/// after handling the current command in the graphics module. ++static void gr_display_nonvirtual_placement(ImagePlacement *placement) { ++ if (placement->virtual) ++ return; ++ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS) ++ return; ++ // Infer the placement size if needed. ++ gr_infer_placement_size_maybe(placement); ++ // Populate the information about the placeholder which will be created ++ // by the terminal. ++ graphics_command_result.create_placeholder = 1; ++ graphics_command_result.placeholder.image_id = placement->image->image_id; ++ graphics_command_result.placeholder.placement_id = placement->placement_id; ++ graphics_command_result.placeholder.columns = placement->cols; ++ graphics_command_result.placeholder.rows = placement->rows; ++ graphics_command_result.placeholder.do_not_move_cursor = ++ placement->do_not_move_cursor; ++ placement->text_underneath = ++ calloc(placement->rows * placement->cols, sizeof(Glyph)); ++ graphics_command_result.placeholder.text_underneath = ++ placement->text_underneath; ++ GR_LOG("Creating a placeholder for %u/%u %d x %d\n", ++ placement->image->image_id, placement->placement_id, ++ placement->cols, placement->rows); ++} ++ ++/// Marks the rows that are occupied by the image as dirty. ++static void gr_schedule_image_redraw(Image *img) { ++ if (!img) ++ return; ++ gr_schedule_image_redraw_by_id(img->image_id); ++} ++ ++/// Closes the file currently being uploaded. This doesn't necessarily finish ++/// the upload since the file may be reopened. ++static void gr_close_current_upload_file() { ++ Image *img = gr_find_image(current_upload_image_id); ++ ImageFrame *frame = gr_get_frame(img, current_upload_frame_index); ++ gr_close_disk_cache_file(frame); ++} ++ ++/// Sets the current image and frame being uploaded. Closes the previous upload ++/// file if it's changed. If `frame` is NULL, clears the current upload ++/// image/frame. ++static void gr_set_current_upload_frame(ImageFrame *frame) { ++ if (frame) { ++ if (current_upload_image_id != frame->image->image_id || ++ current_upload_frame_index != frame->index) { ++ gr_close_current_upload_file(); ++ } ++ current_upload_image_id = frame->image->image_id; ++ current_upload_frame_index = frame->index; ++ GR_LOG("Set current_upload_image_id = %u, " ++ "current_upload_frame_index = %u\n", ++ current_upload_image_id, current_upload_frame_index); ++ } else { ++ gr_close_current_upload_file(); ++ current_upload_image_id = 0; ++ current_upload_frame_index = 0; ++ GR_LOG("Set current_upload_image_id = 0\n"); ++ } ++} ++ ++/// Returns whether direct transmission continuation is allowed for the given ++/// command and frame. ++static int gr_transmission_continuation_is_allowed(GraphicsCommand *cmd, ++ ImageFrame *frame) { ++ if (!frame || frame->status != STATUS_UPLOADING) ++ return 0; ++ ++ // If it's the same image and frame as the current upload, allow it. ++ if (current_upload_image_id == frame->image->image_id && ++ current_upload_frame_index == frame->index) ++ return 1; ++ ++ // Otherwise it's a continuation of an interrupted upload. The kitty ++ // graphics protocol doesn't allow interleaving of direct transmission ++ // with other commands, so interrupted uploads must be aborted. However, ++ // we still allow it as an extension, because it's useful for ++ // protocol-unaware multiplexer. We check that there are no ++ // contradictions, and the time since the last upload activity is small. ++ ++ if (cmd->size && cmd->size != frame->expected_size) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of expected size mismatch\n"); ++ return 0; ++ } ++ if (cmd->format && cmd->format != frame->format) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of format mismatch\n"); ++ return 0; ++ } ++ if (cmd->compression && cmd->compression != frame->compression) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of compression mismatch\n"); ++ return 0; ++ } ++ if ((cmd->frame_pix_width && ++ cmd->frame_pix_width != frame->data_pix_width) || ++ (cmd->frame_pix_height && ++ cmd->frame_pix_height != frame->data_pix_height) || ++ (cmd->background_color && ++ cmd->background_color != frame->background_color) || ++ (cmd->background_frame && ++ cmd->background_frame != frame->background_frame_index) || ++ (cmd->gap && cmd->gap != frame->gap) || ++ (cmd->replace_instead_of_blending && ++ cmd->replace_instead_of_blending != !frame->blend)) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of frame parameters mismatch\n"); ++ return 0; ++ } ++ ++ Milliseconds now = gr_now_ms(); ++ if (now - frame->atime > graphics_direct_transmission_timeout_ms) { ++ fprintf(stderr, "warning: Not resuming interrupted upload " ++ "because of time out\n"); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++/// Appends `data` to the on-disk cache file of the frame `frame`. Creates the ++/// file if it doesn't exist. Updates `frame->disk_size` and the total disk ++/// size. Returns 1 on success and 0 on failure. ++static int gr_append_raw_data_to_file(ImageFrame *frame, const char *data, ++ size_t data_size) { ++ // If there is no open file corresponding to the image, create it. ++ if (!frame->open_file) { ++ gr_make_sure_tmpdir_exists(); ++ char filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE); ++ FILE *file = fopen(filename, frame->disk_size ? "a" : "w"); ++ if (!file) ++ return 0; ++ frame->open_file = file; ++ } ++ ++ // Write data to the file and update disk size variables. ++ fwrite(data, 1, data_size, frame->open_file); ++ frame->disk_size += data_size; ++ frame->image->total_disk_size += data_size; ++ images_disk_size += data_size; ++ gr_touch_frame(frame); ++ return 1; ++} ++ ++/// Appends data from `payload` to the frame `frame` when using direct ++/// transmission. Note that we report errors only for the final command ++/// (`!more`) to avoid spamming the client. If the frame is not specified, use ++/// the image id and frame index we are currently uploading. ++static void gr_append_data(ImageFrame *frame, const char *payload, int more) { ++ gr_set_current_upload_frame(frame); ++ ++ if (frame->status != STATUS_UPLOADING) { ++ if (!more) ++ gr_reportuploaderror(frame); ++ gr_set_current_upload_frame(NULL); ++ return; ++ } ++ ++ // Decode the data. ++ size_t data_size = 0; ++ char *data = gr_base64dec(payload, &data_size); ++ ++ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size, ++ frame->disk_size + data_size); ++ ++ // Do not append this data if the image exceeds the size limit. ++ if (frame->disk_size + data_size > ++ graphics_max_single_image_file_size || ++ frame->expected_size > graphics_max_single_image_file_size) { ++ free(data); ++ gr_delete_imagefile(frame); ++ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; ++ if (!more) ++ gr_reportuploaderror(frame); ++ gr_set_current_upload_frame(NULL); ++ return; ++ } ++ ++ // Append the data to the file. ++ if (!gr_append_raw_data_to_file(frame, data, data_size)) { ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE; ++ if (!more) ++ gr_reportuploaderror(frame); ++ gr_set_current_upload_frame(NULL); ++ return; ++ } ++ free(data); ++ ++ if (!more) { ++ gr_set_current_upload_frame(NULL); ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ uint32_t placement_id = frame->image->default_placement; ++ if (frame->expected_size && ++ frame->expected_size != frame->disk_size) { ++ // Report failure if the uploaded image size doesn't ++ // match the expected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ gr_reportuploaderror(frame); ++ } else { ++ // Make sure to redraw all existing image instances. ++ gr_schedule_image_redraw(frame->image); ++ // Try to load the image into ram and report the result. ++ frame = gr_loadimage_and_report(frame); ++ // If there is a non-virtual image placement, we may ++ // need to display it. ++ if (frame && frame->index == 1) { ++ Image *img = frame->image; ++ ImagePlacement *placement = NULL; ++ kh_foreach_value(img->placements, placement, { ++ gr_display_nonvirtual_placement(placement); ++ }); ++ } ++ } ++ } ++ ++ // Check whether we need to delete old images. ++ gr_check_limits(); ++} ++ ++/// Finds the image either by id or by number specified in the command. ++static Image *gr_find_image_for_command(GraphicsCommand *cmd) { ++ if (cmd->image_id) ++ return gr_find_image(cmd->image_id); ++ Image *img = NULL; ++ // If the image number is not specified, we can't find the image, unless ++ // it's a put command, in which case we will try the last image. ++ if (cmd->image_number == 0 && cmd->action == 'p') ++ img = gr_find_image(last_image_id); ++ else ++ img = gr_find_image_by_number(cmd->image_number); ++ return img; ++} ++ ++/// Creates a new image or a new frame in an existing image (depending on the ++/// command's action) and initializes its parameters from the command. ++static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) { ++ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 && ++ cmd->compression != 0) { ++ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only " ++ "for raw pixel data (f=32 or f=24)"); ++ // Even though we report an error, we still create an image. ++ } ++ ++ Image *img = NULL; ++ if (cmd->action == 'f') { ++ // If it's a frame transmission action, there must be an ++ // existing image. ++ img = gr_find_image_for_command(cmd); ++ if (img) { ++ cmd->image_id = img->image_id; ++ } else { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return NULL; ++ } ++ } else { ++ // Otherwise create a new image object. If the action is `q`, ++ // we'll use random id instead of the one specified in the ++ // command. ++ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id; ++ img = gr_new_image(image_id); ++ if (!img) ++ return NULL; ++ if (cmd->action == 'q') ++ img->query_id = cmd->image_id; ++ else if (!cmd->image_id) ++ cmd->image_id = img->image_id; ++ // Set the image number. ++ img->image_number = cmd->image_number; ++ } ++ ++ ImageFrame *frame = gr_append_new_frame(img); ++ // Initialize the frame. ++ frame->expected_size = cmd->size; ++ // The default format is 32. ++ frame->format = cmd->format ? cmd->format : 32; ++ frame->compression = cmd->compression; ++ frame->background_color = cmd->background_color; ++ frame->background_frame_index = cmd->background_frame; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ frame->blend = !cmd->replace_instead_of_blending; ++ frame->data_pix_width = cmd->frame_pix_width; ++ frame->data_pix_height = cmd->frame_pix_height; ++ if (cmd->action == 'f') { ++ frame->x = cmd->frame_dst_pix_x; ++ frame->y = cmd->frame_dst_pix_y; ++ } ++ // If the expected size is not specified, we can infer it from the pixel ++ // width and height if the format is 24 or 32 and there is no ++ // compression. This is required for the shared memory transmission. ++ if (!frame->expected_size && !frame->compression && ++ (frame->format == 24 || frame->format == 32)) { ++ frame->expected_size = frame->data_pix_width * ++ frame->data_pix_height * ++ (frame->format / 8); ++ } ++ // We save the quietness information in the frame because for direct ++ // transmission subsequent transmission command won't contain this info. ++ frame->quiet = cmd->quiet; ++ return frame; ++} ++ ++/// Removes a file if it actually looks like a temporary file. ++static void gr_delete_tmp_file(const char *filename) { ++ if (strstr(filename, "tty-graphics-protocol") == NULL) ++ return; ++ if (strstr(filename, "/tmp/") != filename) { ++ const char *tmpdir = getenv("TMPDIR"); ++ if (!tmpdir || !tmpdir[0] || ++ strstr(filename, tmpdir) != filename) ++ return; ++ } ++ unlink(filename); ++} ++ ++/// Copy the image file `frame->original_filename` to the cache directory. This ++/// is done when the image is transmitted via file transfer, or when we have ++/// evicted the image from the disk cache and need to restore it. ++/// If `cmd` is not NULL, it's used to report errors, otherwise errors are only ++/// printed to stderr. ++static void gr_copy_imagefile(ImageFrame *frame, GraphicsCommand *cmd) { ++ GR_LOG("Copying image %s\n", ++ sanitized_filename(frame->original_filename)); ++ // Stat the file and check that it's a regular file and not too big. ++ struct stat st; ++ int stat_res = stat(frame->original_filename, &st); ++ ++ const char *stat_error = NULL; ++ if (stat_res) ++ stat_error = strerror(errno); ++ else if (!S_ISREG(st.st_mode)) ++ stat_error = "Not a regular file"; ++ else if (st.st_size == 0) ++ stat_error = "The size of the file is zero"; ++ else if (st.st_size > graphics_max_single_image_file_size) ++ stat_error = "The file is too large"; ++ if (stat_error) { ++ fprintf(stderr, "Could not load the file %s: %s\n", ++ sanitized_filename(frame->original_filename), ++ stat_error); ++ if (cmd) ++ gr_reporterror_cmd(cmd, "EBADF: %s", stat_error); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ return; ++ } ++ ++ // Check the expected size if specified. ++ if (frame->expected_size && frame->expected_size != st.st_size) { ++ fprintf(stderr, ++ "Could not load, the size doesn't match: %s expected " ++ "%u vs actual %ld\n", ++ sanitized_filename(frame->original_filename), ++ frame->expected_size, st.st_size); ++ // The file has unexpected size. ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ if (cmd) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ // If we know the original modification time, we are trying to restore ++ // the evicted image file. Check that the modification time matches. ++ if (frame->original_file_mtime && ++ frame->original_file_mtime != st.st_mtime) { ++ fprintf(stderr, "Could not load, the mtime doesn't match: %s\n", ++ sanitized_filename(frame->original_filename)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_MTIME_MISMATCH; ++ if (cmd) ++ gr_reportuploaderror(frame); ++ return; ++ } ++ ++ frame->original_file_mtime = st.st_mtime; ++ ++ gr_make_sure_tmpdir_exists(); ++ // Build the filename for the cached copy of the file. ++ char cache_filename[MAX_FILENAME_SIZE]; ++ gr_get_frame_filename(frame, cache_filename, MAX_FILENAME_SIZE); ++ // We will create a symlink to the original file, and ++ // then copy the file to the temporary cache dir. We do ++ // this symlink trick mostly to be able to use cp for ++ // copying, and avoid escaping file name characters when ++ // calling system at the same time. ++ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0}; ++ strcat(tmp_filename_symlink, cache_filename); ++ strcat(tmp_filename_symlink, ".sym"); ++ char command[MAX_FILENAME_SIZE + 256]; ++ size_t len = snprintf(command, MAX_FILENAME_SIZE + 255, "cp '%s' '%s'", ++ tmp_filename_symlink, cache_filename); ++ ++ if (len > MAX_FILENAME_SIZE + 255 || ++ symlink(frame->original_filename, tmp_filename_symlink) || ++ system(command) != 0) { ++ fprintf(stderr, ++ "Could not copy the image " ++ "%s (symlink %s) to %s", ++ sanitized_filename(frame->original_filename), ++ tmp_filename_symlink, cache_filename); ++ if (cmd) ++ gr_reporterror_cmd(cmd, "EBADF: could not copy the " ++ "image to the cache dir"); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_COPY_FILE; ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ return; ++ } ++ ++ // Delete the symlink. ++ unlink(tmp_filename_symlink); ++ // Set the status and update disk size variables. ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ frame->disk_size = st.st_size; ++ frame->image->total_disk_size += st.st_size; ++ images_disk_size += frame->disk_size; ++} ++ ++/// Tries to restore the image file for `frame` if the original file is still ++/// available. ++static void gr_try_restore_imagefile(ImageFrame *frame) { ++ if (frame->disk_size != 0) ++ return; ++ if (gr_is_original_file_still_available(frame)) ++ gr_copy_imagefile(frame, NULL); ++} ++ ++/// Handles a data transmission command. ++static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) { ++ // The default is direct transmission. ++ if (!cmd->transmission_medium) ++ cmd->transmission_medium = 'd'; ++ ++ // If neither id, nor image number is specified, and the transmission ++ // medium is 'd' (or unspecified), and there is an active direct upload, ++ // this is a continuation of the upload. ++ if (current_upload_image_id != 0 && cmd->image_id == 0 && ++ cmd->image_number == 0 && cmd->transmission_medium == 'd') { ++ cmd->image_id = current_upload_image_id; ++ GR_LOG("No images id is specified, continuing uploading %u\n", ++ cmd->image_id); ++ } ++ ++ ImageFrame *frame = NULL; ++ if (cmd->transmission_medium == 'f' || ++ cmd->transmission_medium == 't') { ++ // File transmission. ++ // Create a new image or a new frame of an existing image. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ // Decode the filename. ++ frame->original_filename = gr_base64dec(cmd->payload, NULL); ++ // Copy the file to the cache directory. ++ gr_copy_imagefile(frame, cmd); ++ if (frame->status == STATUS_UPLOADING_SUCCESS) { ++ // Everything seems fine, try to load and redraw ++ // existing instances. ++ gr_schedule_image_redraw(frame->image); ++ frame = gr_loadimage_and_report(frame); ++ } ++ // Delete the original file if it's temporary. ++ if (cmd->transmission_medium == 't') ++ gr_delete_tmp_file(frame->original_filename); ++ gr_check_limits(); ++ } else if (cmd->transmission_medium == 'd') { ++ // Direct transmission (default if 't' is not specified). ++ frame = gr_get_last_frame(gr_find_image_for_command(cmd)); ++ if (gr_transmission_continuation_is_allowed(cmd, frame)) { ++ // This is a continuation of the previous transmission. ++ cmd->is_direct_transmission_continuation = 1; ++ cmd->image_id = frame->image->image_id; ++ gr_append_data(frame, cmd->payload, cmd->more); ++ return frame; ++ } ++ // Otherwise create a new image or frame structure. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ frame->status = STATUS_UPLOADING; ++ // Start appending data. ++ gr_append_data(frame, cmd->payload, cmd->more); ++ } else if (cmd->transmission_medium == 's') { ++ // Shared memory transmission. ++ // Create a new image or a new frame of an existing image. ++ frame = gr_new_image_or_frame_from_command(cmd); ++ if (!frame) ++ return NULL; ++ last_image_id = frame->image->image_id; ++ // Check that we know the size. ++ if (!frame->expected_size) { ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_UNEXPECTED_SIZE; ++ gr_reporterror_cmd( ++ cmd, "EINVAL: the size of the image is not " ++ "specified and cannot be inferred"); ++ return frame; ++ } ++ // Check the data size limit. ++ if (frame->expected_size > graphics_max_single_image_file_size) { ++ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT; ++ gr_reportuploaderror(frame); ++ return frame; ++ } ++ // Decode the filename. ++ char *original_filename = gr_base64dec(cmd->payload, NULL); ++ GR_LOG("Loading image from shared memory %s\n", ++ sanitized_filename(original_filename)); ++ // Open the shared memory object. ++ int fd = shm_open(original_filename, O_RDONLY, 0); ++ if (fd == -1) { ++ gr_reporterror_cmd(cmd, "EBADF: shm_open: %s", ++ strerror(errno)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; ++ fprintf(stderr, "shm_open failed for %s\n", ++ sanitized_filename(original_filename)); ++ shm_unlink(original_filename); ++ free(original_filename); ++ return frame; ++ } ++ shm_unlink(original_filename); ++ free(original_filename); ++ // The offset we pass to mmap must be a multiple of the page ++ // size. If it's not, adjust it and the size. ++ size_t page_size = sysconf(_SC_PAGESIZE); ++ if (page_size == -1) ++ page_size = 1; ++ size_t offset = cmd->offset - (cmd->offset % page_size); ++ size_t size = frame->expected_size + (cmd->offset - offset); ++ // Map the shared memory object. ++ void *data = ++ mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset); ++ if (data == MAP_FAILED) { ++ gr_reporterror_cmd(cmd, "EBADF: mmap: %s", ++ strerror(errno)); ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ERROR_CANNOT_OPEN_SHM; ++ fprintf(stderr, ++ "mmap failed for size = %ld, offset = %ld\n", ++ size, offset); ++ close(fd); ++ return frame; ++ } ++ close(fd); ++ // Append the data to the cache file. ++ if (gr_append_raw_data_to_file(frame, ++ data + (cmd->offset - offset), ++ frame->expected_size)) { ++ frame->status = STATUS_UPLOADING_SUCCESS; ++ } else { ++ frame->status = STATUS_UPLOADING_ERROR; ++ frame->uploading_failure = ++ ERROR_CANNOT_OPEN_CACHED_FILE; ++ gr_reportuploaderror(frame); ++ } ++ // Close the cache file. ++ gr_close_disk_cache_file(frame); ++ // Unmap the data ++ if (munmap(data, size) != 0) ++ fprintf(stderr, "munmap failed: %s\n", strerror(errno)); ++ // Try to load and redraw existing instances. ++ gr_schedule_image_redraw(frame->image); ++ frame = gr_loadimage_and_report(frame); ++ gr_check_limits(); ++ } else { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: transmission medium '%c' is not supported", ++ cmd->transmission_medium); ++ return NULL; ++ } ++ ++ return frame; ++} ++ ++/// Handles the 'put' command by creating a placement. ++static void gr_handle_put_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (img) { ++ cmd->image_id = img->image_id; ++ } else { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Create a placement. If a placement with the same id already exists, ++ // it will be deleted. If the id is zero, a random id will be generated. ++ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id); ++ placement->virtual = cmd->virtual; ++ placement->src_pix_x = cmd->src_pix_x; ++ placement->src_pix_y = cmd->src_pix_y; ++ placement->src_pix_width = cmd->src_pix_width; ++ placement->src_pix_height = cmd->src_pix_height; ++ placement->cols = cmd->columns; ++ placement->rows = cmd->rows; ++ placement->do_not_move_cursor = cmd->do_not_move_cursor; ++ ++ if (placement->virtual) { ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else if (placement->cols && placement->rows) { ++ // For classic placements the default is to stretch the image if ++ // both cols and rows are specified. ++ placement->scale_mode = SCALE_MODE_FILL; ++ } else if (placement->cols || placement->rows) { ++ // But if only one of them is specified, the default is to ++ // contain. ++ placement->scale_mode = SCALE_MODE_CONTAIN; ++ } else { ++ // If none of them are specified, the default is to use the ++ // original size. ++ placement->scale_mode = SCALE_MODE_NONE; ++ } ++ ++ // Display the placement unless it's virtual. ++ gr_display_nonvirtual_placement(placement); ++ ++ // Report success. ++ gr_reportsuccess_cmd(cmd); ++} ++ ++/// Information about what to delete. ++typedef struct DeletionData { ++ uint32_t image_id; ++ uint32_t placement_id; ++ /// Visible placement found during screen traversal that need to be ++ /// deleted. Each placement must occur only once in this vector. ++ ImagePlacementVec placements_to_delete; ++} DeletionData; ++ ++/// The callback called for each cell to perform deletion. ++static int gr_deletion_callback(void *data, Glyph *gp) { ++ DeletionData *del_data = data; ++ // Leave unicode placeholders alone. ++ if (!tgetisclassicplaceholder(gp)) ++ return 0; ++ uint32_t image_id = tgetimgid(gp); ++ uint32_t placement_id = tgetimgplacementid(gp); ++ if (del_data->image_id && del_data->image_id != image_id) ++ return 0; ++ if (del_data->placement_id && del_data->placement_id != placement_id) ++ return 0; ++ ++ ImagePlacement *placement = NULL; ++ ++ // Record the placement to delete. We will actually delete it later. ++ for (int i = 0; i < kv_size(del_data->placements_to_delete); ++i) { ++ ImagePlacement *cand = kv_A(del_data->placements_to_delete, i); ++ if (cand->image->image_id == image_id && ++ cand->placement_id == placement_id) { ++ placement = cand; ++ break; ++ } ++ } ++ if (!placement) { ++ placement = gr_find_image_and_placement(image_id, placement_id); ++ if (placement) { ++ kv_push(ImagePlacement *, ++ del_data->placements_to_delete, placement); ++ } ++ } ++ ++ // Restore the text underneath the placement if possible. ++ if (placement && placement->text_underneath) { ++ int row = tgetimgrow(gp) - 1; ++ int col = tgetimgcol(gp) - 1; ++ if (col >= 0 && row >= 0 && row < placement->rows && ++ col < placement->cols) { ++ *gp = placement->text_underneath[row * placement->cols + ++ col]; ++ return 1; ++ } ++ } ++ ++ // Otherwise just erase the cell. ++ gp->mode = 0; ++ gp->u = ' '; ++ return 1; ++} ++ ++/// Handles the delete command. ++static void gr_handle_delete_command(GraphicsCommand *cmd) { ++ DeletionData del_data = {0}; ++ char delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0; ++ char d = tolower(cmd->delete_specifier); ++ ++ if (d == 'n') { ++ d = 'i'; ++ Image *img = gr_find_image_by_number(cmd->image_number); ++ if (!img) ++ return; ++ del_data.image_id = img->image_id; ++ } ++ ++ kv_init(del_data.placements_to_delete); ++ ++ if (!d || d == 'a') { ++ // Delete all visible placements. ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else if (d == 'i') { ++ // Delete the specified image by image id and maybe placement ++ // id. ++ if (!del_data.image_id) ++ del_data.image_id = cmd->image_id; ++ if (!del_data.image_id) { ++ fprintf(stderr, ++ "ERROR: image id is not specified in the " ++ "delete command\n"); ++ kv_destroy(del_data.placements_to_delete); ++ return; ++ } ++ del_data.placement_id = cmd->placement_id; ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ } else { ++ fprintf(stderr, ++ "WARNING: unsupported value of the d key: '%c'. The " ++ "command is ignored.\n", ++ cmd->delete_specifier); ++ } ++ ++ // Delete the placements we have collected and maybe images too. ++ for (int i = 0; i < kv_size(del_data.placements_to_delete); ++i) { ++ ImagePlacement *placement = ++ kv_A(del_data.placements_to_delete, i); ++ // Delete the text underneath the placement and set it to NULL ++ // to avoid erasing it from the screen again. ++ free(placement->text_underneath); ++ placement->text_underneath = NULL; ++ Image *img = placement->image; ++ gr_delete_placement(placement); ++ // Delete the image if image deletion is requested (uppercase ++ // delete specifier) and there are no more placements. ++ if (delete_image_if_no_ref && kh_size(img->placements) == 0) ++ gr_delete_image(img); ++ } ++ ++ // NOTE: It's not very clear whether we should delete the image ++ // even if there are no _visible_ placements to delete. We do ++ // this because otherwise there is no way to delete an image ++ // with virtual placements in one command. ++ if (d == 'i' && !del_data.placement_id && delete_image_if_no_ref) ++ gr_delete_image(gr_find_image(cmd->image_id)); ++ ++ kv_destroy(del_data.placements_to_delete); ++} ++ ++/// Clears the cells occupied by the placement. This is normally done when ++/// implicitly deleting a classic placement. ++static void gr_erase_placement(ImagePlacement *placement) { ++ DeletionData del_data = {0}; ++ del_data.image_id = placement->image->image_id; ++ del_data.placement_id = placement->placement_id; ++ kv_init(del_data.placements_to_delete); ++ gr_for_each_image_cell(gr_deletion_callback, &del_data); ++ // Delete the text underneath the placement and set it to NULL ++ // to avoid erasing it from the screen again. ++ free(placement->text_underneath); ++ placement->text_underneath = NULL; ++ kv_destroy(del_data.placements_to_delete); ++} ++ ++static void gr_handle_animation_control_command(GraphicsCommand *cmd) { ++ if (cmd->image_id == 0 && cmd->image_number == 0) { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: neither image id nor image number " ++ "are specified or both are zero"); ++ return; ++ } ++ ++ // Find the image with the id or number. ++ Image *img = gr_find_image_for_command(cmd); ++ if (img) { ++ cmd->image_id = img->image_id; ++ } else { ++ gr_reporterror_cmd(cmd, "ENOENT: image not found"); ++ return; ++ } ++ ++ // Find the frame to edit, if requested. ++ ImageFrame *frame = NULL; ++ if (cmd->edit_frame) ++ frame = gr_get_frame(img, cmd->edit_frame); ++ if (cmd->edit_frame || cmd->gap) { ++ if (!frame) { ++ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found", ++ cmd->edit_frame); ++ return; ++ } ++ if (cmd->gap) { ++ img->total_duration -= frame->gap; ++ frame->gap = cmd->gap; ++ img->total_duration += frame->gap; ++ } ++ } ++ ++ // Set animation-related parameters of the image. ++ if (cmd->current_frame) ++ img->current_frame = cmd->current_frame; ++ if (cmd->animation_state) { ++ if (cmd->animation_state == 1) { ++ img->animation_state = ANIMATION_STATE_STOPPED; ++ } else if (cmd->animation_state == 2) { ++ img->animation_state = ANIMATION_STATE_LOADING; ++ } else if (cmd->animation_state == 3) { ++ img->animation_state = ANIMATION_STATE_LOOPING; ++ } else { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: invalid animation state: %d", ++ cmd->animation_state); ++ } ++ } ++ // TODO: Set the number of loops to cmd->loops ++ ++ // Make sure we redraw all instances of the image. ++ gr_schedule_image_redraw(img); ++} ++ ++/// Handles a command. ++static void gr_handle_command(GraphicsCommand *cmd) { ++ if (!cmd->image_id && !cmd->image_number) { ++ // If there is no image id or image number, nobody expects a ++ // response, so set quiet to 2. ++ cmd->quiet = 2; ++ } ++ ++ int was_transmission = 0; ++ ImageFrame *frame = NULL; ++ ++ switch (cmd->action) { ++ case 0: ++ // If no action is specified, it is data transmission. ++ case 't': ++ case 'q': ++ case 'f': ++ was_transmission = 1; ++ // Transmit data. 'q' means query, which is basically the same ++ // as transmit, but the image is discarded, and the id is fake. ++ // 'f' appends a frame to an existing image. ++ gr_handle_transmit_command(cmd); ++ break; ++ case 'p': ++ // Display (put) the image. ++ gr_handle_put_command(cmd); ++ break; ++ case 'T': ++ was_transmission = 1; ++ // Transmit and display. ++ frame = gr_handle_transmit_command(cmd); ++ if (frame && !cmd->is_direct_transmission_continuation) { ++ gr_handle_put_command(cmd); ++ if (cmd->placement_id) ++ frame->image->initial_placement_id = ++ cmd->placement_id; ++ } ++ break; ++ case 'd': ++ gr_handle_delete_command(cmd); ++ break; ++ case 'a': ++ gr_handle_animation_control_command(cmd); ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c", ++ cmd->action); ++ break; ++ } ++ ++ if (!was_transmission || ++ (cmd->transmission_medium && cmd->transmission_medium != 'd')) { ++ // If it wasn't a transmission command, or if the transmission ++ // wasn't direct, clear the current upload frame and close the ++ // file. (If it was a direct transmission, the current upload ++ // was handled inside `gr_append_data`.) ++ gr_set_current_upload_frame(NULL); ++ } ++} ++ ++/// A partially parsed key-value pair. ++typedef struct KeyAndValue { ++ char *key_start; ++ char *val_start; ++ unsigned key_len, val_len; ++} KeyAndValue; ++ ++/// Parses the value of a key and assigns it to the appropriate field of `cmd`. ++static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) { ++ char *key_start = kv->key_start; ++ char *key_end = key_start + kv->key_len; ++ char *value_start = kv->val_start; ++ char *value_end = value_start + kv->val_len; ++ // Currently all keys are one-character. ++ if (key_end - key_start != 1) { ++ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s", ++ key_end - key_start, key_start); ++ return; ++ } ++ long num = 0; ++ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' || ++ *key_start == 'o') { ++ // Some keys have one-character values. ++ if (value_end - value_start != 1) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: value of 'a', 't' or 'd' must be a " ++ "single char: %s", ++ key_start); ++ return; ++ } ++ } else { ++ // All the other keys have integer values. ++ char *num_end = NULL; ++ num = strtol(value_start, &num_end, 10); ++ if (num_end != value_end) { ++ gr_reporterror_cmd( ++ cmd, "EINVAL: could not parse number value: %s", ++ key_start); ++ return; ++ } ++ } ++ switch (*key_start) { ++ case 'a': ++ cmd->action = *value_start; ++ break; ++ case 't': ++ cmd->transmission_medium = *value_start; ++ break; ++ case 'd': ++ cmd->delete_specifier = *value_start; ++ break; ++ case 'q': ++ cmd->quiet = num; ++ break; ++ case 'f': ++ cmd->format = num; ++ if (num != 0 && num != 24 && num != 32 && num != 100) { ++ gr_reporterror_cmd( ++ cmd, ++ "EINVAL: unsupported format specification: %s", ++ key_start); ++ } ++ break; ++ case 'o': ++ cmd->compression = *value_start; ++ if (cmd->compression != 'z') { ++ gr_reporterror_cmd(cmd, ++ "EINVAL: unsupported compression " ++ "specification: %s", ++ key_start); ++ } ++ break; ++ case 's': ++ if (cmd->action == 'a') ++ cmd->animation_state = num; ++ else ++ cmd->frame_pix_width = num; ++ break; ++ case 'v': ++ if (cmd->action == 'a') ++ cmd->loops = num; ++ else ++ cmd->frame_pix_height = num; ++ break; ++ case 'i': ++ cmd->image_id = num; ++ break; ++ case 'I': ++ cmd->image_number = num; ++ break; ++ case 'p': ++ cmd->placement_id = num; ++ break; ++ case 'x': ++ cmd->src_pix_x = num; ++ cmd->frame_dst_pix_x = num; ++ break; ++ case 'y': ++ if (cmd->action == 'f') ++ cmd->frame_dst_pix_y = num; ++ else ++ cmd->src_pix_y = num; ++ break; ++ case 'w': ++ cmd->src_pix_width = num; ++ break; ++ case 'h': ++ cmd->src_pix_height = num; ++ break; ++ case 'c': ++ if (cmd->action == 'f') ++ cmd->background_frame = num; ++ else if (cmd->action == 'a') ++ cmd->current_frame = num; ++ else ++ cmd->columns = num; ++ break; ++ case 'r': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->edit_frame = num; ++ else ++ cmd->rows = num; ++ break; ++ case 'm': ++ cmd->more = num; ++ break; ++ case 'S': ++ cmd->size = num; ++ break; ++ case 'O': ++ cmd->offset = num; ++ break; ++ case 'U': ++ cmd->virtual = num; ++ break; ++ case 'X': ++ if (cmd->action == 'f') ++ cmd->replace_instead_of_blending = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'Y': ++ if (cmd->action == 'f') ++ cmd->background_color = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'z': ++ if (cmd->action == 'f' || cmd->action == 'a') ++ cmd->gap = num; ++ else ++ break; /*ignore*/ ++ break; ++ case 'C': ++ cmd->do_not_move_cursor = num; ++ break; ++ default: ++ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s", ++ key_start); ++ return; ++ } ++} ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters. Returns 1 on success. ++int gr_parse_command(char *buf, size_t len) { ++ if (buf[0] != 'G') ++ return 0; ++ ++ Milliseconds command_start_time = gr_now_ms(); ++ debug_loaded_files_counter = 0; ++ debug_loaded_pixmaps_counter = 0; ++ global_command_counter++; ++ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf); ++ ++ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult)); ++ ++ // Eat the 'G'. ++ ++buf; ++ --len; ++ ++ GraphicsCommand cmd = {.command = buf}; ++ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to ++ // parse the payload. ++ char state = 'k'; ++ // An array of partially parsed key-value pairs. ++ KeyAndValue key_vals[32]; ++ unsigned key_vals_count = 0; ++ char *key_start = buf; ++ char *key_end = NULL; ++ char *val_start = NULL; ++ char *val_end = NULL; ++ char *c = buf; ++ while (c - buf < len + 1) { ++ if (state == 'k') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ key_end = c; ++ gr_reporterror_cmd( ++ &cmd, "EINVAL: key without value: %s ", ++ key_start); ++ break; ++ case '=': ++ key_end = c; ++ state = 'v'; ++ val_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'v') { ++ switch (*c) { ++ case ',': ++ case ';': ++ case '\0': ++ state = *c == ',' ? 'k' : 'p'; ++ val_end = c; ++ if (key_vals_count >= ++ sizeof(key_vals) / sizeof(*key_vals)) { ++ gr_reporterror_cmd(&cmd, ++ "EINVAL: too many " ++ "key-value pairs"); ++ break; ++ } ++ key_vals[key_vals_count].key_start = key_start; ++ key_vals[key_vals_count].val_start = val_start; ++ key_vals[key_vals_count].key_len = ++ key_end - key_start; ++ key_vals[key_vals_count].val_len = ++ val_end - val_start; ++ ++key_vals_count; ++ key_start = c + 1; ++ break; ++ default: ++ break; ++ } ++ } else if (state == 'p') { ++ cmd.payload = c; ++ // break out of the loop, we don't check the payload ++ break; ++ } ++ ++c; ++ } ++ ++ // Set the action key ('a=') first because we need it to disambiguate ++ // some keys. Also set 'i=' and 'I=' for better error reporting. ++ for (unsigned i = 0; i < key_vals_count; ++i) { ++ if (key_vals[i].key_len == 1) { ++ char *start = key_vals[i].key_start; ++ if (*start == 'a' || *start == 'i' || *start == 'I') { ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ break; ++ } ++ } ++ } ++ // Set the rest of the keys. ++ for (unsigned i = 0; i < key_vals_count; ++i) ++ gr_set_keyvalue(&cmd, &key_vals[i]); ++ ++ if (!cmd.payload) ++ cmd.payload = buf + len; ++ ++ if (cmd.payload && cmd.payload[0]) ++ GR_LOG(" payload size: %ld\n", strlen(cmd.payload)); ++ ++ if (!graphics_command_result.error) ++ gr_handle_command(&cmd); ++ ++ if (graphics_debug_mode) { ++ fprintf(stderr, "Response: "); ++ for (const char *resp = graphics_command_result.response; ++ *resp != '\0'; ++resp) { ++ if (isprint(*resp)) ++ fprintf(stderr, "%c", *resp); ++ else ++ fprintf(stderr, "(0x%x)", *resp); ++ } ++ fprintf(stderr, "\n"); ++ } ++ ++ // Make sure that we suppress response if needed. Usually cmd.quiet is ++ // taken into account when creating the response, but it's not very ++ // reliable in the current implementation. ++ if (cmd.quiet) { ++ if (!graphics_command_result.error || cmd.quiet >= 2) ++ graphics_command_result.response[0] = '\0'; ++ } ++ ++ Milliseconds command_end_time = gr_now_ms(); ++ GR_LOG("Command %lu took %ld ms (loaded %d files, %d pixmaps)\n\n", ++ global_command_counter, command_end_time - command_start_time, ++ debug_loaded_files_counter, debug_loaded_pixmaps_counter); ++ ++ return 1; ++} ++ ++//////////////////////////////////////////////////////////////////////////////// ++// base64 decoding part is basically copied from st.c ++//////////////////////////////////////////////////////////////////////////////// ++ ++static const char gr_base64_digits[] = { ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, ++ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, ++ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ++ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, ++ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, ++ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; ++ ++static char gr_base64_getc(const char **src) { ++ while (**src && !isprint(**src)) ++ (*src)++; ++ return **src ? *((*src)++) : '='; /* emulate padding if string ends */ ++} ++ ++char *gr_base64dec(const char *src, size_t *size) { ++ size_t in_len = strlen(src); ++ char *result, *dst; ++ ++ result = dst = malloc((in_len + 3) / 4 * 3 + 1); ++ while (*src) { ++ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)]; ++ ++ if (a == -1 || b == -1) ++ break; ++ ++ *dst++ = (a << 2) | ((b & 0x30) >> 4); ++ if (c == -1) ++ break; ++ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); ++ if (d == -1) ++ break; ++ *dst++ = ((c & 0x03) << 6) | d; ++ } ++ *dst = '\0'; ++ if (size) { ++ *size = dst - result; ++ } ++ return result; ++} +diff --git a/graphics.h b/graphics.h +new file mode 100644 +index 0000000..f9a649a +--- /dev/null ++++ b/graphics.h +@@ -0,0 +1,112 @@ ++ ++#include ++#include ++#include ++ ++/// Initialize the graphics module. ++void gr_init(Display *disp, Visual *vis, Colormap cm); ++/// Deinitialize the graphics module. ++void gr_deinit(); ++ ++/// Add an image rectangle to a list if rectangles to draw. This function may ++/// actually draw some rectangles, or it may wait till more rectangles are ++/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`. ++/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the ++/// part of the image to draw (row/col indices are zero-based, ends are ++/// excluded). ++/// - `x_col` and `y_row` are the coordinates of the top-left corner of the ++/// image in the terminal grid. ++/// - `x_pix` and `y_pix` are the same but in pixels. ++/// - `reverse` indicates whether colors should be inverted. ++void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id, ++ int img_start_col, int img_end_col, int img_start_row, ++ int img_end_row, int x_col, int y_row, int x_pix, ++ int y_pix, int cw, int ch, int reverse); ++/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell. ++void gr_start_drawing(Drawable buf, int cw, int ch); ++/// Finish image drawing. This functions will draw all the rectangles left to ++/// draw. ++void gr_finish_drawing(Drawable buf); ++/// Mark rows containing animations as dirty if it's time to redraw them. Must ++/// be called right after `gr_start_drawing`. ++void gr_mark_dirty_animations(int *dirty, int rows); ++ ++/// Parse and execute a graphics command. `buf` must start with 'G' and contain ++/// at least `len + 1` characters (including '\0'). Returns 1 on success. ++/// Additional informations is returned through `graphics_command_result`. ++int gr_parse_command(char *buf, size_t len); ++ ++/// Executes `command` with the name of the file corresponding to `image_id` as ++/// the argument. Executes xmessage with an error message on failure. ++void gr_preview_image(uint32_t image_id, const char *command); ++ ++/// Executes ` -e less ` where is the name of a temporary file ++/// containing the information about an image and placement, and is ++/// specified with `st_executable`. ++void gr_show_image_info(uint32_t image_id, uint32_t placement_id, ++ uint32_t imgcol, uint32_t imgrow, ++ char is_classic_placeholder, int32_t diacritic_count, ++ char *st_executable); ++ ++/// Dumps the internal state (images and placements) to stderr. ++void gr_dump_state(); ++ ++/// Unloads images to reduce RAM usage. ++void gr_unload_images_to_reduce_ram(); ++ ++/// Executes `callback` for each image cell. The callback should return 1 if it ++/// changed the glyph. This function is implemented in `st.c`. ++void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp), ++ void *data); ++ ++/// Marks all the rows containing the image with `image_id` as dirty. ++void gr_schedule_image_redraw_by_id(uint32_t image_id); ++ ++/// Returns a pointer to the glyph under the classic placement with `image_id` ++/// and `placement_id` at `col` and `row` (1-based). May return NULL if the ++/// underneath text is unknown. ++Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id, ++ int col, int row); ++ ++typedef enum { ++ GRAPHICS_DEBUG_NONE = 0, ++ GRAPHICS_DEBUG_LOG = 1, ++ GRAPHICS_DEBUG_LOG_AND_BOXES = 2, ++} GraphicsDebugMode; ++ ++/// Print additional information, draw bounding bounding boxes, etc. ++extern GraphicsDebugMode graphics_debug_mode; ++ ++/// Whether to display images or just draw bounding boxes. ++extern char graphics_display_images; ++ ++/// The time in milliseconds until the next redraw to update animations. ++/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`. ++extern int graphics_next_redraw_delay; ++ ++#define MAX_GRAPHICS_RESPONSE_LEN 256 ++ ++/// A structure representing the result of a graphics command. ++typedef struct { ++ /// Indicates if the terminal needs to be redrawn. ++ char redraw; ++ /// The response of the command that should be sent back to the client ++ /// (may be empty if the quiet flag is set). ++ char response[MAX_GRAPHICS_RESPONSE_LEN]; ++ /// Whether there was an error executing this command (not very useful, ++ /// the response must be sent back anyway). ++ char error; ++ /// Whether the terminal has to create a placeholder for a non-virtual ++ /// placement. ++ char create_placeholder; ++ /// The placeholder that needs to be created. ++ struct { ++ uint32_t rows, columns; ++ uint32_t image_id, placement_id; ++ char do_not_move_cursor; ++ Glyph *text_underneath; ++ } placeholder; ++} GraphicsCommandResult; ++ ++/// The result of a graphics command. ++extern GraphicsCommandResult graphics_command_result; +diff --git a/icat-mini.sh b/icat-mini.sh +new file mode 100755 +index 0000000..8f240e9 +--- /dev/null ++++ b/icat-mini.sh +@@ -0,0 +1,875 @@ ++#!/bin/sh ++ ++# vim: shiftwidth=4 ++ ++script_name="$(basename "$0")" ++ ++short_help="Usage: $script_name [OPTIONS] ++ ++This is a script to display images in the terminal using the kitty graphics ++protocol with Unicode placeholders. It is very basic, please use something else ++if you have alternatives. ++ ++Options: ++ -h Show this help. ++ -s SCALE The scale of the image, may be floating point. ++ -c N, --cols N The number of columns. ++ -r N, --rows N The number of rows. ++ --max-cols N The maximum number of columns. ++ --max-rows N The maximum number of rows. ++ --cell-size WxH The cell size in pixels. ++ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. ++ --speed SPEED The multiplier for the animation speed (float). ++" ++ ++# Exit the script on keyboard interrupt ++trap "echo 'icat-mini was interrupted' >&2; exit 1" INT ++ ++cols="" ++rows="" ++file="" ++command_tty="" ++response_tty="" ++uploading_method="auto" ++cell_size="" ++scale=1 ++max_cols="" ++max_rows="" ++speed="" ++ ++# Parse the command line. ++while [ $# -gt 0 ]; do ++ case "$1" in ++ -c|--columns|--cols) ++ cols="$2" ++ shift 2 ++ ;; ++ -r|--rows|-l|--lines) ++ rows="$2" ++ shift 2 ++ ;; ++ -s|--scale) ++ scale="$2" ++ shift 2 ++ ;; ++ -h|--help) ++ echo "$short_help" ++ exit 0 ++ ;; ++ -m|--upload-method|--uploading-method) ++ uploading_method="$2" ++ shift 2 ++ ;; ++ --cell-size) ++ cell_size="$2" ++ shift 2 ++ ;; ++ --max-cols) ++ max_cols="$2" ++ shift 2 ++ ;; ++ --max-rows) ++ max_rows="$2" ++ shift 2 ++ ;; ++ --speed) ++ speed="$2" ++ shift 2 ++ ;; ++ --) ++ file="$2" ++ shift 2 ++ ;; ++ -*) ++ echo "Unknown option: $1" >&2 ++ exit 1 ++ ;; ++ *) ++ if [ -n "$file" ]; then ++ echo "Multiple image files are not supported: $file and $1" >&2 ++ exit 1 ++ fi ++ file="$1" ++ shift ++ ;; ++ esac ++done ++ ++file="$(realpath "$file")" ++ ++##################################################################### ++# Detect imagemagick ++##################################################################### ++ ++# If there is the 'magick' command, use it instead of separate 'convert' and ++# 'identify' commands. ++if command -v magick > /dev/null; then ++ identify="magick identify" ++ convert="magick" ++else ++ identify="identify" ++ convert="convert" ++fi ++ ++##################################################################### ++# Detect tmux ++##################################################################### ++ ++# Check if we are inside tmux. ++inside_tmux="" ++if [ -n "$TMUX" ]; then ++ inside_tmux=1 ++fi ++ ++if [ -z "$command_tty" ] && [ -n "$inside_tmux" ]; then ++ # Get the pty of the current tmux pane. ++ command_tty="$(tmux display-message -t "$TMUX_PANE" -p "#{pane_tty}")" ++ if [ ! -e "$command_tty" ]; then ++ command_tty="" ++ fi ++fi ++ ++##################################################################### ++# Adjust the terminal state ++##################################################################### ++ ++if [ -z "$command_tty" ]; then ++ command_tty="/dev/tty" ++fi ++if [ -z "$response_tty" ]; then ++ response_tty="/dev/tty" ++fi ++ ++stty_orig="$(stty -g < "$response_tty")" ++stty -echo < "$response_tty" ++# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some ++# horrible issues otherwise. ++stty susp undef < "$response_tty" ++stty -icanon < "$response_tty" ++ ++restore_echo() { ++ [ -n "$stty_orig" ] || return ++ stty $stty_orig < "$response_tty" ++} ++ ++trap restore_echo EXIT TERM ++ ++##################################################################### ++# Compute the number of rows and columns ++##################################################################### ++ ++is_pos_int() { ++ if [ -z "$1" ]; then ++ return 1 # false ++ fi ++ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then ++ if [ "$1" -gt 0 ]; then ++ return 0 # true ++ fi ++ fi ++ return 1 # false ++} ++ ++if [ -n "$cols" ] || [ -n "$rows" ]; then ++ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then ++ echo "You can't specify both max-cols/rows and cols/rows" >&2 ++ exit 1 ++ fi ++fi ++ ++# Get the max number of cols and rows. ++[ -n "$max_cols" ] || max_cols="$(tput cols)" ++[ -n "$max_rows" ] || max_rows="$(tput lines)" ++if [ "$max_rows" -gt 255 ]; then ++ max_rows=255 ++fi ++ ++python_ioctl_command="import array, fcntl, termios ++buf = array.array('H', [0, 0, 0, 0]) ++fcntl.ioctl(0, termios.TIOCGWINSZ, buf) ++print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" ++ ++# Get the cell size in pixels if either cols or rows are not specified. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ cell_width="" ++ cell_height="" ++ # If the cell size is specified, use it. ++ if [ -n "$cell_size" ]; then ++ cell_width="${cell_size%x*}" ++ cell_height="${cell_size#*x}" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ echo "Invalid cell size: $cell_size" >&2 ++ exit 1 ++ fi ++ fi ++ # Otherwise try to use TIOCGWINSZ ioctl via python. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$command_tty" 2> /dev/null)" ++ cell_width="${cell_size_ioctl% *}" ++ cell_height="${cell_size_ioctl#* }" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width="" ++ cell_height="" ++ fi ++ fi ++ # If it didn't work, try to use csi XTWINOPS. ++ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then ++ if [ -n "$inside_tmux" ]; then ++ printf '\ePtmux;\e\e[16t\e\\' >> "$command_tty" ++ else ++ printf '\e[16t' >> "$command_tty" ++ fi ++ # The expected response will look like ^[[6;;t ++ term_response="" ++ while true; do ++ char=$(dd bs=1 count=1 <"$response_tty" 2>/dev/null) ++ if [ "$char" = "t" ]; then ++ break ++ fi ++ term_response="$term_response$char" ++ done ++ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" ++ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" ++ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then ++ cell_width=8 ++ cell_height=16 ++ fi ++ fi ++fi ++ ++# Compute a formula with bc and round to the nearest integer. ++bc_round() { ++ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" ++} ++ ++# Compute the number of rows and columns of the image. ++if [ -z "$cols" ] || [ -z "$rows" ]; then ++ # Get the size of the image and its resolution. If it's an animation, use ++ # the first frame. ++ format_output="$($identify -format '%w %h\n' "$file" | head -1)" ++ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" ++ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" ++ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then ++ echo "Couldn't get image size from identify: $format_output" >&2 ++ echo >&2 ++ exit 1 ++ fi ++ opt_cols_expr="(${scale}*${img_width}/${cell_width})" ++ opt_rows_expr="(${scale}*${img_height}/${cell_height})" ++ if [ -z "$cols" ] && [ -z "$rows" ]; then ++ # If columns and rows are not specified, compute the optimal values ++ # using the information about rows and columns per inch. ++ cols="$(bc_round "$opt_cols_expr")" ++ rows="$(bc_round "$opt_rows_expr")" ++ # Make sure that automatically computed rows and columns are within some ++ # sane limits ++ if [ "$cols" -gt "$max_cols" ]; then ++ rows="$(bc_round "$rows * $max_cols / $cols")" ++ cols="$max_cols" ++ fi ++ if [ "$rows" -gt "$max_rows" ]; then ++ cols="$(bc_round "$cols * $max_rows / $rows")" ++ rows="$max_rows" ++ fi ++ elif [ -z "$cols" ]; then ++ # If only one dimension is specified, compute the other one to match the ++ # aspect ratio as close as possible. ++ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" ++ elif [ -z "$rows" ]; then ++ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" ++ fi ++ ++ if [ "$cols" -lt 1 ]; then ++ cols=1 ++ fi ++ if [ "$rows" -lt 1 ]; then ++ rows=1 ++ fi ++fi ++ ++##################################################################### ++# Generate an image id ++##################################################################### ++ ++image_id="" ++while [ -z "$image_id" ]; do ++ image_id="$(shuf -i 16777217-4294967295 -n 1)" ++ # Check that the id requires 24-bit fg colors. ++ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then ++ image_id="" ++ fi ++done ++ ++##################################################################### ++# Uploading the image ++##################################################################### ++ ++# Choose the uploading method ++if [ "$uploading_method" = "auto" ]; then ++ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then ++ uploading_method="direct" ++ else ++ uploading_method="file" ++ fi ++fi ++ ++# Functions to emit the start and the end of a graphics command. ++if [ -n "$inside_tmux" ]; then ++ # If we are in tmux we have to wrap the command in Ptmux. ++ graphics_command_start='\ePtmux;\e\e_G' ++ graphics_command_end='\e\e\\\e\\' ++else ++ graphics_command_start='\e_G' ++ graphics_command_end='\e\\' ++fi ++ ++# Send a graphics command with the correct start and end ++gr_command() { ++ printf "${graphics_command_start}%s${graphics_command_end}" "$1" >> "$command_tty" ++} ++ ++# Compute the size of a data chunk for direct transmission. ++if [ "$uploading_method" = "direct" ]; then ++ # Get the value of PIPE_BUF. ++ pipe_buf="$(getconf PIPE_BUF "$command_tty" 2> /dev/null)" ++ if is_pos_int "$pipe_buf"; then ++ # Make sure it's between 512 and 4096. ++ if [ "$(expr "$pipe_buf" \< 512)" -eq 1 ]; then ++ pipe_buf=512 ++ elif [ "$(expr "$pipe_buf" \> 4096)" -eq 1 ]; then ++ pipe_buf=4096 ++ fi ++ else ++ pipe_buf=512 ++ fi ++ ++ # The size of each graphics command shouldn't be more than PIPE_BUF, so we ++ # set the size of an encoded chunk to be PIPE_BUF - 128 to leave some space ++ # for the command. ++ chunk_size="$(expr "$pipe_buf" - 128)" ++fi ++ ++# Check if the image format is supported. ++is_format_supported() { ++ arg_format="$1" ++ if [ "$arg_format" = "PNG" ]; then ++ return 0 ++ elif [ "$arg_format" = "JPEG" ]; then ++ if [ -z "$inside_tmux" ]; then ++ actual_term="$TERM" ++ else ++ # Get the actual current terminal name from tmux. ++ actual_term="$(tmux display-message -p "#{client_termname}")" ++ fi ++ # st is known to support JPEG. ++ case "$actual_term" in ++ st | *-st | st-* | *-st-*) ++ return 0 ++ ;; ++ esac ++ return 1 ++ else ++ return 1 ++ fi ++} ++ ++# Send an uploading command. Usage: gr_upload ++# Where is a part of command that specifies the action, it will be ++# repeated for every chunk (if the method is direct), and is the rest ++# of the command that specifies the image parameters. and ++# must not include the transmission method or ';'. ++# Example: ++# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++gr_upload() { ++ arg_action="$1" ++ arg_command="$2" ++ arg_file="$3" ++ if [ "$uploading_method" = "file" ]; then ++ # base64-encode the filename ++ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) ++ # If the file name contains 'tty-graphics-protocol', assume it's ++ # temporary and use t=t. ++ medium="t=f" ++ case "$arg_file" in ++ *tty-graphics-protocol*) ++ medium="t=t" ++ ;; ++ *) ++ medium="t=f" ++ ;; ++ esac ++ gr_command "${arg_action},${arg_command},${medium};${encoded_filename}" ++ fi ++ if [ "$uploading_method" = "direct" ]; then ++ # Create a temporary directory to store the chunked image. ++ chunkdir="$(mktemp -d)" ++ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then ++ echo "Can't create a temp dir" >&2 ++ exit 1 ++ fi ++ # base64-encode the file and split it into chunks. The size of each ++ # graphics command shouldn't be more than 4096, so we set the size of an ++ # encoded chunk to be 3968, slightly less than that. ++ chunk_size=3968 ++ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" ++ ++ # Issue a command indicating that we want to start data transmission for ++ # a new image. ++ gr_command "${arg_action},${arg_command},t=d,m=1" ++ ++ # Transmit chunks. ++ for chunk in "$chunkdir/chunk_"*; do ++ gr_command "${arg_action},i=${image_id},m=1;$(cat "$chunk")" ++ rm "$chunk" ++ done ++ ++ # Tell the terminal that we are done. ++ gr_command "${arg_action},i=$image_id,m=0" ++ ++ # Remove the temporary directory. ++ rmdir "$chunkdir" ++ fi ++} ++ ++delayed_frame_dir_cleanup() { ++ arg_frame_dir="$1" ++ sleep 2 ++ if [ -n "$arg_frame_dir" ]; then ++ for frame in "$arg_frame_dir"/frame_*.png; do ++ rm "$frame" ++ done ++ rmdir "$arg_frame_dir" ++ fi ++} ++ ++upload_image_and_print_placeholder() { ++ # Check if the file is an animation. ++ format_output=$($identify -format '%n %m\n' "$file" | head -n 1) ++ frame_count="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" ++ image_format="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" ++ ++ if [ "$frame_count" -gt 1 ]; then ++ # The file is an animation, decompose into frames and upload each frame. ++ frame_dir="$(mktemp -d)" ++ frame_dir="$HOME/temp/frames${frame_dir}" ++ mkdir -p "$frame_dir" ++ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then ++ echo "Can't create a temp dir for frames" >&2 ++ exit 1 ++ fi ++ ++ # Decompose the animation into separate frames. ++ $convert "$file" -coalesce "$frame_dir/frame_%06d.png" ++ ++ # Get all frame delays at once, in centiseconds, as a space-separated ++ # string. ++ delays=$($identify -format "%T " "$file") ++ ++ frame_number=1 ++ for frame in "$frame_dir"/frame_*.png; do ++ # Read the delay for the current frame and convert it from ++ # centiseconds to milliseconds. ++ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") ++ delay=$((delay * 10)) ++ # If the delay is 0, set it to 100ms. ++ if [ "$delay" -eq 0 ]; then ++ delay=100 ++ fi ++ ++ if [ -n "$speed" ]; then ++ delay=$(bc_round "$delay / $speed") ++ fi ++ ++ if [ "$frame_number" -eq 1 ]; then ++ # Abort the previous transmission, just in case. ++ gr_command "q=2,a=t,i=${image_id},m=0" ++ # Upload the first frame with a=T ++ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" ++ # Set the delay for the first frame and also play the animation ++ # in loading mode (s=2). ++ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" ++ # Print the placeholder after the first frame to reduce the wait ++ # time. ++ print_placeholder ++ else ++ # Upload subsequent frames with a=f ++ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" ++ fi ++ ++ frame_number=$((frame_number + 1)) ++ done ++ ++ # Play the animation in loop mode (s=3). ++ gr_command "a=a,v=1,s=3,i=${image_id}" ++ ++ # Remove the temporary directory, but do it in the background with a ++ # delay to avoid removing files before they are loaded by the terminal. ++ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & ++ elif is_format_supported "$image_format"; then ++ # The file is not an animation and has a supported format, upload it ++ # directly. ++ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" ++ # Print the placeholder ++ print_placeholder ++ else ++ # The format is not supported, try to convert it to png. ++ temp_file="$(mktemp --tmpdir "icat-mini-tty-graphics-protocol-XXXXX.png")" ++ if ! $convert "$file" "$temp_file"; then ++ echo "Failed to convert the image to PNG" >&2 ++ exit 1 ++ fi ++ # Upload the converted image. ++ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$temp_file" ++ # Print the placeholder ++ print_placeholder ++ fi ++} ++ ++##################################################################### ++# Printing the image placeholder ++##################################################################### ++ ++print_placeholder() { ++ # Each line starts with the escape sequence to set the foreground color to ++ # the image id. ++ blue="$(expr "$image_id" % 256 )" ++ green="$(expr \( "$image_id" / 256 \) % 256 )" ++ red="$(expr \( "$image_id" / 65536 \) % 256 )" ++ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" ++ line_end="$(printf "\e[39;m")" ++ ++ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" ++ eval "id_diacritic=\$d${id4th}" ++ ++ # Reset the brush state, mostly to reset the underline color. ++ printf "\e[0m" ++ ++ # Fill the output with characters representing the image ++ for y in $(seq 0 "$(expr "$rows" - 1)"); do ++ eval "row_diacritic=\$d${y}" ++ line="$line_start" ++ for x in $(seq 0 "$(expr "$cols" - 1)"); do ++ eval "col_diacritic=\$d${x}" ++ # Note that when $x is out of bounds, the column diacritic will ++ # be empty, meaning that the column should be guessed by the ++ # terminal. ++ if [ "$x" -ge "$num_diacritics" ]; then ++ line="${line}${placeholder}${row_diacritic}" ++ else ++ line="${line}${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" ++ fi ++ done ++ line="${line}${line_end}" ++ printf "%s\n" "$line" ++ done ++ ++ printf "\e[0m" ++} ++ ++d0="̅" ++d1="̍" ++d2="̎" ++d3="̐" ++d4="̒" ++d5="̽" ++d6="̾" ++d7="̿" ++d8="͆" ++d9="͊" ++d10="͋" ++d11="͌" ++d12="͐" ++d13="͑" ++d14="͒" ++d15="͗" ++d16="͛" ++d17="ͣ" ++d18="ͤ" ++d19="ͥ" ++d20="ͦ" ++d21="ͧ" ++d22="ͨ" ++d23="ͩ" ++d24="ͪ" ++d25="ͫ" ++d26="ͬ" ++d27="ͭ" ++d28="ͮ" ++d29="ͯ" ++d30="҃" ++d31="҄" ++d32="҅" ++d33="҆" ++d34="҇" ++d35="֒" ++d36="֓" ++d37="֔" ++d38="֕" ++d39="֗" ++d40="֘" ++d41="֙" ++d42="֜" ++d43="֝" ++d44="֞" ++d45="֟" ++d46="֠" ++d47="֡" ++d48="֨" ++d49="֩" ++d50="֫" ++d51="֬" ++d52="֯" ++d53="ׄ" ++d54="ؐ" ++d55="ؑ" ++d56="ؒ" ++d57="ؓ" ++d58="ؔ" ++d59="ؕ" ++d60="ؖ" ++d61="ؗ" ++d62="ٗ" ++d63="٘" ++d64="ٙ" ++d65="ٚ" ++d66="ٛ" ++d67="ٝ" ++d68="ٞ" ++d69="ۖ" ++d70="ۗ" ++d71="ۘ" ++d72="ۙ" ++d73="ۚ" ++d74="ۛ" ++d75="ۜ" ++d76="۟" ++d77="۠" ++d78="ۡ" ++d79="ۢ" ++d80="ۤ" ++d81="ۧ" ++d82="ۨ" ++d83="۫" ++d84="۬" ++d85="ܰ" ++d86="ܲ" ++d87="ܳ" ++d88="ܵ" ++d89="ܶ" ++d90="ܺ" ++d91="ܽ" ++d92="ܿ" ++d93="݀" ++d94="݁" ++d95="݃" ++d96="݅" ++d97="݇" ++d98="݉" ++d99="݊" ++d100="߫" ++d101="߬" ++d102="߭" ++d103="߮" ++d104="߯" ++d105="߰" ++d106="߱" ++d107="߳" ++d108="ࠖ" ++d109="ࠗ" ++d110="࠘" ++d111="࠙" ++d112="ࠛ" ++d113="ࠜ" ++d114="ࠝ" ++d115="ࠞ" ++d116="ࠟ" ++d117="ࠠ" ++d118="ࠡ" ++d119="ࠢ" ++d120="ࠣ" ++d121="ࠥ" ++d122="ࠦ" ++d123="ࠧ" ++d124="ࠩ" ++d125="ࠪ" ++d126="ࠫ" ++d127="ࠬ" ++d128="࠭" ++d129="॑" ++d130="॓" ++d131="॔" ++d132="ྂ" ++d133="ྃ" ++d134="྆" ++d135="྇" ++d136="፝" ++d137="፞" ++d138="፟" ++d139="៝" ++d140="᤺" ++d141="ᨗ" ++d142="᩵" ++d143="᩶" ++d144="᩷" ++d145="᩸" ++d146="᩹" ++d147="᩺" ++d148="᩻" ++d149="᩼" ++d150="᭫" ++d151="᭭" ++d152="᭮" ++d153="᭯" ++d154="᭰" ++d155="᭱" ++d156="᭲" ++d157="᭳" ++d158="᳐" ++d159="᳑" ++d160="᳒" ++d161="᳚" ++d162="᳛" ++d163="᳠" ++d164="᷀" ++d165="᷁" ++d166="᷃" ++d167="᷄" ++d168="᷅" ++d169="᷆" ++d170="᷇" ++d171="᷈" ++d172="᷉" ++d173="᷋" ++d174="᷌" ++d175="᷑" ++d176="᷒" ++d177="ᷓ" ++d178="ᷔ" ++d179="ᷕ" ++d180="ᷖ" ++d181="ᷗ" ++d182="ᷘ" ++d183="ᷙ" ++d184="ᷚ" ++d185="ᷛ" ++d186="ᷜ" ++d187="ᷝ" ++d188="ᷞ" ++d189="ᷟ" ++d190="ᷠ" ++d191="ᷡ" ++d192="ᷢ" ++d193="ᷣ" ++d194="ᷤ" ++d195="ᷥ" ++d196="ᷦ" ++d197="᷾" ++d198="⃐" ++d199="⃑" ++d200="⃔" ++d201="⃕" ++d202="⃖" ++d203="⃗" ++d204="⃛" ++d205="⃜" ++d206="⃡" ++d207="⃧" ++d208="⃩" ++d209="⃰" ++d210="⳯" ++d211="⳰" ++d212="⳱" ++d213="ⷠ" ++d214="ⷡ" ++d215="ⷢ" ++d216="ⷣ" ++d217="ⷤ" ++d218="ⷥ" ++d219="ⷦ" ++d220="ⷧ" ++d221="ⷨ" ++d222="ⷩ" ++d223="ⷪ" ++d224="ⷫ" ++d225="ⷬ" ++d226="ⷭ" ++d227="ⷮ" ++d228="ⷯ" ++d229="ⷰ" ++d230="ⷱ" ++d231="ⷲ" ++d232="ⷳ" ++d233="ⷴ" ++d234="ⷵ" ++d235="ⷶ" ++d236="ⷷ" ++d237="ⷸ" ++d238="ⷹ" ++d239="ⷺ" ++d240="ⷻ" ++d241="ⷼ" ++d242="ⷽ" ++d243="ⷾ" ++d244="ⷿ" ++d245="꙯" ++d246="꙼" ++d247="꙽" ++d248="꛰" ++d249="꛱" ++d250="꣠" ++d251="꣡" ++d252="꣢" ++d253="꣣" ++d254="꣤" ++d255="꣥" ++d256="꣦" ++d257="꣧" ++d258="꣨" ++d259="꣩" ++d260="꣪" ++d261="꣫" ++d262="꣬" ++d263="꣭" ++d264="꣮" ++d265="꣯" ++d266="꣰" ++d267="꣱" ++d268="ꪰ" ++d269="ꪲ" ++d270="ꪳ" ++d271="ꪷ" ++d272="ꪸ" ++d273="ꪾ" ++d274="꪿" ++d275="꫁" ++d276="︠" ++d277="︡" ++d278="︢" ++d279="︣" ++d280="︤" ++d281="︥" ++d282="︦" ++d283="𐨏" ++d284="𐨸" ++d285="𝆅" ++d286="𝆆" ++d287="𝆇" ++d288="𝆈" ++d289="𝆉" ++d290="𝆪" ++d291="𝆫" ++d292="𝆬" ++d293="𝆭" ++d294="𝉂" ++d295="𝉃" ++d296="𝉄" ++ ++num_diacritics="297" ++ ++placeholder="􎻮" ++ ++##################################################################### ++# Upload the image and print the placeholder ++##################################################################### ++ ++upload_image_and_print_placeholder +diff --git a/khash.h b/khash.h +new file mode 100644 +index 0000000..f75f347 +--- /dev/null ++++ b/khash.h +@@ -0,0 +1,627 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, 2009, 2011 by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "khash.h" ++KHASH_MAP_INIT_INT(32, char) ++int main() { ++ int ret, is_missing; ++ khiter_t k; ++ khash_t(32) *h = kh_init(32); ++ k = kh_put(32, h, 5, &ret); ++ kh_value(h, k) = 10; ++ k = kh_get(32, h, 10); ++ is_missing = (k == kh_end(h)); ++ k = kh_get(32, h, 5); ++ kh_del(32, h, k); ++ for (k = kh_begin(h); k != kh_end(h); ++k) ++ if (kh_exist(h, k)) kh_value(h, k) = 1; ++ kh_destroy(32, h); ++ return 0; ++} ++*/ ++ ++/* ++ 2013-05-02 (0.2.8): ++ ++ * Use quadratic probing. When the capacity is power of 2, stepping function ++ i*(i+1)/2 guarantees to traverse each bucket. It is better than double ++ hashing on cache performance and is more robust than linear probing. ++ ++ In theory, double hashing should be more robust than quadratic probing. ++ However, my implementation is probably not for large hash tables, because ++ the second hash function is closely tied to the first hash function, ++ which reduce the effectiveness of double hashing. ++ ++ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php ++ ++ 2011-12-29 (0.2.7): ++ ++ * Minor code clean up; no actual effect. ++ ++ 2011-09-16 (0.2.6): ++ ++ * The capacity is a power of 2. This seems to dramatically improve the ++ speed for simple keys. Thank Zilong Tan for the suggestion. Reference: ++ ++ - http://code.google.com/p/ulib/ ++ - http://nothings.org/computer/judy/ ++ ++ * Allow to optionally use linear probing which usually has better ++ performance for random input. Double hashing is still the default as it ++ is more robust to certain non-random input. ++ ++ * Added Wang's integer hash function (not used by default). This hash ++ function is more robust to certain non-random input. ++ ++ 2011-02-14 (0.2.5): ++ ++ * Allow to declare global functions. ++ ++ 2009-09-26 (0.2.4): ++ ++ * Improve portability ++ ++ 2008-09-19 (0.2.3): ++ ++ * Corrected the example ++ * Improved interfaces ++ ++ 2008-09-11 (0.2.2): ++ ++ * Improved speed a little in kh_put() ++ ++ 2008-09-10 (0.2.1): ++ ++ * Added kh_clear() ++ * Fixed a compiling error ++ ++ 2008-09-02 (0.2.0): ++ ++ * Changed to token concatenation which increases flexibility. ++ ++ 2008-08-31 (0.1.2): ++ ++ * Fixed a bug in kh_get(), which has not been tested previously. ++ ++ 2008-08-31 (0.1.1): ++ ++ * Added destructor ++*/ ++ ++ ++#ifndef __AC_KHASH_H ++#define __AC_KHASH_H ++ ++/*! ++ @header ++ ++ Generic hash table library. ++ */ ++ ++#define AC_VERSION_KHASH_H "0.2.8" ++ ++#include ++#include ++#include ++ ++/* compiler specific configuration */ ++ ++#if UINT_MAX == 0xffffffffu ++typedef unsigned int khint32_t; ++#elif ULONG_MAX == 0xffffffffu ++typedef unsigned long khint32_t; ++#endif ++ ++#if ULONG_MAX == ULLONG_MAX ++typedef unsigned long khint64_t; ++#else ++typedef unsigned long long khint64_t; ++#endif ++ ++#ifndef kh_inline ++#ifdef _MSC_VER ++#define kh_inline __inline ++#else ++#define kh_inline inline ++#endif ++#endif /* kh_inline */ ++ ++#ifndef klib_unused ++#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) ++#define klib_unused __attribute__ ((__unused__)) ++#else ++#define klib_unused ++#endif ++#endif /* klib_unused */ ++ ++typedef khint32_t khint_t; ++typedef khint_t khiter_t; ++ ++#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) ++#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) ++#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) ++#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) ++#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) ++#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) ++#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) ++ ++#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) ++ ++#ifndef kroundup32 ++#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++#endif ++ ++#ifndef kcalloc ++#define kcalloc(N,Z) calloc(N,Z) ++#endif ++#ifndef kmalloc ++#define kmalloc(Z) malloc(Z) ++#endif ++#ifndef krealloc ++#define krealloc(P,Z) realloc(P,Z) ++#endif ++#ifndef kfree ++#define kfree(P) free(P) ++#endif ++ ++static const double __ac_HASH_UPPER = 0.77; ++ ++#define __KHASH_TYPE(name, khkey_t, khval_t) \ ++ typedef struct kh_##name##_s { \ ++ khint_t n_buckets, size, n_occupied, upper_bound; \ ++ khint32_t *flags; \ ++ khkey_t *keys; \ ++ khval_t *vals; \ ++ } kh_##name##_t; ++ ++#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ ++ extern kh_##name##_t *kh_init_##name(void); \ ++ extern void kh_destroy_##name(kh_##name##_t *h); \ ++ extern void kh_clear_##name(kh_##name##_t *h); \ ++ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ ++ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ ++ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ ++ extern void kh_del_##name(kh_##name##_t *h, khint_t x); ++ ++#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ SCOPE kh_##name##_t *kh_init_##name(void) { \ ++ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ ++ } \ ++ SCOPE void kh_destroy_##name(kh_##name##_t *h) \ ++ { \ ++ if (h) { \ ++ kfree((void *)h->keys); kfree(h->flags); \ ++ kfree((void *)h->vals); \ ++ kfree(h); \ ++ } \ ++ } \ ++ SCOPE void kh_clear_##name(kh_##name##_t *h) \ ++ { \ ++ if (h && h->flags) { \ ++ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ ++ h->size = h->n_occupied = 0; \ ++ } \ ++ } \ ++ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ ++ { \ ++ if (h->n_buckets) { \ ++ khint_t k, i, last, mask, step = 0; \ ++ mask = h->n_buckets - 1; \ ++ k = __hash_func(key); i = k & mask; \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) return h->n_buckets; \ ++ } \ ++ return __ac_iseither(h->flags, i)? h->n_buckets : i; \ ++ } else return 0; \ ++ } \ ++ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ ++ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ ++ khint32_t *new_flags = 0; \ ++ khint_t j = 1; \ ++ { \ ++ kroundup32(new_n_buckets); \ ++ if (new_n_buckets < 4) new_n_buckets = 4; \ ++ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ ++ else { /* hash table size to be changed (shrink or expand); rehash */ \ ++ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (!new_flags) return -1; \ ++ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ ++ if (h->n_buckets < new_n_buckets) { /* expand */ \ ++ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (!new_keys) { kfree(new_flags); return -1; } \ ++ h->keys = new_keys; \ ++ if (kh_is_map) { \ ++ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ if (!new_vals) { kfree(new_flags); return -1; } \ ++ h->vals = new_vals; \ ++ } \ ++ } /* otherwise shrink */ \ ++ } \ ++ } \ ++ if (j) { /* rehashing is needed */ \ ++ for (j = 0; j != h->n_buckets; ++j) { \ ++ if (__ac_iseither(h->flags, j) == 0) { \ ++ khkey_t key = h->keys[j]; \ ++ khval_t val; \ ++ khint_t new_mask; \ ++ new_mask = new_n_buckets - 1; \ ++ if (kh_is_map) val = h->vals[j]; \ ++ __ac_set_isdel_true(h->flags, j); \ ++ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ ++ khint_t k, i, step = 0; \ ++ k = __hash_func(key); \ ++ i = k & new_mask; \ ++ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ ++ __ac_set_isempty_false(new_flags, i); \ ++ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ ++ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ ++ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ ++ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ ++ } else { /* write the element and jump out of the loop */ \ ++ h->keys[i] = key; \ ++ if (kh_is_map) h->vals[i] = val; \ ++ break; \ ++ } \ ++ } \ ++ } \ ++ } \ ++ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ ++ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ ++ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ ++ } \ ++ kfree(h->flags); /* free the working space */ \ ++ h->flags = new_flags; \ ++ h->n_buckets = new_n_buckets; \ ++ h->n_occupied = h->size; \ ++ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ ++ } \ ++ return 0; \ ++ } \ ++ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ ++ { \ ++ khint_t x; \ ++ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ ++ if (h->n_buckets > (h->size<<1)) { \ ++ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ ++ *ret = -1; return h->n_buckets; \ ++ } \ ++ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ ++ { \ ++ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ ++ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ ++ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ ++ else { \ ++ last = i; \ ++ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ ++ if (__ac_isdel(h->flags, i)) site = i; \ ++ i = (i + (++step)) & mask; \ ++ if (i == last) { x = site; break; } \ ++ } \ ++ if (x == h->n_buckets) { \ ++ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ ++ else x = i; \ ++ } \ ++ } \ ++ } \ ++ if (__ac_isempty(h->flags, x)) { /* not present at all */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; ++h->n_occupied; \ ++ *ret = 1; \ ++ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ ++ h->keys[x] = key; \ ++ __ac_set_isboth_false(h->flags, x); \ ++ ++h->size; \ ++ *ret = 2; \ ++ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ ++ return x; \ ++ } \ ++ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ ++ { \ ++ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ ++ __ac_set_isdel_true(h->flags, x); \ ++ --h->size; \ ++ } \ ++ } ++ ++#define KHASH_DECLARE(name, khkey_t, khval_t) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_PROTOTYPES(name, khkey_t, khval_t) ++ ++#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ __KHASH_TYPE(name, khkey_t, khval_t) \ ++ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ ++ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) ++ ++/* --- BEGIN OF HASH FUNCTIONS --- */ ++ ++/*! @function ++ @abstract Integer hash function ++ @param key The integer [khint32_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int_hash_func(key) (khint32_t)(key) ++/*! @function ++ @abstract Integer comparison function ++ */ ++#define kh_int_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract 64-bit integer hash function ++ @param key The integer [khint64_t] ++ @return The hash value [khint_t] ++ */ ++#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) ++/*! @function ++ @abstract 64-bit integer comparison function ++ */ ++#define kh_int64_hash_equal(a, b) ((a) == (b)) ++/*! @function ++ @abstract const char* hash function ++ @param s Pointer to a null terminated string ++ @return The hash value ++ */ ++static kh_inline khint_t __ac_X31_hash_string(const char *s) ++{ ++ khint_t h = (khint_t)*s; ++ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; ++ return h; ++} ++/*! @function ++ @abstract Another interface to const char* hash function ++ @param key Pointer to a null terminated string [const char*] ++ @return The hash value [khint_t] ++ */ ++#define kh_str_hash_func(key) __ac_X31_hash_string(key) ++/*! @function ++ @abstract Const char* comparison function ++ */ ++#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) ++ ++static kh_inline khint_t __ac_Wang_hash(khint_t key) ++{ ++ key += ~(key << 15); ++ key ^= (key >> 10); ++ key += (key << 3); ++ key ^= (key >> 6); ++ key += ~(key << 11); ++ key ^= (key >> 16); ++ return key; ++} ++#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) ++ ++/* --- END OF HASH FUNCTIONS --- */ ++ ++/* Other convenient macros... */ ++ ++/*! ++ @abstract Type of the hash table. ++ @param name Name of the hash table [symbol] ++ */ ++#define khash_t(name) kh_##name##_t ++ ++/*! @function ++ @abstract Initiate a hash table. ++ @param name Name of the hash table [symbol] ++ @return Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_init(name) kh_init_##name() ++ ++/*! @function ++ @abstract Destroy a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_destroy(name, h) kh_destroy_##name(h) ++ ++/*! @function ++ @abstract Reset a hash table without deallocating memory. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ */ ++#define kh_clear(name, h) kh_clear_##name(h) ++ ++/*! @function ++ @abstract Resize a hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param s New size [khint_t] ++ */ ++#define kh_resize(name, h, s) kh_resize_##name(h, s) ++ ++/*! @function ++ @abstract Insert a key to the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @param r Extra return code: -1 if the operation failed; ++ 0 if the key is present in the hash table; ++ 1 if the bucket is empty (never used); 2 if the element in ++ the bucket has been deleted [int*] ++ @return Iterator to the inserted element [khint_t] ++ */ ++#define kh_put(name, h, k, r) kh_put_##name(h, k, r) ++ ++/*! @function ++ @abstract Retrieve a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Key [type of keys] ++ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] ++ */ ++#define kh_get(name, h, k) kh_get_##name(h, k) ++ ++/*! @function ++ @abstract Remove a key from the hash table. ++ @param name Name of the hash table [symbol] ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param k Iterator to the element to be deleted [khint_t] ++ */ ++#define kh_del(name, h, k) kh_del_##name(h, k) ++ ++/*! @function ++ @abstract Test whether a bucket contains data. ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return 1 if containing data; 0 otherwise [int] ++ */ ++#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) ++ ++/*! @function ++ @abstract Get key given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Key [type of keys] ++ */ ++#define kh_key(h, x) ((h)->keys[x]) ++ ++/*! @function ++ @abstract Get value given an iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param x Iterator to the bucket [khint_t] ++ @return Value [type of values] ++ @discussion For hash sets, calling this results in segfault. ++ */ ++#define kh_val(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Alias of kh_val() ++ */ ++#define kh_value(h, x) ((h)->vals[x]) ++ ++/*! @function ++ @abstract Get the start iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The start iterator [khint_t] ++ */ ++#define kh_begin(h) (khint_t)(0) ++ ++/*! @function ++ @abstract Get the end iterator ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return The end iterator [khint_t] ++ */ ++#define kh_end(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Get the number of elements in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of elements in the hash table [khint_t] ++ */ ++#define kh_size(h) ((h)->size) ++ ++/*! @function ++ @abstract Get the number of buckets in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @return Number of buckets in the hash table [khint_t] ++ */ ++#define kh_n_buckets(h) ((h)->n_buckets) ++ ++/*! @function ++ @abstract Iterate over the entries in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param kvar Variable to which key will be assigned ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (kvar) = kh_key(h,__i); \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/*! @function ++ @abstract Iterate over the values in the hash table ++ @param h Pointer to the hash table [khash_t(name)*] ++ @param vvar Variable to which value will be assigned ++ @param code Block of code to execute ++ */ ++#define kh_foreach_value(h, vvar, code) { khint_t __i; \ ++ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ ++ if (!kh_exist(h,__i)) continue; \ ++ (vvar) = kh_val(h,__i); \ ++ code; \ ++ } } ++ ++/* More convenient interfaces */ ++ ++/*! @function ++ @abstract Instantiate a hash set containing integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT(name) \ ++ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT(name, khval_t) \ ++ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash set containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_INT64(name) \ ++ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing 64-bit integer keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_INT64(name, khval_t) \ ++ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) ++ ++typedef const char *kh_cstr_t; ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ */ ++#define KHASH_SET_INIT_STR(name) \ ++ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) ++ ++/*! @function ++ @abstract Instantiate a hash map containing const char* keys ++ @param name Name of the hash table [symbol] ++ @param khval_t Type of values [type] ++ */ ++#define KHASH_MAP_INIT_STR(name, khval_t) \ ++ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) ++ ++#endif /* __AC_KHASH_H */ +diff --git a/kvec.h b/kvec.h +new file mode 100644 +index 0000000..10f1c5b +--- /dev/null ++++ b/kvec.h +@@ -0,0 +1,90 @@ ++/* The MIT License ++ ++ Copyright (c) 2008, by Attractive Chaos ++ ++ Permission is hereby granted, free of charge, to any person obtaining ++ a copy of this software and associated documentation files (the ++ "Software"), to deal in the Software without restriction, including ++ without limitation the rights to use, copy, modify, merge, publish, ++ distribute, sublicense, and/or sell copies of the Software, and to ++ permit persons to whom the Software is furnished to do so, subject to ++ the following conditions: ++ ++ The above copyright notice and this permission notice shall be ++ included in all copies or substantial portions of the Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS ++ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ++ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ SOFTWARE. ++*/ ++ ++/* ++ An example: ++ ++#include "kvec.h" ++int main() { ++ kvec_t(int) array; ++ kv_init(array); ++ kv_push(int, array, 10); // append ++ kv_a(int, array, 20) = 5; // dynamic ++ kv_A(array, 20) = 4; // static ++ kv_destroy(array); ++ return 0; ++} ++*/ ++ ++/* ++ 2008-09-22 (0.1.0): ++ ++ * The initial version. ++ ++*/ ++ ++#ifndef AC_KVEC_H ++#define AC_KVEC_H ++ ++#include ++ ++#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) ++ ++#define kvec_t(type) struct { size_t n, m; type *a; } ++#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) ++#define kv_destroy(v) free((v).a) ++#define kv_A(v, i) ((v).a[(i)]) ++#define kv_pop(v) ((v).a[--(v).n]) ++#define kv_size(v) ((v).n) ++#define kv_max(v) ((v).m) ++ ++#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) ++ ++#define kv_copy(type, v1, v0) do { \ ++ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ ++ (v1).n = (v0).n; \ ++ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ ++ } while (0) \ ++ ++#define kv_push(type, v, x) do { \ ++ if ((v).n == (v).m) { \ ++ (v).m = (v).m? (v).m<<1 : 2; \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ ++ } \ ++ (v).a[(v).n++] = (x); \ ++ } while (0) ++ ++#define kv_pushp(type, v) ((((v).n == (v).m)? \ ++ ((v).m = ((v).m? (v).m<<1 : 2), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : 0), ((v).a + ((v).n++))) ++ ++#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ ++ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ ++ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ ++ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ ++ : 0), (v).a[(i)]) ++ ++#endif +diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c +new file mode 100644 +index 0000000..829c0fc +--- /dev/null ++++ b/rowcolumn_diacritics_helpers.c +@@ -0,0 +1,391 @@ ++#include ++ ++uint16_t diacritic_to_num(uint32_t code) ++{ ++ switch (code) { ++ case 0x305: ++ return code - 0x305 + 1; ++ case 0x30d: ++ case 0x30e: ++ return code - 0x30d + 2; ++ case 0x310: ++ return code - 0x310 + 4; ++ case 0x312: ++ return code - 0x312 + 5; ++ case 0x33d: ++ case 0x33e: ++ case 0x33f: ++ return code - 0x33d + 6; ++ case 0x346: ++ return code - 0x346 + 9; ++ case 0x34a: ++ case 0x34b: ++ case 0x34c: ++ return code - 0x34a + 10; ++ case 0x350: ++ case 0x351: ++ case 0x352: ++ return code - 0x350 + 13; ++ case 0x357: ++ return code - 0x357 + 16; ++ case 0x35b: ++ return code - 0x35b + 17; ++ case 0x363: ++ case 0x364: ++ case 0x365: ++ case 0x366: ++ case 0x367: ++ case 0x368: ++ case 0x369: ++ case 0x36a: ++ case 0x36b: ++ case 0x36c: ++ case 0x36d: ++ case 0x36e: ++ case 0x36f: ++ return code - 0x363 + 18; ++ case 0x483: ++ case 0x484: ++ case 0x485: ++ case 0x486: ++ case 0x487: ++ return code - 0x483 + 31; ++ case 0x592: ++ case 0x593: ++ case 0x594: ++ case 0x595: ++ return code - 0x592 + 36; ++ case 0x597: ++ case 0x598: ++ case 0x599: ++ return code - 0x597 + 40; ++ case 0x59c: ++ case 0x59d: ++ case 0x59e: ++ case 0x59f: ++ case 0x5a0: ++ case 0x5a1: ++ return code - 0x59c + 43; ++ case 0x5a8: ++ case 0x5a9: ++ return code - 0x5a8 + 49; ++ case 0x5ab: ++ case 0x5ac: ++ return code - 0x5ab + 51; ++ case 0x5af: ++ return code - 0x5af + 53; ++ case 0x5c4: ++ return code - 0x5c4 + 54; ++ case 0x610: ++ case 0x611: ++ case 0x612: ++ case 0x613: ++ case 0x614: ++ case 0x615: ++ case 0x616: ++ case 0x617: ++ return code - 0x610 + 55; ++ case 0x657: ++ case 0x658: ++ case 0x659: ++ case 0x65a: ++ case 0x65b: ++ return code - 0x657 + 63; ++ case 0x65d: ++ case 0x65e: ++ return code - 0x65d + 68; ++ case 0x6d6: ++ case 0x6d7: ++ case 0x6d8: ++ case 0x6d9: ++ case 0x6da: ++ case 0x6db: ++ case 0x6dc: ++ return code - 0x6d6 + 70; ++ case 0x6df: ++ case 0x6e0: ++ case 0x6e1: ++ case 0x6e2: ++ return code - 0x6df + 77; ++ case 0x6e4: ++ return code - 0x6e4 + 81; ++ case 0x6e7: ++ case 0x6e8: ++ return code - 0x6e7 + 82; ++ case 0x6eb: ++ case 0x6ec: ++ return code - 0x6eb + 84; ++ case 0x730: ++ return code - 0x730 + 86; ++ case 0x732: ++ case 0x733: ++ return code - 0x732 + 87; ++ case 0x735: ++ case 0x736: ++ return code - 0x735 + 89; ++ case 0x73a: ++ return code - 0x73a + 91; ++ case 0x73d: ++ return code - 0x73d + 92; ++ case 0x73f: ++ case 0x740: ++ case 0x741: ++ return code - 0x73f + 93; ++ case 0x743: ++ return code - 0x743 + 96; ++ case 0x745: ++ return code - 0x745 + 97; ++ case 0x747: ++ return code - 0x747 + 98; ++ case 0x749: ++ case 0x74a: ++ return code - 0x749 + 99; ++ case 0x7eb: ++ case 0x7ec: ++ case 0x7ed: ++ case 0x7ee: ++ case 0x7ef: ++ case 0x7f0: ++ case 0x7f1: ++ return code - 0x7eb + 101; ++ case 0x7f3: ++ return code - 0x7f3 + 108; ++ case 0x816: ++ case 0x817: ++ case 0x818: ++ case 0x819: ++ return code - 0x816 + 109; ++ case 0x81b: ++ case 0x81c: ++ case 0x81d: ++ case 0x81e: ++ case 0x81f: ++ case 0x820: ++ case 0x821: ++ case 0x822: ++ case 0x823: ++ return code - 0x81b + 113; ++ case 0x825: ++ case 0x826: ++ case 0x827: ++ return code - 0x825 + 122; ++ case 0x829: ++ case 0x82a: ++ case 0x82b: ++ case 0x82c: ++ case 0x82d: ++ return code - 0x829 + 125; ++ case 0x951: ++ return code - 0x951 + 130; ++ case 0x953: ++ case 0x954: ++ return code - 0x953 + 131; ++ case 0xf82: ++ case 0xf83: ++ return code - 0xf82 + 133; ++ case 0xf86: ++ case 0xf87: ++ return code - 0xf86 + 135; ++ case 0x135d: ++ case 0x135e: ++ case 0x135f: ++ return code - 0x135d + 137; ++ case 0x17dd: ++ return code - 0x17dd + 140; ++ case 0x193a: ++ return code - 0x193a + 141; ++ case 0x1a17: ++ return code - 0x1a17 + 142; ++ case 0x1a75: ++ case 0x1a76: ++ case 0x1a77: ++ case 0x1a78: ++ case 0x1a79: ++ case 0x1a7a: ++ case 0x1a7b: ++ case 0x1a7c: ++ return code - 0x1a75 + 143; ++ case 0x1b6b: ++ return code - 0x1b6b + 151; ++ case 0x1b6d: ++ case 0x1b6e: ++ case 0x1b6f: ++ case 0x1b70: ++ case 0x1b71: ++ case 0x1b72: ++ case 0x1b73: ++ return code - 0x1b6d + 152; ++ case 0x1cd0: ++ case 0x1cd1: ++ case 0x1cd2: ++ return code - 0x1cd0 + 159; ++ case 0x1cda: ++ case 0x1cdb: ++ return code - 0x1cda + 162; ++ case 0x1ce0: ++ return code - 0x1ce0 + 164; ++ case 0x1dc0: ++ case 0x1dc1: ++ return code - 0x1dc0 + 165; ++ case 0x1dc3: ++ case 0x1dc4: ++ case 0x1dc5: ++ case 0x1dc6: ++ case 0x1dc7: ++ case 0x1dc8: ++ case 0x1dc9: ++ return code - 0x1dc3 + 167; ++ case 0x1dcb: ++ case 0x1dcc: ++ return code - 0x1dcb + 174; ++ case 0x1dd1: ++ case 0x1dd2: ++ case 0x1dd3: ++ case 0x1dd4: ++ case 0x1dd5: ++ case 0x1dd6: ++ case 0x1dd7: ++ case 0x1dd8: ++ case 0x1dd9: ++ case 0x1dda: ++ case 0x1ddb: ++ case 0x1ddc: ++ case 0x1ddd: ++ case 0x1dde: ++ case 0x1ddf: ++ case 0x1de0: ++ case 0x1de1: ++ case 0x1de2: ++ case 0x1de3: ++ case 0x1de4: ++ case 0x1de5: ++ case 0x1de6: ++ return code - 0x1dd1 + 176; ++ case 0x1dfe: ++ return code - 0x1dfe + 198; ++ case 0x20d0: ++ case 0x20d1: ++ return code - 0x20d0 + 199; ++ case 0x20d4: ++ case 0x20d5: ++ case 0x20d6: ++ case 0x20d7: ++ return code - 0x20d4 + 201; ++ case 0x20db: ++ case 0x20dc: ++ return code - 0x20db + 205; ++ case 0x20e1: ++ return code - 0x20e1 + 207; ++ case 0x20e7: ++ return code - 0x20e7 + 208; ++ case 0x20e9: ++ return code - 0x20e9 + 209; ++ case 0x20f0: ++ return code - 0x20f0 + 210; ++ case 0x2cef: ++ case 0x2cf0: ++ case 0x2cf1: ++ return code - 0x2cef + 211; ++ case 0x2de0: ++ case 0x2de1: ++ case 0x2de2: ++ case 0x2de3: ++ case 0x2de4: ++ case 0x2de5: ++ case 0x2de6: ++ case 0x2de7: ++ case 0x2de8: ++ case 0x2de9: ++ case 0x2dea: ++ case 0x2deb: ++ case 0x2dec: ++ case 0x2ded: ++ case 0x2dee: ++ case 0x2def: ++ case 0x2df0: ++ case 0x2df1: ++ case 0x2df2: ++ case 0x2df3: ++ case 0x2df4: ++ case 0x2df5: ++ case 0x2df6: ++ case 0x2df7: ++ case 0x2df8: ++ case 0x2df9: ++ case 0x2dfa: ++ case 0x2dfb: ++ case 0x2dfc: ++ case 0x2dfd: ++ case 0x2dfe: ++ case 0x2dff: ++ return code - 0x2de0 + 214; ++ case 0xa66f: ++ return code - 0xa66f + 246; ++ case 0xa67c: ++ case 0xa67d: ++ return code - 0xa67c + 247; ++ case 0xa6f0: ++ case 0xa6f1: ++ return code - 0xa6f0 + 249; ++ case 0xa8e0: ++ case 0xa8e1: ++ case 0xa8e2: ++ case 0xa8e3: ++ case 0xa8e4: ++ case 0xa8e5: ++ case 0xa8e6: ++ case 0xa8e7: ++ case 0xa8e8: ++ case 0xa8e9: ++ case 0xa8ea: ++ case 0xa8eb: ++ case 0xa8ec: ++ case 0xa8ed: ++ case 0xa8ee: ++ case 0xa8ef: ++ case 0xa8f0: ++ case 0xa8f1: ++ return code - 0xa8e0 + 251; ++ case 0xaab0: ++ return code - 0xaab0 + 269; ++ case 0xaab2: ++ case 0xaab3: ++ return code - 0xaab2 + 270; ++ case 0xaab7: ++ case 0xaab8: ++ return code - 0xaab7 + 272; ++ case 0xaabe: ++ case 0xaabf: ++ return code - 0xaabe + 274; ++ case 0xaac1: ++ return code - 0xaac1 + 276; ++ case 0xfe20: ++ case 0xfe21: ++ case 0xfe22: ++ case 0xfe23: ++ case 0xfe24: ++ case 0xfe25: ++ case 0xfe26: ++ return code - 0xfe20 + 277; ++ case 0x10a0f: ++ return code - 0x10a0f + 284; ++ case 0x10a38: ++ return code - 0x10a38 + 285; ++ case 0x1d185: ++ case 0x1d186: ++ case 0x1d187: ++ case 0x1d188: ++ case 0x1d189: ++ return code - 0x1d185 + 286; ++ case 0x1d1aa: ++ case 0x1d1ab: ++ case 0x1d1ac: ++ case 0x1d1ad: ++ return code - 0x1d1aa + 291; ++ case 0x1d242: ++ case 0x1d243: ++ case 0x1d244: ++ return code - 0x1d242 + 295; ++ } ++ return 0; ++} +diff --git a/st.c b/st.c +index 8e57991..9824ea6 100644 +--- a/st.c ++++ b/st.c +@@ -19,6 +19,7 @@ + + #include "st.h" + #include "win.h" ++#include "graphics.h" + + #if defined(__linux) + #include +@@ -36,6 +37,10 @@ + #define STR_BUF_SIZ ESC_BUF_SIZ + #define STR_ARG_SIZ ESC_ARG_SIZ + ++/* PUA character used as an image placeholder */ ++#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE ++#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE ++ + /* macros */ + #define IS_SET(flag) ((term.mode & (flag)) != 0) + #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +@@ -113,6 +118,8 @@ typedef struct { + typedef struct { + int row; /* nb row */ + int col; /* nb col */ ++ int pixw; /* width of the text area in pixels */ ++ int pixh; /* height of the text area in pixels */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ +@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); + static char utf8encodebyte(Rune, size_t); + static size_t utf8validate(Rune *, size_t); + +-static char *base64dec(const char *); + static char base64dec_getc(const char **); + + static ssize_t xwrite(int, const char *, size_t); +@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; + static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; + static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + ++/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 ++ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ ++uint16_t diacritic_to_num(uint32_t code); ++ + ssize_t + xwrite(int fd, const char *s, size_t len) + { +@@ -616,6 +626,12 @@ getsel(void) + if (gp->mode & ATTR_WDUMMY) + continue; + ++ if (gp->mode & ATTR_IMAGE) { ++ // TODO: Copy diacritics as well ++ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); ++ continue; ++ } ++ + ptr += utf8encode(gp->u, ptr); + } + +@@ -715,11 +731,14 @@ sigchld(int a) + int stat; + pid_t p; + +- if ((p = waitpid(pid, &stat, WNOHANG)) < 0) ++ if ((p = waitpid(-1, &stat, WNOHANG)) < 0) + die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); + +- if (pid != p) ++ if (pid != p) { ++ /* reinstall sigchld handler */ ++ signal(SIGCHLD, sigchld); + return; ++ } + + if (WIFEXITED(stat) && WEXITSTATUS(stat)) + die("child exited with status %d\n", WEXITSTATUS(stat)); +@@ -819,7 +838,11 @@ ttyread(void) + { + static char buf[BUFSIZ]; + static int buflen = 0; +- int ret, written; ++ static int already_processing = 0; ++ int ret, written = 0; ++ ++ if (buflen >= LEN(buf)) ++ return 0; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); +@@ -831,7 +854,24 @@ ttyread(void) + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; +- written = twrite(buf, buflen, 0); ++ if (already_processing) { ++ /* Avoid recursive call to twrite() */ ++ return ret; ++ } ++ already_processing = 1; ++ while (1) { ++ int buflen_before_processing = buflen; ++ written += twrite(buf + written, buflen - written, 0); ++ // If buflen changed during the call to twrite, there is ++ // new data, and we need to keep processing, otherwise ++ // we can exit. This will not loop forever because the ++ // buffer is limited, and we don't clean it in this ++ // loop, so at some point ttywrite will have to drop ++ // some data. ++ if (buflen_before_processing == buflen) ++ break; ++ } ++ already_processing = 0; + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) +@@ -874,6 +914,7 @@ ttywriteraw(const char *s, size_t n) + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; ++ int retries_left = 100; + + /* + * Remember that we are using a pty, which might be a modem line. +@@ -882,6 +923,9 @@ ttywriteraw(const char *s, size_t n) + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { ++ if (retries_left-- <= 0) ++ goto too_many_retries; ++ + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); +@@ -923,11 +967,16 @@ ttywriteraw(const char *s, size_t n) + + write_error: + die("write error on tty: %s\n", strerror(errno)); ++too_many_retries: ++ fprintf(stderr, "Could not write %zu bytes to tty\n", n); + } + + void + ttyresize(int tw, int th) + { ++ term.pixw = tw; ++ term.pixh = th; ++ + struct winsize w; + + w.ws_row = term.row; +@@ -1015,7 +1064,8 @@ treset(void) + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, +- .bg = defaultbg ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); +@@ -1038,7 +1088,9 @@ treset(void) + void + tnew(int col, int row) + { +- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; ++ term = (Term){.c = {.attr = {.fg = defaultfg, ++ .bg = defaultbg, ++ .decor = DECOR_DEFAULT_COLOR}}}; + tresize(col, row); + treset(); + } +@@ -1215,9 +1267,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + ++ if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE && ++ tgetisclassicplaceholder(&term.line[y][x])) { ++ // This is a workaround: don't overwrite classic placement ++ // placeholders with space symbols (unlike Unicode placeholders ++ // which must be overwritten by anything). ++ term.line[y][x].bg = attr->bg; ++ term.dirty[y] = 1; ++ return; ++ } ++ + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; ++ ++ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) { ++ term.line[y][x].u = 0; ++ term.line[y][x].mode |= ATTR_IMAGE; ++ } + } + + void +@@ -1244,12 +1311,110 @@ tclearregion(int x1, int y1, int x2, int y2) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; ++ gp->decor = term.c.attr.decor; + gp->mode = 0; + gp->u = ' '; + } + } + } + ++/// Fills a rectangle area with an image placeholder. The starting point is the ++/// cursor. Adds empty lines if needed. The placeholder will be marked as ++/// classic. ++void tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id, int cols, ++ int rows, char do_not_move_cursor, ++ Glyph *text_underneath) { ++ for (int row = 0; row < rows; ++row) { ++ int y = term.c.y; ++ term.dirty[y] = 1; ++ for (int col = 0; col < cols; ++col) { ++ int x = term.c.x + col; ++ if (x >= term.col) ++ break; ++ Glyph *gp = &term.line[y][x]; ++ if (selected(x, y)) ++ selclear(); ++ if (text_underneath) { ++ Glyph *to_save = gp; ++ // If there is already a classic placeholder, ++ // use the text underneath it. This will leave ++ // holes in images, but at least we are ++ // guaranteed to restore the original text. ++ if (gp->mode & ATTR_IMAGE && ++ tgetisclassicplaceholder(gp)) { ++ Glyph *under = ++ gr_get_glyph_underneath_image( ++ tgetimgid(gp), ++ tgetimgplacementid(gp), ++ tgetimgcol(gp), ++ tgetimgrow(gp)); ++ if (under) ++ to_save = under; ++ } ++ text_underneath[cols * row + col] = *to_save; ++ } ++ gp->mode = ATTR_IMAGE; ++ gp->u = 0; ++ tsetimgrow(gp, row + 1); ++ tsetimgcol(gp, col + 1); ++ tsetimgid(gp, image_id); ++ tsetimgplacementid(gp, placement_id); ++ tsetimgdiacriticcount(gp, 3); ++ tsetisclassicplaceholder(gp, 1); ++ } ++ // If moving the cursor is not allowed and this is the last line ++ // of the terminal, we are done. ++ if (do_not_move_cursor && y == term.row - 1) ++ break; ++ // Move the cursor down, maybe creating a new line. The x is ++ // preserved (we never change term.c.x in the loop above). ++ if (row != rows - 1) ++ tnewline(/*first_col=*/0); ++ } ++ if (do_not_move_cursor) { ++ // Return the cursor to the original position. ++ tmoveto(term.c.x, term.c.y - rows + 1); ++ } else { ++ // Move the cursor beyond the last column, as required by the ++ // protocol. If the cursor goes beyond the screen edge, insert a ++ // newline to match the behavior of kitty. ++ if (term.c.x + cols >= term.col) ++ tnewline(/*first_col=*/1); ++ else ++ tmoveto(term.c.x + cols, term.c.y); ++ } ++} ++ ++void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp), ++ void *data) { ++ for (int row = 0; row < term.row; ++row) { ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ if (callback(data, gp)) ++ term.dirty[row] = 1; ++ } ++ } ++ } ++} ++ ++void gr_schedule_image_redraw_by_id(uint32_t image_id) { ++ for (int row = 0; row < term.row; ++row) { ++ if (term.dirty[row]) ++ continue; ++ for (int col = 0; col < term.col; ++col) { ++ Glyph *gp = &term.line[row][col]; ++ if (gp->mode & ATTR_IMAGE) { ++ uint32_t cell_image_id = tgetimgid(gp); ++ if (cell_image_id == image_id) { ++ term.dirty[row] = 1; ++ break; ++ } ++ } ++ } ++ } ++} ++ + void + tdeletechar(int n) + { +@@ -1368,6 +1533,7 @@ tsetattr(const int *attr, int l) + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; ++ term.c.attr.decor = DECOR_DEFAULT_COLOR; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; +@@ -1380,6 +1546,20 @@ tsetattr(const int *attr, int l) + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; ++ if (i + 1 < l) { ++ idx = attr[++i]; ++ if (BETWEEN(idx, 1, 5)) { ++ tsetdecorstyle(&term.c.attr, idx); ++ } else if (idx == 0) { ++ term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); ++ } else { ++ fprintf(stderr, ++ "erresc: unknown underline " ++ "style %d\n", ++ idx); ++ } ++ } + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ +@@ -1403,6 +1583,7 @@ tsetattr(const int *attr, int l) + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; ++ tsetdecorstyle(&term.c.attr, 0); + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; +@@ -1431,10 +1612,11 @@ tsetattr(const int *attr, int l) + term.c.attr.bg = defaultbg; + break; + case 58: +- /* This starts a sequence to change the color of +- * "underline" pixels. We don't support that and +- * instead eat up a following "5;n" or "2;r;g;b". */ +- tdefcolor(attr, &i, l); ++ if ((idx = tdefcolor(attr, &i, l)) >= 0) ++ tsetdecorcolor(&term.c.attr, idx); ++ break; ++ case 59: ++ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR); + break; + default: + if (BETWEEN(attr[i], 30, 37)) { +@@ -1823,6 +2005,39 @@ csihandle(void) + goto unknown; + } + break; ++ case '>': ++ switch (csiescseq.mode[1]) { ++ case 'q': /* XTVERSION -- Print terminal name and version */ ++ len = snprintf(buf, sizeof(buf), ++ "\033P>|st-graphics(%s)\033\\", VERSION); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; ++ case 't': /* XTWINOPS -- Window manipulation */ ++ switch (csiescseq.arg[0]) { ++ case 14: /* Report text area size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[4;%i;%it", ++ term.pixh, term.pixw); ++ ttywrite(buf, len, 0); ++ break; ++ case 16: /* Report character cell size in pixels. */ ++ len = snprintf(buf, sizeof(buf), "\033[6;%i;%it", ++ term.pixh / term.row, ++ term.pixw / term.col); ++ ttywrite(buf, len, 0); ++ break; ++ case 18: /* Report the size of the text area in characters. */ ++ len = snprintf(buf, sizeof(buf), "\033[8;%i;%it", ++ term.row, term.col); ++ ttywrite(buf, len, 0); ++ break; ++ default: ++ goto unknown; ++ } ++ break; + } + } + +@@ -1985,8 +2200,27 @@ strhandle(void) + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; +- case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ ++ if (gr_parse_command(strescseq.buf, strescseq.len)) { ++ GraphicsCommandResult *res = &graphics_command_result; ++ if (res->create_placeholder) { ++ tcreateimgplaceholder( ++ res->placeholder.image_id, ++ res->placeholder.placement_id, ++ res->placeholder.columns, ++ res->placeholder.rows, ++ res->placeholder.do_not_move_cursor, ++ res->placeholder.text_underneath); ++ } ++ if (res->response[0]) ++ ttywrite(res->response, strlen(res->response), ++ 0); ++ if (res->redraw) ++ tfulldirt(); ++ return; ++ } ++ return; ++ case 'P': /* DCS -- Device Control String */ + case '^': /* PM -- Privacy Message */ + return; + } +@@ -2492,6 +2726,41 @@ check_control_code: + if (selected(term.c.x, term.c.y)) + selclear(); + ++ // wcwidth is broken on some systems, set the width to 0 if it's a known ++ // diacritic used for images. ++ uint16_t num = diacritic_to_num(u); ++ if (num != 0) ++ width = 0; ++ // Set the width to 1 if it's an image placeholder character. ++ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) ++ width = 1; ++ ++ if (width == 0) { ++ // It's probably a combining char. Combining characters are not ++ // supported, so we just ignore them, unless it denotes the row and ++ // column of an image character. ++ if (term.c.y <= 0 && term.c.x <= 0) ++ return; ++ else if (term.c.x == 0) ++ gp = &term.line[term.c.y-1][term.col-1]; ++ else if (term.c.state & CURSOR_WRAPNEXT) ++ gp = &term.line[term.c.y][term.c.x]; ++ else ++ gp = &term.line[term.c.y][term.c.x-1]; ++ if (num && (gp->mode & ATTR_IMAGE)) { ++ unsigned diaccount = tgetimgdiacriticcount(gp); ++ if (diaccount == 0) ++ tsetimgrow(gp, num); ++ else if (diaccount == 1) ++ tsetimgcol(gp, num); ++ else if (diaccount == 2) ++ tsetimg4thbyteplus1(gp, num); ++ tsetimgdiacriticcount(gp, diaccount + 1); ++ } ++ term.lastc = u; ++ return; ++ } ++ + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; +@@ -2658,6 +2927,8 @@ drawregion(int x1, int y1, int x2, int y2) + { + int y; + ++ xstartimagedraw(term.dirty, term.row); ++ + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; +@@ -2665,6 +2936,8 @@ drawregion(int x1, int y1, int x2, int y2) + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } ++ ++ xfinishimagedraw(); + } + + void +@@ -2699,3 +2972,9 @@ redraw(void) + tfulldirt(); + draw(); + } ++ ++Glyph ++getglyphat(int col, int row) ++{ ++ return term.line[row][col]; ++} +diff --git a/st.h b/st.h +index fd3b0d8..c5dd731 100644 +--- a/st.h ++++ b/st.h +@@ -12,7 +12,7 @@ + #define DEFAULT(a, b) (a) = (a) ? (a) : (b) + #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) + #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ +- (a).bg != (b).bg) ++ (a).bg != (b).bg || (a).decor != (b).decor) + #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) + #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) +@@ -20,6 +20,10 @@ + #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) + #define IS_TRUECOL(x) (1 << 24 & (x)) + ++// This decor color indicates that the fg color should be used. Note that it's ++// not a 24-bit color because the 25-th bit is not set. ++#define DECOR_DEFAULT_COLOR 0x0ffffff ++ + enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, +@@ -34,6 +38,7 @@ enum glyph_attribute { + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, ++ ATTR_IMAGE = 1 << 14, + }; + + enum selection_mode { +@@ -52,6 +57,14 @@ enum selection_snap { + SNAP_LINE = 2 + }; + ++enum underline_style { ++ UNDERLINE_STRAIGHT = 1, ++ UNDERLINE_DOUBLE = 2, ++ UNDERLINE_CURLY = 3, ++ UNDERLINE_DOTTED = 4, ++ UNDERLINE_DASHED = 5, ++}; ++ + typedef unsigned char uchar; + typedef unsigned int uint; + typedef unsigned long ulong; +@@ -65,6 +78,7 @@ typedef struct { + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ ++ uint32_t decor; /* decoration (like underline) */ + } Glyph; + + typedef Glyph *Line; +@@ -105,6 +119,8 @@ void selextend(int, int, int, int); + int selected(int, int); + char *getsel(void); + ++Glyph getglyphat(int, int); ++ + size_t utf8encode(Rune, char *); + + void *xmalloc(size_t); +@@ -124,3 +140,69 @@ extern unsigned int tabspaces; + extern unsigned int defaultfg; + extern unsigned int defaultbg; + extern unsigned int defaultcs; ++ ++// Accessors to decoration properties stored in `decor`. ++// The 25-th bit is used to indicate if it's a 24-bit color. ++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; } ++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; } ++static inline void tsetdecorcolor(Glyph *g, uint32_t color) { ++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff); ++} ++static inline void tsetdecorstyle(Glyph *g, uint32_t style) { ++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25); ++} ++ ++ ++// Some accessors to image placeholder properties stored in `u`: ++// - row (1-base) - 9 bits ++// - column (1-base) - 9 bits ++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified, ++// don't forget to subtract 1). ++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits ++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit ++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; } ++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; } ++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; } ++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; } ++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; } ++static inline void tsetimgrow(Glyph *g, uint32_t row) { ++ g->u = (g->u & ~0x1ff) | (row & 0x1ff); ++} ++static inline void tsetimgcol(Glyph *g, uint32_t col) { ++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9); ++} ++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) { ++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18); ++} ++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) { ++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27); ++} ++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) { ++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29); ++} ++ ++/// Returns the full image id. This is a naive implementation, if the most ++/// significant byte is not specified, it's assumed to be 0 instead of inferring ++/// it from the cells to the left. ++static inline uint32_t tgetimgid(Glyph *g) { ++ uint32_t msb = tgetimgid4thbyteplus1(g); ++ if (msb != 0) ++ --msb; ++ return (msb << 24) | (g->fg & 0xFFFFFF); ++} ++ ++/// Sets the full image id. ++static inline void tsetimgid(Glyph *g, uint32_t id) { ++ g->fg = (id & 0xFFFFFF) | (1 << 24); ++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1); ++} ++ ++static inline uint32_t tgetimgplacementid(Glyph *g) { ++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR) ++ return 0; ++ return g->decor & 0xFFFFFF; ++} ++ ++static inline void tsetimgplacementid(Glyph *g, uint32_t id) { ++ g->decor = (id & 0xFFFFFF) | (1 << 24); ++} +diff --git a/st.info b/st.info +index efab2cf..ded76c1 100644 +--- a/st.info ++++ b/st.info +@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, ++ Smulx=\E[4:%p1%dm, + + st| simpleterm, + use=st-mono, +@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, ++# Underline colors ++ Su, ++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m, ++ Setulc1=\E[58:5:%p1%dm, ++ ol=\E[59m, + + st-meta| simpleterm with meta key, + use=st, +diff --git a/win.h b/win.h +index 6de960d..31b3fff 100644 +--- a/win.h ++++ b/win.h +@@ -39,3 +39,6 @@ void xsetpointermotion(int); + void xsetsel(char *); + int xstartdraw(void); + void xximspot(int, int); ++ ++void xstartimagedraw(int *dirty, int rows); ++void xfinishimagedraw(); +diff --git a/x.c b/x.c +index d73152b..6f1bf8c 100644 +--- a/x.c ++++ b/x.c +@@ -4,6 +4,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -19,6 +21,7 @@ char *argv0; + #include "arg.h" + #include "st.h" + #include "win.h" ++#include "graphics.h" + + /* types used in config.h */ + typedef struct { +@@ -59,6 +62,12 @@ static void zoom(const Arg *); + static void zoomabs(const Arg *); + static void zoomreset(const Arg *); + static void ttysend(const Arg *); ++static void previewimage(const Arg *); ++static void showimageinfo(const Arg *); ++static void togglegrdebug(const Arg *); ++static void dumpgrstate(const Arg *); ++static void unloadimages(const Arg *); ++static void toggleimages(const Arg *); + + /* config.h for applying patches and the configuration. */ + #include "config.h" +@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec; + typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ ++ int hborderpx, vborderpx; + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ +@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int); + static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); + static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); + static void xdrawglyph(Glyph, int, int); ++static void xdrawimages(Glyph, Line, int x1, int y1, int x2); ++static void xdrawoneimagecell(Glyph, int x, int y); + static void xclear(int, int, int, int); + static int xgeommasktogravity(int); + static int ximopen(Display *); +@@ -220,6 +232,7 @@ static DC dc; + static XWindow xw; + static XSelection xsel; + static TermWindow win; ++static unsigned int mouse_col = 0, mouse_row = 0; + + /* Font Ring Cache */ + enum { +@@ -328,10 +341,72 @@ ttysend(const Arg *arg) + ttywrite(arg->s, strlen(arg->s), 1); + } + ++void ++previewimage(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ gr_preview_image(image_id, arg->s); ++ } ++} ++ ++void ++showimageinfo(const Arg *arg) ++{ ++ Glyph g = getglyphat(mouse_col, mouse_row); ++ if (g.mode & ATTR_IMAGE) { ++ uint32_t image_id = tgetimgid(&g); ++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n", ++ image_id, tgetimgplacementid(&g), tgetimgcol(&g), ++ tgetimgrow(&g)); ++ char stcommand[256] = {0}; ++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0); ++ if (len > sizeof(stcommand) - 1) { ++ fprintf(stderr, "Executable name too long: %s\n", ++ argv0); ++ return; ++ } ++ gr_show_image_info(image_id, tgetimgplacementid(&g), ++ tgetimgcol(&g), tgetimgrow(&g), ++ tgetisclassicplaceholder(&g), ++ tgetimgdiacriticcount(&g), argv0); ++ } ++} ++ ++void ++togglegrdebug(const Arg *arg) ++{ ++ graphics_debug_mode = (graphics_debug_mode + 1) % 3; ++ redraw(); ++} ++ ++void ++dumpgrstate(const Arg *arg) ++{ ++ gr_dump_state(); ++} ++ ++void ++unloadimages(const Arg *arg) ++{ ++ gr_unload_images_to_reduce_ram(); ++} ++ ++void ++toggleimages(const Arg *arg) ++{ ++ graphics_display_images = !graphics_display_images; ++ redraw(); ++} ++ + int + evcol(XEvent *e) + { +- int x = e->xbutton.x - borderpx; ++ int x = e->xbutton.x - win.hborderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; + } +@@ -339,7 +414,7 @@ evcol(XEvent *e) + int + evrow(XEvent *e) + { +- int y = e->xbutton.y - borderpx; ++ int y = e->xbutton.y - win.vborderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; + } +@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release) + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + ++ mouse_col = evcol(e); ++ mouse_row = evrow(e); ++ + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && +@@ -739,6 +817,9 @@ cresize(int width, int height) + col = MAX(1, col); + row = MAX(1, row); + ++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100; ++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100; ++ + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +@@ -869,8 +950,8 @@ xhints(void) + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; +- sizeh->height_inc = win.ch; +- sizeh->width_inc = win.cw; ++ sizeh->height_inc = 1; ++ sizeh->width_inc = 1; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; +@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } +- defaultfontsize = usedfontsize; ++ if (defaultfontsize <= 0) ++ defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) +@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize) + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; +- if (fontsize == 0) ++ if (defaultfontsize <= 0 && fontsize == 0) + defaultfontsize = fontval; + } + +@@ -1152,8 +1234,8 @@ xinit(int cols, int rows) + xloadcols(); + + /* adjust fixed window geometry */ +- win.w = 2 * borderpx + cols * win.cw; +- win.h = 2 * borderpx + rows * win.ch; ++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw; ++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) +@@ -1240,12 +1322,15 @@ xinit(int cols, int rows) + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; ++ ++ // Initialize the graphics (image display) module. ++ gr_init(xw.dpy, xw.vis, xw.cmap); + } + + int + xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) + { +- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; ++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; +@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + if (mode == ATTR_WDUMMY) + continue; + ++ /* Draw spaces for image placeholders (images will be drawn ++ * separately). */ ++ if (mode & ATTR_IMAGE) ++ rune = ' '; ++ + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; +@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x + return numspecs; + } + ++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen` ++ * is the length of the dash plus the length of the gap. `fraction` is the ++ * fraction of the dash length compared to `wavelen`. */ ++static void ++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w, ++ int wavelen, float fraction, int thick) ++{ ++ int dashw = MAX(1, fraction * wavelen); ++ for (int i = x - x % wavelen; i < x + w; i += wavelen) { ++ int startx = MAX(i, x); ++ int endx = MIN(i + dashw, x + w); ++ if (startx < endx) ++ XftDrawRect(xw.draw, color, startx, y, endx - startx, ++ thick); ++ } ++} ++ ++/* Draws an undercurl. `h` is the total height, including line thickness. */ ++static void ++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) ++{ ++ XGCValues gcvals = {.foreground = color->pixel, ++ .line_width = thick, ++ .line_style = LineSolid, ++ .cap_style = CapRound}; ++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw), ++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle, ++ &gcvals); ++ ++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h}; ++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted); ++ ++ int yoffset = thick / 2; ++ int segh = MAX(1, h - thick); ++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't ++ * look good without antialiasing. */ ++ int segw = segh; ++ int wavelen = MAX(1, segw * 2); ++ ++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) { ++ XPoint points[3] = {{.x = i, .y = y + yoffset}, ++ {.x = i + segw, .y = y + yoffset + segh}, ++ {.x = i + wavelen, .y = y + yoffset}}; ++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3, ++ CoordModeOrigin); ++ } ++ ++ XFreeGC(xw.dpy, gc); ++} ++ + void + xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) + { + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); +- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, ++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; +@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { +- xclear(0, (y == 0)? 0 : winy, borderpx, ++ xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + +- ((winy + win.ch >= borderpx + win.th)? win.h : 0)); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } +- if (winx + width >= borderpx + win.tw) { ++ if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, +- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); ++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) +- xclear(winx, 0, winx + width, borderpx); +- if (winy + win.ch >= borderpx + win.th) ++ xclear(winx, 0, winx + width, win.vborderpx); ++ if (winy + win.ch >= win.vborderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ +@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + +- /* Render the glyphs. */ +- XftDrawGlyphFontSpec(xw.draw, fg, specs, len); +- +- /* Render underline and strikethrough. */ ++ /* Decoration color. */ ++ Color decor; ++ uint32_t decorcolor = tgetdecorcolor(&base); ++ if (decorcolor == DECOR_DEFAULT_COLOR) { ++ decor = *fg; ++ } else if (IS_TRUECOL(decorcolor)) { ++ colfg.alpha = 0xffff; ++ colfg.red = TRUERED(decorcolor); ++ colfg.green = TRUEGREEN(decorcolor); ++ colfg.blue = TRUEBLUE(decorcolor); ++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor); ++ } else { ++ decor = dc.col[decorcolor]; ++ } ++ decor.color.alpha = 0xffff; ++ decor.pixel |= 0xff << 24; ++ ++ /* Float thickness, used as a base to compute other values. */ ++ float fthick = dc.font.height / 18.0; ++ /* Integer thickness in pixels. Must not be 0. */ ++ int thick = MAX(1, roundf(fthick)); ++ /* The default gap between the baseline and a single underline. */ ++ int gap = roundf(fthick * 2); ++ /* The total thickness of a double underline. */ ++ int doubleh = thick * 2 + ceilf(fthick * 0.5); ++ /* The total thickness of an undercurl. */ ++ int curlh = thick * 2 + roundf(fthick * 0.75); ++ ++ /* Render the underline before the glyphs. */ + if (base.mode & ATTR_UNDERLINE) { +- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, +- width, 1); ++ uint32_t style = tgetdecorstyle(&base); ++ int liney = winy + dc.font.ascent + gap; ++ /* Adjust liney to guarantee that a single underline fits. */ ++ liney -= MAX(0, liney + thick - (winy + win.ch)); ++ if (style == UNDERLINE_DOUBLE) { ++ liney -= MAX(0, liney + doubleh - (winy + win.ch)); ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ XftDrawRect(xw.draw, &decor, winx, ++ liney + doubleh - thick, width, thick); ++ } else if (style == UNDERLINE_DOTTED) { ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ thick * 2, 0.5, thick); ++ } else if (style == UNDERLINE_DASHED) { ++ int wavelen = MAX(2, win.cw * 0.9); ++ xdrawunderdashed(xw.draw, &decor, winx, liney, width, ++ wavelen, 0.65, thick); ++ } else if (style == UNDERLINE_CURLY) { ++ liney -= MAX(0, liney + curlh - (winy + win.ch)); ++ xdrawundercurl(xw.draw, &decor, winx, liney, width, ++ curlh, thick); ++ } else { ++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick); ++ } + } + ++ /* Render the glyphs. */ ++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len); ++ ++ /* Render strikethrough. Alway use the fg color. */ + if (base.mode & ATTR_STRUCK) { +- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, +- width, 1); ++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, ++ width, thick); + } + + /* Reset clip to none. */ +@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y) + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); ++ if (g.mode & ATTR_IMAGE) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ xdrawoneimagecell(g, x, y); ++ gr_finish_drawing(xw.buf); ++ } + } + + void +@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + if (IS_SET(MODE_HIDE)) + return; + ++ // If it's an image, just draw a ballot box for simplicity. ++ if (g.mode & ATTR_IMAGE) ++ g.u = 0x2610; ++ + /* + * Select the right color for the right mode. + */ +@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - \ ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + cy * win.ch, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + (cx + 1) * win.cw - 1, +- borderpx + cy * win.ch, ++ win.hborderpx + (cx + 1) * win.cw - 1, ++ win.vborderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, +- borderpx + cx * win.cw, +- borderpx + (cy + 1) * win.ch - 1, ++ win.hborderpx + cx * win.cw, ++ win.vborderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } + } + ++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming ++ * that they have the same attributes (and thus the same lower 24 bits of the ++ * image ID and the same placement ID). */ ++void ++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) { ++ int y_pix = win.vborderpx + y1 * win.ch; ++ uint32_t image_id_24bits = base.fg & 0xFFFFFF; ++ uint32_t placement_id = tgetimgplacementid(&base); ++ // Columns and rows are 1-based, 0 means unspecified. ++ int last_col = 0; ++ int last_row = 0; ++ int last_start_col = 0; ++ int last_start_x = x1; ++ // The most significant byte is also 1-base, subtract 1 before use. ++ uint32_t last_id_4thbyteplus1 = 0; ++ // We may need to inherit row/column/4th byte from the previous cell. ++ Glyph *prev = &line[x1 - 1]; ++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) && ++ (prev->fg & 0xFFFFFF) == image_id_24bits && ++ prev->decor == base.decor) { ++ last_row = tgetimgrow(prev); ++ last_col = tgetimgcol(prev); ++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev); ++ last_start_col = last_col + 1; ++ } ++ for (int x = x1; x < x2; ++x) { ++ Glyph *g = &line[x]; ++ uint32_t cur_row = tgetimgrow(g); ++ uint32_t cur_col = tgetimgcol(g); ++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g); ++ uint32_t num_diacritics = tgetimgdiacriticcount(g); ++ // If the row is not specified, assume it's the same as the row ++ // of the previous cell. Note that `cur_row` may contain a ++ // value imputed earlier, which will be preserved if `last_row` ++ // is zero (i.e. we don't know the row of the previous cell). ++ if (last_row && (num_diacritics == 0 || !cur_row)) ++ cur_row = last_row; ++ // If the column is not specified and the row is the same as the ++ // row of the previous cell, then assume that the column is the ++ // next one. ++ if (last_col && (num_diacritics <= 1 || !cur_col) && ++ cur_row == last_row) ++ cur_col = last_col + 1; ++ // If the additional id byte is not specified and the ++ // coordinates are consecutive, assume the byte is also the ++ // same. ++ if (last_id_4thbyteplus1 && ++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) && ++ cur_row == last_row && cur_col == last_col + 1) ++ cur_id_4thbyteplus1 = last_id_4thbyteplus1; ++ // If we couldn't infer row and column, start from the top left ++ // corner. ++ if (cur_row == 0) ++ cur_row = 1; ++ if (cur_col == 0) ++ cur_col = 1; ++ // If this cell breaks a contiguous stripe of image cells, draw ++ // that line and start a new one. ++ if (cur_col != last_col + 1 || cur_row != last_row || ++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) { ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ if (last_row != 0) { ++ int x_pix = ++ win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect( ++ xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, ++ last_row - 1, last_row, last_start_x, ++ y1, x_pix, y_pix, win.cw, win.ch, ++ base.mode & ATTR_REVERSE); ++ } ++ last_start_col = cur_col; ++ last_start_x = x; ++ } ++ last_row = cur_row; ++ last_col = cur_col; ++ last_id_4thbyteplus1 = cur_id_4thbyteplus1; ++ // Populate the missing glyph data to enable inheritance between ++ // runs and support the naive implementation of tgetimgid. ++ if (!tgetimgrow(g)) ++ tsetimgrow(g, cur_row); ++ // We cannot save this information if there are > 511 cols. ++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0) ++ tsetimgcol(g, cur_col); ++ if (!tgetimgid4thbyteplus1(g)) ++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1); ++ } ++ uint32_t image_id = image_id_24bits; ++ if (last_id_4thbyteplus1) ++ image_id |= (last_id_4thbyteplus1 - 1) << 24; ++ // Draw the last contiguous stripe. ++ if (last_row != 0) { ++ int x_pix = win.hborderpx + last_start_x * win.cw; ++ gr_append_imagerect(xw.buf, image_id, placement_id, ++ last_start_col - 1, last_col, last_row - 1, ++ last_row, last_start_x, y1, x_pix, y_pix, ++ win.cw, win.ch, base.mode & ATTR_REVERSE); ++ } ++} ++ ++/* Draw just one image cell without inheriting attributes from the left. */ ++void xdrawoneimagecell(Glyph g, int x, int y) { ++ if (!(g.mode & ATTR_IMAGE)) ++ return; ++ int x_pix = win.hborderpx + x * win.cw; ++ int y_pix = win.vborderpx + y * win.ch; ++ uint32_t row = tgetimgrow(&g) - 1; ++ uint32_t col = tgetimgcol(&g) - 1; ++ uint32_t placement_id = tgetimgplacementid(&g); ++ uint32_t image_id = tgetimgid(&g); ++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row, ++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch, ++ g.mode & ATTR_REVERSE); ++} ++ ++/* Prepare for image drawing. */ ++void xstartimagedraw(int *dirty, int rows) { ++ gr_start_drawing(xw.buf, win.cw, win.ch); ++ gr_mark_dirty_animations(dirty, rows); ++} ++ ++/* Draw all queued image cells. */ ++void xfinishimagedraw() { ++ gr_finish_drawing(xw.buf); ++} ++ + void + xsetenv(void) + { +@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + specs += i; + numspecs -= i; + i = 0; +@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2) + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); ++ if (i > 0 && base.mode & ATTR_IMAGE) ++ xdrawimages(base, line, ox, y1, x); + } + + void +@@ -1907,6 +2238,7 @@ cmessage(XEvent *e) + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); ++ gr_deinit(); + exit(0); + } + } +@@ -1957,6 +2289,13 @@ run(void) + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + ++ /* Decrease the timeout if there are active animations. */ ++ if (graphics_next_redraw_delay != INT_MAX && ++ IS_SET(MODE_VISIBLE)) ++ timeout = timeout < 0 ? graphics_next_redraw_delay ++ : MIN(timeout, ++ graphics_next_redraw_delay); ++ + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; +-- +2.43.0 + diff --git a/st/patches/st-ligatures-20251007-0.9.3.diff b/suckless/st/patches/st-ligatures-20251007-0.9.3.diff similarity index 100% rename from st/patches/st-ligatures-20251007-0.9.3.diff rename to suckless/st/patches/st-ligatures-20251007-0.9.3.diff diff --git a/suckless/st/rowcolumn_diacritics_helpers.c b/suckless/st/rowcolumn_diacritics_helpers.c new file mode 100644 index 0000000..829c0fc --- /dev/null +++ b/suckless/st/rowcolumn_diacritics_helpers.c @@ -0,0 +1,391 @@ +#include + +uint16_t diacritic_to_num(uint32_t code) +{ + switch (code) { + case 0x305: + return code - 0x305 + 1; + case 0x30d: + case 0x30e: + return code - 0x30d + 2; + case 0x310: + return code - 0x310 + 4; + case 0x312: + return code - 0x312 + 5; + case 0x33d: + case 0x33e: + case 0x33f: + return code - 0x33d + 6; + case 0x346: + return code - 0x346 + 9; + case 0x34a: + case 0x34b: + case 0x34c: + return code - 0x34a + 10; + case 0x350: + case 0x351: + case 0x352: + return code - 0x350 + 13; + case 0x357: + return code - 0x357 + 16; + case 0x35b: + return code - 0x35b + 17; + case 0x363: + case 0x364: + case 0x365: + case 0x366: + case 0x367: + case 0x368: + case 0x369: + case 0x36a: + case 0x36b: + case 0x36c: + case 0x36d: + case 0x36e: + case 0x36f: + return code - 0x363 + 18; + case 0x483: + case 0x484: + case 0x485: + case 0x486: + case 0x487: + return code - 0x483 + 31; + case 0x592: + case 0x593: + case 0x594: + case 0x595: + return code - 0x592 + 36; + case 0x597: + case 0x598: + case 0x599: + return code - 0x597 + 40; + case 0x59c: + case 0x59d: + case 0x59e: + case 0x59f: + case 0x5a0: + case 0x5a1: + return code - 0x59c + 43; + case 0x5a8: + case 0x5a9: + return code - 0x5a8 + 49; + case 0x5ab: + case 0x5ac: + return code - 0x5ab + 51; + case 0x5af: + return code - 0x5af + 53; + case 0x5c4: + return code - 0x5c4 + 54; + case 0x610: + case 0x611: + case 0x612: + case 0x613: + case 0x614: + case 0x615: + case 0x616: + case 0x617: + return code - 0x610 + 55; + case 0x657: + case 0x658: + case 0x659: + case 0x65a: + case 0x65b: + return code - 0x657 + 63; + case 0x65d: + case 0x65e: + return code - 0x65d + 68; + case 0x6d6: + case 0x6d7: + case 0x6d8: + case 0x6d9: + case 0x6da: + case 0x6db: + case 0x6dc: + return code - 0x6d6 + 70; + case 0x6df: + case 0x6e0: + case 0x6e1: + case 0x6e2: + return code - 0x6df + 77; + case 0x6e4: + return code - 0x6e4 + 81; + case 0x6e7: + case 0x6e8: + return code - 0x6e7 + 82; + case 0x6eb: + case 0x6ec: + return code - 0x6eb + 84; + case 0x730: + return code - 0x730 + 86; + case 0x732: + case 0x733: + return code - 0x732 + 87; + case 0x735: + case 0x736: + return code - 0x735 + 89; + case 0x73a: + return code - 0x73a + 91; + case 0x73d: + return code - 0x73d + 92; + case 0x73f: + case 0x740: + case 0x741: + return code - 0x73f + 93; + case 0x743: + return code - 0x743 + 96; + case 0x745: + return code - 0x745 + 97; + case 0x747: + return code - 0x747 + 98; + case 0x749: + case 0x74a: + return code - 0x749 + 99; + case 0x7eb: + case 0x7ec: + case 0x7ed: + case 0x7ee: + case 0x7ef: + case 0x7f0: + case 0x7f1: + return code - 0x7eb + 101; + case 0x7f3: + return code - 0x7f3 + 108; + case 0x816: + case 0x817: + case 0x818: + case 0x819: + return code - 0x816 + 109; + case 0x81b: + case 0x81c: + case 0x81d: + case 0x81e: + case 0x81f: + case 0x820: + case 0x821: + case 0x822: + case 0x823: + return code - 0x81b + 113; + case 0x825: + case 0x826: + case 0x827: + return code - 0x825 + 122; + case 0x829: + case 0x82a: + case 0x82b: + case 0x82c: + case 0x82d: + return code - 0x829 + 125; + case 0x951: + return code - 0x951 + 130; + case 0x953: + case 0x954: + return code - 0x953 + 131; + case 0xf82: + case 0xf83: + return code - 0xf82 + 133; + case 0xf86: + case 0xf87: + return code - 0xf86 + 135; + case 0x135d: + case 0x135e: + case 0x135f: + return code - 0x135d + 137; + case 0x17dd: + return code - 0x17dd + 140; + case 0x193a: + return code - 0x193a + 141; + case 0x1a17: + return code - 0x1a17 + 142; + case 0x1a75: + case 0x1a76: + case 0x1a77: + case 0x1a78: + case 0x1a79: + case 0x1a7a: + case 0x1a7b: + case 0x1a7c: + return code - 0x1a75 + 143; + case 0x1b6b: + return code - 0x1b6b + 151; + case 0x1b6d: + case 0x1b6e: + case 0x1b6f: + case 0x1b70: + case 0x1b71: + case 0x1b72: + case 0x1b73: + return code - 0x1b6d + 152; + case 0x1cd0: + case 0x1cd1: + case 0x1cd2: + return code - 0x1cd0 + 159; + case 0x1cda: + case 0x1cdb: + return code - 0x1cda + 162; + case 0x1ce0: + return code - 0x1ce0 + 164; + case 0x1dc0: + case 0x1dc1: + return code - 0x1dc0 + 165; + case 0x1dc3: + case 0x1dc4: + case 0x1dc5: + case 0x1dc6: + case 0x1dc7: + case 0x1dc8: + case 0x1dc9: + return code - 0x1dc3 + 167; + case 0x1dcb: + case 0x1dcc: + return code - 0x1dcb + 174; + case 0x1dd1: + case 0x1dd2: + case 0x1dd3: + case 0x1dd4: + case 0x1dd5: + case 0x1dd6: + case 0x1dd7: + case 0x1dd8: + case 0x1dd9: + case 0x1dda: + case 0x1ddb: + case 0x1ddc: + case 0x1ddd: + case 0x1dde: + case 0x1ddf: + case 0x1de0: + case 0x1de1: + case 0x1de2: + case 0x1de3: + case 0x1de4: + case 0x1de5: + case 0x1de6: + return code - 0x1dd1 + 176; + case 0x1dfe: + return code - 0x1dfe + 198; + case 0x20d0: + case 0x20d1: + return code - 0x20d0 + 199; + case 0x20d4: + case 0x20d5: + case 0x20d6: + case 0x20d7: + return code - 0x20d4 + 201; + case 0x20db: + case 0x20dc: + return code - 0x20db + 205; + case 0x20e1: + return code - 0x20e1 + 207; + case 0x20e7: + return code - 0x20e7 + 208; + case 0x20e9: + return code - 0x20e9 + 209; + case 0x20f0: + return code - 0x20f0 + 210; + case 0x2cef: + case 0x2cf0: + case 0x2cf1: + return code - 0x2cef + 211; + case 0x2de0: + case 0x2de1: + case 0x2de2: + case 0x2de3: + case 0x2de4: + case 0x2de5: + case 0x2de6: + case 0x2de7: + case 0x2de8: + case 0x2de9: + case 0x2dea: + case 0x2deb: + case 0x2dec: + case 0x2ded: + case 0x2dee: + case 0x2def: + case 0x2df0: + case 0x2df1: + case 0x2df2: + case 0x2df3: + case 0x2df4: + case 0x2df5: + case 0x2df6: + case 0x2df7: + case 0x2df8: + case 0x2df9: + case 0x2dfa: + case 0x2dfb: + case 0x2dfc: + case 0x2dfd: + case 0x2dfe: + case 0x2dff: + return code - 0x2de0 + 214; + case 0xa66f: + return code - 0xa66f + 246; + case 0xa67c: + case 0xa67d: + return code - 0xa67c + 247; + case 0xa6f0: + case 0xa6f1: + return code - 0xa6f0 + 249; + case 0xa8e0: + case 0xa8e1: + case 0xa8e2: + case 0xa8e3: + case 0xa8e4: + case 0xa8e5: + case 0xa8e6: + case 0xa8e7: + case 0xa8e8: + case 0xa8e9: + case 0xa8ea: + case 0xa8eb: + case 0xa8ec: + case 0xa8ed: + case 0xa8ee: + case 0xa8ef: + case 0xa8f0: + case 0xa8f1: + return code - 0xa8e0 + 251; + case 0xaab0: + return code - 0xaab0 + 269; + case 0xaab2: + case 0xaab3: + return code - 0xaab2 + 270; + case 0xaab7: + case 0xaab8: + return code - 0xaab7 + 272; + case 0xaabe: + case 0xaabf: + return code - 0xaabe + 274; + case 0xaac1: + return code - 0xaac1 + 276; + case 0xfe20: + case 0xfe21: + case 0xfe22: + case 0xfe23: + case 0xfe24: + case 0xfe25: + case 0xfe26: + return code - 0xfe20 + 277; + case 0x10a0f: + return code - 0x10a0f + 284; + case 0x10a38: + return code - 0x10a38 + 285; + case 0x1d185: + case 0x1d186: + case 0x1d187: + case 0x1d188: + case 0x1d189: + return code - 0x1d185 + 286; + case 0x1d1aa: + case 0x1d1ab: + case 0x1d1ac: + case 0x1d1ad: + return code - 0x1d1aa + 291; + case 0x1d242: + case 0x1d243: + case 0x1d244: + return code - 0x1d242 + 295; + } + return 0; +} diff --git a/st/st.1 b/suckless/st/st.1 similarity index 100% rename from st/st.1 rename to suckless/st/st.1 diff --git a/st/st.c b/suckless/st/st.c similarity index 87% rename from st/st.c rename to suckless/st/st.c index e16541a..cd9e718 100644 --- a/st/st.c +++ b/suckless/st/st.c @@ -19,6 +19,7 @@ #include "st.h" #include "win.h" +#include "graphics.h" #if defined(__linux) #include @@ -36,6 +37,10 @@ #define STR_BUF_SIZ ESC_BUF_SIZ #define STR_ARG_SIZ ESC_ARG_SIZ +/* PUA character used as an image placeholder */ +#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE +#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE + /* macros */ #define IS_SET(flag) ((term.mode & (flag)) != 0) #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) @@ -113,6 +118,8 @@ typedef struct { typedef struct { int row; /* nb row */ int col; /* nb col */ + int pixw; /* width of the text area in pixels */ + int pixh; /* height of the text area in pixels */ Line *line; /* screen */ Line *alt; /* alternate screen */ int *dirty; /* dirtyness of lines */ @@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *); static char utf8encodebyte(Rune, size_t); static size_t utf8validate(Rune *, size_t); -static char *base64dec(const char *); static char base64dec_getc(const char **); static ssize_t xwrite(int, const char *, size_t); @@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; +/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0 + * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */ +uint16_t diacritic_to_num(uint32_t code); + ssize_t xwrite(int fd, const char *s, size_t len) { @@ -616,6 +626,12 @@ getsel(void) if (gp->mode & ATTR_WDUMMY) continue; + if (gp->mode & ATTR_IMAGE) { + // TODO: Copy diacritics as well + ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr); + continue; + } + ptr += utf8encode(gp->u, ptr); } @@ -715,11 +731,14 @@ sigchld(int a) int stat; pid_t p; - if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + if ((p = waitpid(-1, &stat, WNOHANG)) < 0) die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); - if (pid != p) + if (pid != p) { + /* reinstall sigchld handler */ + signal(SIGCHLD, sigchld); return; + } if (WIFEXITED(stat) && WEXITSTATUS(stat)) die("child exited with status %d\n", WEXITSTATUS(stat)); @@ -819,7 +838,11 @@ ttyread(void) { static char buf[BUFSIZ]; static int buflen = 0; - int ret, written; + static int already_processing = 0; + int ret, written = 0; + + if (buflen >= LEN(buf)) + return 0; /* append read bytes to unprocessed bytes */ ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); @@ -831,7 +854,24 @@ ttyread(void) die("couldn't read from shell: %s\n", strerror(errno)); default: buflen += ret; - written = twrite(buf, buflen, 0); + if (already_processing) { + /* Avoid recursive call to twrite() */ + return ret; + } + already_processing = 1; + while (1) { + int buflen_before_processing = buflen; + written += twrite(buf + written, buflen - written, 0); + // If buflen changed during the call to twrite, there is + // new data, and we need to keep processing, otherwise + // we can exit. This will not loop forever because the + // buffer is limited, and we don't clean it in this + // loop, so at some point ttywrite will have to drop + // some data. + if (buflen_before_processing == buflen) + break; + } + already_processing = 0; buflen -= written; /* keep any incomplete UTF-8 byte sequence for the next call */ if (buflen > 0) @@ -874,6 +914,7 @@ ttywriteraw(const char *s, size_t n) fd_set wfd, rfd; ssize_t r; size_t lim = 256; + int retries_left = 100; /* * Remember that we are using a pty, which might be a modem line. @@ -882,6 +923,9 @@ ttywriteraw(const char *s, size_t n) * FIXME: Migrate the world to Plan 9. */ while (n > 0) { + if (retries_left-- <= 0) + goto too_many_retries; + FD_ZERO(&wfd); FD_ZERO(&rfd); FD_SET(cmdfd, &wfd); @@ -923,11 +967,16 @@ ttywriteraw(const char *s, size_t n) write_error: die("write error on tty: %s\n", strerror(errno)); +too_many_retries: + fprintf(stderr, "Could not write %zu bytes to tty\n", n); } void ttyresize(int tw, int th) { + term.pixw = tw; + term.pixh = th; + struct winsize w; w.ws_row = term.row; @@ -1018,7 +1067,8 @@ treset(void) term.c = (TCursor){{ .mode = ATTR_NULL, .fg = defaultfg, - .bg = defaultbg + .bg = defaultbg, + .decor = DECOR_DEFAULT_COLOR }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; memset(term.tabs, 0, term.col * sizeof(*term.tabs)); @@ -1041,7 +1091,9 @@ treset(void) void tnew(int col, int row) { - term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + term = (Term){.c = {.attr = {.fg = defaultfg, + .bg = defaultbg, + .decor = DECOR_DEFAULT_COLOR}}}; tresize(col, row); treset(); } @@ -1218,9 +1270,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) term.line[y][x-1].mode &= ~ATTR_WIDE; } + if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE && + tgetisclassicplaceholder(&term.line[y][x])) { + // This is a workaround: don't overwrite classic placement + // placeholders with space symbols (unlike Unicode placeholders + // which must be overwritten by anything). + term.line[y][x].bg = attr->bg; + term.dirty[y] = 1; + return; + } + term.dirty[y] = 1; term.line[y][x] = *attr; term.line[y][x].u = u; + + if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) { + term.line[y][x].u = 0; + term.line[y][x].mode |= ATTR_IMAGE; + } } void @@ -1247,12 +1314,110 @@ tclearregion(int x1, int y1, int x2, int y2) selclear(); gp->fg = term.c.attr.fg; gp->bg = term.c.attr.bg; + gp->decor = term.c.attr.decor; gp->mode = 0; gp->u = ' '; } } } +/// Fills a rectangle area with an image placeholder. The starting point is the +/// cursor. Adds empty lines if needed. The placeholder will be marked as +/// classic. +void tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id, int cols, + int rows, char do_not_move_cursor, + Glyph *text_underneath) { + for (int row = 0; row < rows; ++row) { + int y = term.c.y; + term.dirty[y] = 1; + for (int col = 0; col < cols; ++col) { + int x = term.c.x + col; + if (x >= term.col) + break; + Glyph *gp = &term.line[y][x]; + if (selected(x, y)) + selclear(); + if (text_underneath) { + Glyph *to_save = gp; + // If there is already a classic placeholder, + // use the text underneath it. This will leave + // holes in images, but at least we are + // guaranteed to restore the original text. + if (gp->mode & ATTR_IMAGE && + tgetisclassicplaceholder(gp)) { + Glyph *under = + gr_get_glyph_underneath_image( + tgetimgid(gp), + tgetimgplacementid(gp), + tgetimgcol(gp), + tgetimgrow(gp)); + if (under) + to_save = under; + } + text_underneath[cols * row + col] = *to_save; + } + gp->mode = ATTR_IMAGE; + gp->u = 0; + tsetimgrow(gp, row + 1); + tsetimgcol(gp, col + 1); + tsetimgid(gp, image_id); + tsetimgplacementid(gp, placement_id); + tsetimgdiacriticcount(gp, 3); + tsetisclassicplaceholder(gp, 1); + } + // If moving the cursor is not allowed and this is the last line + // of the terminal, we are done. + if (do_not_move_cursor && y == term.row - 1) + break; + // Move the cursor down, maybe creating a new line. The x is + // preserved (we never change term.c.x in the loop above). + if (row != rows - 1) + tnewline(/*first_col=*/0); + } + if (do_not_move_cursor) { + // Return the cursor to the original position. + tmoveto(term.c.x, term.c.y - rows + 1); + } else { + // Move the cursor beyond the last column, as required by the + // protocol. If the cursor goes beyond the screen edge, insert a + // newline to match the behavior of kitty. + if (term.c.x + cols >= term.col) + tnewline(/*first_col=*/1); + else + tmoveto(term.c.x + cols, term.c.y); + } +} + +void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp), + void *data) { + for (int row = 0; row < term.row; ++row) { + for (int col = 0; col < term.col; ++col) { + Glyph *gp = &term.line[row][col]; + if (gp->mode & ATTR_IMAGE) { + if (callback(data, gp)) + term.dirty[row] = 1; + } + } + } +} + +void gr_schedule_image_redraw_by_id(uint32_t image_id) { + for (int row = 0; row < term.row; ++row) { + if (term.dirty[row]) + continue; + for (int col = 0; col < term.col; ++col) { + Glyph *gp = &term.line[row][col]; + if (gp->mode & ATTR_IMAGE) { + uint32_t cell_image_id = tgetimgid(gp); + if (cell_image_id == image_id) { + term.dirty[row] = 1; + break; + } + } + } + } +} + void tdeletechar(int n) { @@ -1371,6 +1536,7 @@ tsetattr(const int *attr, int l) ATTR_STRUCK ); term.c.attr.fg = defaultfg; term.c.attr.bg = defaultbg; + term.c.attr.decor = DECOR_DEFAULT_COLOR; break; case 1: term.c.attr.mode |= ATTR_BOLD; @@ -1383,6 +1549,20 @@ tsetattr(const int *attr, int l) break; case 4: term.c.attr.mode |= ATTR_UNDERLINE; + if (i + 1 < l) { + idx = attr[++i]; + if (BETWEEN(idx, 1, 5)) { + tsetdecorstyle(&term.c.attr, idx); + } else if (idx == 0) { + term.c.attr.mode &= ~ATTR_UNDERLINE; + tsetdecorstyle(&term.c.attr, 0); + } else { + fprintf(stderr, + "erresc: unknown underline " + "style %d\n", + idx); + } + } break; case 5: /* slow blink */ /* FALLTHROUGH */ @@ -1406,6 +1586,7 @@ tsetattr(const int *attr, int l) break; case 24: term.c.attr.mode &= ~ATTR_UNDERLINE; + tsetdecorstyle(&term.c.attr, 0); break; case 25: term.c.attr.mode &= ~ATTR_BLINK; @@ -1434,10 +1615,11 @@ tsetattr(const int *attr, int l) term.c.attr.bg = defaultbg; break; case 58: - /* This starts a sequence to change the color of - * "underline" pixels. We don't support that and - * instead eat up a following "5;n" or "2;r;g;b". */ - tdefcolor(attr, &i, l); + if ((idx = tdefcolor(attr, &i, l)) >= 0) + tsetdecorcolor(&term.c.attr, idx); + break; + case 59: + tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR); break; default: if (BETWEEN(attr[i], 30, 37)) { @@ -1826,6 +2008,39 @@ csihandle(void) goto unknown; } break; + case '>': + switch (csiescseq.mode[1]) { + case 'q': /* XTVERSION -- Print terminal name and version */ + len = snprintf(buf, sizeof(buf), + "\033P>|st-graphics(%s)\033\\", VERSION); + ttywrite(buf, len, 0); + break; + default: + goto unknown; + } + break; + case 't': /* XTWINOPS -- Window manipulation */ + switch (csiescseq.arg[0]) { + case 14: /* Report text area size in pixels. */ + len = snprintf(buf, sizeof(buf), "\033[4;%i;%it", + term.pixh, term.pixw); + ttywrite(buf, len, 0); + break; + case 16: /* Report character cell size in pixels. */ + len = snprintf(buf, sizeof(buf), "\033[6;%i;%it", + term.pixh / term.row, + term.pixw / term.col); + ttywrite(buf, len, 0); + break; + case 18: /* Report the size of the text area in characters. */ + len = snprintf(buf, sizeof(buf), "\033[8;%i;%it", + term.row, term.col); + ttywrite(buf, len, 0); + break; + default: + goto unknown; + } + break; } } @@ -1988,8 +2203,27 @@ strhandle(void) case 'k': /* old title set compatibility */ xsettitle(strescseq.args[0]); return; - case 'P': /* DCS -- Device Control String */ case '_': /* APC -- Application Program Command */ + if (gr_parse_command(strescseq.buf, strescseq.len)) { + GraphicsCommandResult *res = &graphics_command_result; + if (res->create_placeholder) { + tcreateimgplaceholder( + res->placeholder.image_id, + res->placeholder.placement_id, + res->placeholder.columns, + res->placeholder.rows, + res->placeholder.do_not_move_cursor, + res->placeholder.text_underneath); + } + if (res->response[0]) + ttywrite(res->response, strlen(res->response), + 0); + if (res->redraw) + tfulldirt(); + return; + } + return; + case 'P': /* DCS -- Device Control String */ case '^': /* PM -- Privacy Message */ return; } @@ -2496,6 +2730,41 @@ check_control_code: if (selected(term.c.x, term.c.y)) selclear(); + // wcwidth is broken on some systems, set the width to 0 if it's a known + // diacritic used for images. + uint16_t num = diacritic_to_num(u); + if (num != 0) + width = 0; + // Set the width to 1 if it's an image placeholder character. + if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) + width = 1; + + if (width == 0) { + // It's probably a combining char. Combining characters are not + // supported, so we just ignore them, unless it denotes the row and + // column of an image character. + if (term.c.y <= 0 && term.c.x <= 0) + return; + else if (term.c.x == 0) + gp = &term.line[term.c.y-1][term.col-1]; + else if (term.c.state & CURSOR_WRAPNEXT) + gp = &term.line[term.c.y][term.c.x]; + else + gp = &term.line[term.c.y][term.c.x-1]; + if (num && (gp->mode & ATTR_IMAGE)) { + unsigned diaccount = tgetimgdiacriticcount(gp); + if (diaccount == 0) + tsetimgrow(gp, num); + else if (diaccount == 1) + tsetimgcol(gp, num); + else if (diaccount == 2) + tsetimg4thbyteplus1(gp, num); + tsetimgdiacriticcount(gp, diaccount + 1); + } + term.lastc = u; + return; + } + gp = &term.line[term.c.y][term.c.x]; if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { gp->mode |= ATTR_WRAP; @@ -2662,6 +2931,8 @@ drawregion(int x1, int y1, int x2, int y2) { int y; + xstartimagedraw(term.dirty, term.row); + for (y = y1; y < y2; y++) { if (!term.dirty[y]) continue; @@ -2669,6 +2940,8 @@ drawregion(int x1, int y1, int x2, int y2) term.dirty[y] = 0; xdrawline(term.line[y], x1, y, x2); } + + xfinishimagedraw(); } void @@ -2704,3 +2977,9 @@ redraw(void) tfulldirt(); draw(); } + +Glyph +getglyphat(int col, int row) +{ + return term.line[row][col]; +} diff --git a/suckless/st/st.h b/suckless/st/st.h new file mode 100644 index 0000000..864ddfb --- /dev/null +++ b/suckless/st/st.h @@ -0,0 +1,215 @@ +/* See LICENSE for license details. */ + +#include +#include + +/* macros */ +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) < (b) ? (b) : (a)) +#define LEN(a) (sizeof(a) / sizeof(a)[0]) +#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b)) +#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d)) +#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 & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \ + (a).fg != (b).fg || \ + (a).bg != (b).bg || (a).decor != (b).decor) +#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) +#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) + +#define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) +#define IS_TRUECOL(x) (1 << 24 & (x)) + +// 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, + ATTR_FAINT = 1 << 1, + ATTR_ITALIC = 1 << 2, + ATTR_UNDERLINE = 1 << 3, + ATTR_BLINK = 1 << 4, + ATTR_REVERSE = 1 << 5, + ATTR_INVISIBLE = 1 << 6, + ATTR_STRUCK = 1 << 7, + ATTR_WRAP = 1 << 8, + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, + ATTR_IMAGE = 1 << 14, +}; + +enum drawing_mode { + DRAW_NONE = 0, + DRAW_BG = 1 << 0, + DRAW_FG = 1 << 1, +}; + +enum selection_mode { + SEL_IDLE = 0, + SEL_EMPTY = 1, + SEL_READY = 2 +}; + +enum selection_type { + SEL_REGULAR = 1, + SEL_RECTANGULAR = 2 +}; + +enum selection_snap { + SNAP_WORD = 1, + 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; +typedef unsigned short ushort; + +typedef uint_least32_t Rune; + +#define Glyph Glyph_ +typedef struct { + Rune u; /* character code */ + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ + uint32_t decor; /* decoration (like underline) */ +} Glyph; + +typedef Glyph *Line; + +typedef union { + int i; + uint ui; + float f; + const void *v; + const char *s; +} Arg; + +void die(const char *, ...); +void redraw(void); +void draw(void); + +void printscreen(const Arg *); +void printsel(const Arg *); +void sendbreak(const Arg *); +void toggleprinter(const Arg *); + +int tattrset(int); +void tnew(int, int); +void tresize(int, int); +void tsetdirtattr(int); +void ttyhangup(void); +int ttynew(const char *, char *, const char *, char **); +size_t ttyread(void); +void ttyresize(int, int); +void ttywrite(const char *, size_t, int); + +void resettitle(void); + +void selclear(void); +void selinit(void); +void selstart(int, int, int); +void selextend(int, int, int, int); +int selected(int, int); +char *getsel(void); + +Glyph getglyphat(int, int); + +size_t utf8encode(Rune, char *); + +void *xmalloc(size_t); +void *xrealloc(void *, size_t); +char *xstrdup(const char *); + +/* config.h globals */ +extern char *utmp; +extern char *scroll; +extern char *stty_args; +extern char *vtiden; +extern wchar_t *worddelimiters; +extern int allowaltscreen; +extern int allowwindowops; +extern char *termname; +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/st.info b/suckless/st/st.info similarity index 96% rename from st/st.info rename to suckless/st/st.info index efab2cf..ded76c1 100644 --- a/st/st.info +++ b/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/st/win.h b/suckless/st/win.h similarity index 94% rename from st/win.h rename to suckless/st/win.h index 94679e4..c292af5 100644 --- a/st/win.h +++ b/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/st/x.c b/suckless/st/x.c similarity index 72% rename from st/x.c rename to suckless/st/x.c index 9292287..2261ff1 100644 --- a/st/x.c +++ b/suckless/st/x.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ char *argv0; #include "arg.h" #include "st.h" #include "win.h" +#include "graphics.h" #include "hb.h" /* types used in config.h */ @@ -60,6 +63,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" @@ -146,8 +155,10 @@ typedef struct { static inline ushort sixd_to_16bit(int); static void xresetfontsettings(ushort mode, Font **font, int *frcflags); static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); -static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int); +static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int, int); static void xdrawglyph(Glyph, int, int); +static void xdrawimages(Glyph, Line, int x1, int y1, int x2); +static void xdrawoneimagecell(Glyph, int x, int y); static void xclear(int, int, int, int); static int xgeommasktogravity(int); static int ximopen(Display *); @@ -224,6 +235,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 { @@ -332,6 +344,68 @@ 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) { @@ -456,6 +530,9 @@ mouseaction(XEvent *e, uint release) /* ignore Buttonmask for Button - it's set on release */ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + mouse_col = evcol(e); + mouse_row = evrow(e); + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { if (ms->release == release && ms->button == e->xbutton.button && @@ -743,8 +820,8 @@ cresize(int width, int height) col = MAX(1, col); row = MAX(1, row); - win.hborderpx = (win.w - col * win.cw) / 2; - win.vborderpx = (win.h - row * win.ch) / 2; + 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); @@ -1031,7 +1108,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)) @@ -1041,7 +1119,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; } @@ -1271,6 +1349,9 @@ 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); } void @@ -1293,132 +1374,220 @@ int xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) { float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp; - ushort mode = glyphs[0].mode & ~ATTR_WRAP; + ushort mode, prevmode = USHRT_MAX; Font *font = &dc.font; int frcflags = FRC_NORMAL; - float runewidth = win.cw * ((glyphs[0].mode & ATTR_WIDE) ? 2.0f : 1.0f); + float runewidth = win.cw; Rune rune; FT_UInt glyphidx; FcResult fcres; FcPattern *fcpattern, *fontpattern; FcFontSet *fcsets[] = { NULL }; FcCharSet *fccharset; - int f, code_idx, numspecs = 0; + int i, f, length = 0, start = 0, numspecs = 0; float cluster_xp = xp, cluster_yp = yp; HbTransformData shaped = { 0 }; /* Initial values. */ + mode = prevmode = glyphs[0].mode & ~ATTR_WRAP; xresetfontsettings(mode, &font, &frcflags); - /* Shape the segment. */ - hbtransform(&shaped, font->match, glyphs, 0, len); - xp = winx; yp = winy + font->ascent; - cluster_xp = xp; cluster_yp = yp; + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + mode = glyphs[i].mode & ~ATTR_WRAP; - for (code_idx = 0; code_idx < shaped.count; code_idx++) { - int idx = shaped.glyphs[code_idx].cluster; - - if (glyphs[idx].mode & ATTR_WDUMMY) + /* Skip dummy wide-character spacing. */ + if (mode & ATTR_WDUMMY && i < (len - 1)) continue; - /* Advance the drawing cursor if we've moved to a new cluster */ - if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) { - xp += runewidth * (idx - shaped.glyphs[code_idx - 1].cluster); - cluster_xp = xp; - cluster_yp = yp; - } + if ( + prevmode != mode + || ATTRCMP(glyphs[start], glyphs[i]) + || selected(x + i, y) != selected(x + start, y) + || i == (len - 1) + ) { + /* Handle 1-character wide segments and end of line */ + length = i - start; + if (i == start) { + length = 1; + } else if (i == (len - 1)) { + length = (i - start + 1); + } - if (shaped.glyphs[code_idx].codepoint != 0) { - /* If symbol is found, put it into the specs. */ - specs[numspecs].font = font->match; - specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint; - specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.); - specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.); - cluster_xp += shaped.positions[code_idx].x_advance / 64.; - cluster_yp += shaped.positions[code_idx].y_advance / 64.; - numspecs++; - } else { - /* If it's not found, try to fetch it through the font cache. */ - rune = glyphs[idx].u; - for (f = 0; f < frclen; f++) { - glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); - /* Everything correct. */ - if (glyphidx && frc[f].flags == frcflags) - break; - /* We got a default font for a not found glyph. */ - if (!glyphidx && frc[f].flags == frcflags - && frc[f].unicodep == rune) { - break; + /* Shape the segment. */ + hbtransform(&shaped, font->match, glyphs, start, length); + runewidth = win.cw * ((glyphs[start].mode & ATTR_WIDE) ? 2.0f : 1.0f); + cluster_xp = xp; cluster_yp = yp; + for (int code_idx = 0; code_idx < shaped.count; code_idx++) { + int idx = shaped.glyphs[code_idx].cluster; + + if (glyphs[start + idx].mode & ATTR_WDUMMY) + continue; + + /* Draw spaces for image placeholders (images + * will be drawn separately). */ + if (glyphs[start + idx].mode & ATTR_IMAGE) + continue; + + /* Advance the drawing cursor if we've moved to a new cluster */ + if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) { + xp += runewidth; + cluster_xp = xp; + cluster_yp = yp; + runewidth = win.cw * ((glyphs[start + idx].mode & ATTR_WIDE) ? 2.0f : 1.0f); + } + + if (shaped.glyphs[code_idx].codepoint != 0) { + /* If symbol is found, put it into the specs. */ + specs[numspecs].font = font->match; + specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint; + specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.); + specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.); + cluster_xp += shaped.positions[code_idx].x_advance / 64.; + cluster_yp += shaped.positions[code_idx].y_advance / 64.; + numspecs++; + } else { + /* If it's not found, try to fetch it through the font cache. */ + rune = glyphs[start + idx].u; + for (f = 0; f < frclen; f++) { + glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); + /* Everything correct. */ + if (glyphidx && frc[f].flags == frcflags) + break; + /* We got a default font for a not found glyph. */ + if (!glyphidx && frc[f].flags == frcflags + && frc[f].unicodep == rune) { + break; + } + } + + /* Nothing was found. Use fontconfig to find matching font. */ + if (f >= frclen) { + if (!font->set) + font->set = FcFontSort(0, font->pattern, 1, 0, &fcres); + fcsets[0] = font->set; + + /* + * Nothing was found in the cache. Now use + * some dozen of Fontconfig calls to get the + * font for one single character. + * + * Xft and fontconfig are design failures. + */ + fcpattern = FcPatternDuplicate(font->pattern); + fccharset = FcCharSetCreate(); + + FcCharSetAddChar(fccharset, rune); + FcPatternAddCharSet(fcpattern, FC_CHARSET, + fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, 1); + + FcConfigSubstitute(0, fcpattern, + FcMatchPattern); + FcDefaultSubstitute(fcpattern); + + fontpattern = FcFontSetMatch(0, fcsets, 1, + fcpattern, &fcres); + + /* Allocate memory for the new cache entry. */ + if (frclen >= frccap) { + frccap += 16; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + frc[frclen].font = XftFontOpenPattern(xw.dpy, + fontpattern); + if (!frc[frclen].font) + die("XftFontOpenPattern failed seeking fallback font: %s\n", + strerror(errno)); + frc[frclen].flags = frcflags; + frc[frclen].unicodep = rune; + + glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + + f = frclen; + frclen++; + + FcPatternDestroy(fcpattern); + FcCharSetDestroy(fccharset); + } + + specs[numspecs].font = frc[f].font; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + numspecs++; } } - /* 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; + /* Cleanup and get ready for next segment. */ + hbcleanup(&shaped); + start = i; - /* - * 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); + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; + xresetfontsettings(mode, &font, &frcflags); + yp = winy + font->ascent; } - - specs[numspecs].font = frc[f].font; - specs[numspecs].glyph = glyphidx; - specs[numspecs].x = (short)xp; - specs[numspecs].y = (short)yp; - numspecs++; } } - /* Cleanup and get ready for next segment. */ hbcleanup(&shaped); return numspecs; } +/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen` + * is the length of the dash plus the length of the gap. `fraction` is the + * fraction of the dash length compared to `wavelen`. */ +static void +xdrawunderdashed(Draw draw, Color *color, int x, int y, int w, + int wavelen, float fraction, int thick) +{ + int dashw = MAX(1, fraction * wavelen); + for (int i = x - x % wavelen; i < x + w; i += wavelen) { + int startx = MAX(i, x); + int endx = MIN(i + dashw, x + w); + if (startx < endx) + XftDrawRect(xw.draw, color, startx, y, endx - startx, + thick); + } +} + +/* Draws an undercurl. `h` is the total height, including line thickness. */ +static void +xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick) +{ + XGCValues gcvals = {.foreground = color->pixel, + .line_width = thick, + .line_style = LineSolid, + .cap_style = CapRound}; + GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw), + GCForeground | GCLineWidth | GCLineStyle | GCCapStyle, + &gcvals); + + XRectangle clip = {.x = x, .y = y, .width = w, .height = h}; + XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted); + + int yoffset = thick / 2; + int segh = MAX(1, h - thick); + /* Make sure every segment is at a 45 degree angle, otherwise it doesn't + * look good without antialiasing. */ + int segw = segh; + int wavelen = MAX(1, segw * 2); + + for (int i = x - (x % wavelen); i < x + w; i += wavelen) { + XPoint points[3] = {{.x = i, .y = y + yoffset}, + {.x = i + segw, .y = y + yoffset + segh}, + {.x = i + wavelen, .y = y + yoffset}}; + XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3, + CoordModeOrigin); + } + + XFreeGC(xw.dpy, gc); +} + void -xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int charlen) +xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int charlen, int dmode) { int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, width = charlen * win.cw; @@ -1509,46 +1678,101 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i fg = bg; /* Intelligent cleaning up of the borders. */ - if (x == 0) { - xclear(0, (y == 0)? 0 : winy, 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); + if (dmode & DRAW_BG) { + if (x == 0) { + xclear(0, (y == 0)? 0 : winy, win.hborderpx, + winy + win.ch + + ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0)); + } + if (winx + width >= win.hborderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, + ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) + xclear(winx, 0, winx + width, win.vborderpx); + if (winy + win.ch >= win.vborderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); - /* Clean up the region we want to draw to. */ - XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + /* 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. */ + if (!(dmode & DRAW_FG)) + return; + + /* Set the clip region only for the underline. */ r.x = 0; r.y = 0; r.height = win.ch; r.width = width; XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); - /* Render the glyphs. */ - XftDrawGlyphFontSpec(xw.draw, fg, specs, len); - - /* Render underline and strikethrough. */ - if (base.mode & ATTR_UNDERLINE) { - XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, - width, 1); + /* Decoration color. */ + Color decor; + uint32_t decorcolor = tgetdecorcolor(&base); + if (decorcolor == DECOR_DEFAULT_COLOR) { + decor = *fg; + } else if (IS_TRUECOL(decorcolor)) { + colfg.alpha = 0xffff; + colfg.red = TRUERED(decorcolor); + colfg.green = TRUEGREEN(decorcolor); + colfg.blue = TRUEBLUE(decorcolor); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor); + } else { + decor = dc.col[decorcolor]; } + decor.color.alpha = 0xffff; + decor.pixel |= 0xff << 24; - if (base.mode & ATTR_STRUCK) { - XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, - width, 1); + /* Float thickness, used as a base to compute other values. */ + float fthick = dc.font.height / 18.0; + /* Integer thickness in pixels. Must not be 0. */ + int thick = MAX(1, roundf(fthick)); + /* The default gap between the baseline and a single underline. */ + int gap = roundf(fthick * 2); + /* The total thickness of a double underline. */ + int doubleh = thick * 2 + ceilf(fthick * 0.5); + /* The total thickness of an undercurl. */ + int curlh = thick * 2 + roundf(fthick * 0.75); + + /* Render the underline before the glyphs. */ + if (base.mode & ATTR_UNDERLINE) { + uint32_t style = tgetdecorstyle(&base); + int liney = winy + dc.font.ascent + gap; + /* Adjust liney to guarantee that a single underline fits. */ + liney -= MAX(0, liney + thick - (winy + win.ch)); + if (style == UNDERLINE_DOUBLE) { + liney -= MAX(0, liney + doubleh - (winy + win.ch)); + XftDrawRect(xw.draw, &decor, winx, liney, width, thick); + XftDrawRect(xw.draw, &decor, winx, + liney + doubleh - thick, width, thick); + } else if (style == UNDERLINE_DOTTED) { + xdrawunderdashed(xw.draw, &decor, winx, liney, width, + thick * 2, 0.5, thick); + } else if (style == UNDERLINE_DASHED) { + int wavelen = MAX(2, win.cw * 0.9); + xdrawunderdashed(xw.draw, &decor, winx, liney, width, + wavelen, 0.65, thick); + } else if (style == UNDERLINE_CURLY) { + liney -= MAX(0, liney + curlh - (winy + win.ch)); + xdrawundercurl(xw.draw, &decor, winx, liney, width, + curlh, thick); + } else { + XftDrawRect(xw.draw, &decor, winx, liney, width, thick); + } } /* Reset clip to none. */ XftDrawSetClip(xw.draw, 0); + + /* Render the glyphs. */ + XftDrawGlyphFontSpec(xw.draw, fg, specs, len); + + /* Render strikethrough. Alway use the fg color. */ + if (base.mode & ATTR_STRUCK) { + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3, + width, thick); + } } void @@ -1558,7 +1782,12 @@ xdrawglyph(Glyph g, int x, int y) XftGlyphFontSpec *specs = xw.specbuf; numspecs = xmakeglyphfontspecs(specs, &g, 1, x, y); - xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1); + xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1, DRAW_BG | DRAW_FG); + if (g.mode & ATTR_IMAGE) { + gr_start_drawing(xw.buf, win.cw, win.ch); + xdrawoneimagecell(g, x, y); + gr_finish_drawing(xw.buf); + } } void @@ -1572,11 +1801,17 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int le /* Redraw the line where cursor was previously. * It will restore the ligatures broken by the cursor. */ + gr_start_drawing(xw.buf, win.cw, win.ch); xdrawline(line, 0, oy, len); + gr_finish_drawing(xw.buf); if (IS_SET(MODE_HIDE)) return; + // If it's an image, just draw a ballot box for simplicity. + if (g.mode & ATTR_IMAGE) + g.u = 0x2610; + /* * Select the right color for the right mode. */ @@ -1650,6 +1885,134 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int le } } +/* 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) { @@ -1704,29 +2067,38 @@ xdrawline(Line line, int x1, int y1, int x2) { int i, x, ox, numspecs; Glyph base, new; - XftGlyphFontSpec *specs = xw.specbuf; + XftGlyphFontSpec *specs; - i = ox = 0; - for (x = x1; x < x2; x++) { - new = line[x]; - if (new.mode == ATTR_WDUMMY) - continue; - if (selected(x, y1)) - new.mode ^= ATTR_REVERSE; - if ((i > 0) && ATTRCMP(base, new)) { - numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1); - xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox); - i = 0; + /* Draw line in 2 passes: background and foreground. This way wide + * glyphs won't get truncated (#223) */ + for (int dmode = DRAW_BG; dmode <= DRAW_FG; dmode <<= 1) { + specs = xw.specbuf; + i = ox = 0; + for (x = x1; x < x2; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1); + xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox, dmode); + if (base.mode & ATTR_IMAGE && dmode == DRAW_FG) + xdrawimages(base, line, ox, y1, x); + i = 0; + } + if (i == 0) { + ox = x; + base = new; + } + i++; } - if (i == 0) { - ox = x; - base = new; + if (i > 0) { + numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1); + xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox, dmode); } - i++; - } - if (i > 0) { - numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1); - xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox); + if (i > 0 && base.mode & ATTR_IMAGE && dmode == DRAW_FG) + xdrawimages(base, line, ox, y1, x); } } @@ -1746,8 +2118,8 @@ xximspot(int x, int y) if (xw.ime.xic == NULL) return; - xw.ime.spot.x = win.hborderpx + x * win.cw; - xw.ime.spot.y = win.vborderpx + (y + 1) * win.ch; + 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); } @@ -1952,6 +2324,7 @@ cmessage(XEvent *e) } } else if (e->xclient.data.l[0] == xw.wmdeletewin) { ttyhangup(); + gr_deinit(); exit(0); } } @@ -2002,6 +2375,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;