From fbda573897bd026467e256de8559f3fe1a8c9d2d Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:51:52 +0200 Subject: [PATCH 01/11] Mover los programas a suckless/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primer paso para convertir el repo en dotfiles: los programas de suckless van juntos en una carpeta y la raíz queda para la configuración de la sesión y el script de instalación. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m --- .gitignore | 28 +++++++++--------- {clipmenu => suckless/clipmenu}/.clang-format | 0 .../clipmenu}/.github/workflows/ci.yml | 0 {clipmenu => suckless/clipmenu}/.gitignore | 0 {clipmenu => suckless/clipmenu}/LICENSE | 0 {clipmenu => suckless/clipmenu}/Makefile | 0 {clipmenu => suckless/clipmenu}/README.md | 0 .../clipmenu}/init/clipmenud.service.in | 0 {clipmenu => suckless/clipmenu}/man/clipctl.1 | 0 {clipmenu => suckless/clipmenu}/man/clipdel.1 | 0 .../clipmenu}/man/clipdelmenu.1 | 0 .../clipmenu}/man/clipmenu.1 | 0 .../clipmenu}/man/clipmenu.conf.5 | 0 .../clipmenu}/man/clipmenud.1 | 0 .../clipmenu}/man/clipserve.1 | 0 {clipmenu => suckless/clipmenu}/src/clipctl.c | 0 {clipmenu => suckless/clipmenu}/src/clipdel.c | 0 .../clipmenu}/src/clipdelmenu.c | 0 .../clipmenu}/src/clipmenu.c | 0 .../clipmenu}/src/clipmenud.c | 0 .../clipmenu}/src/clipserve.c | 0 {clipmenu => suckless/clipmenu}/src/config.c | 0 {clipmenu => suckless/clipmenu}/src/config.h | 0 .../clipmenu}/src/menu_util.c | 0 .../clipmenu}/src/menu_util.h | 0 {clipmenu => suckless/clipmenu}/src/store.c | 0 {clipmenu => suckless/clipmenu}/src/store.h | 0 {clipmenu => suckless/clipmenu}/src/util.c | 0 {clipmenu => suckless/clipmenu}/src/util.h | 0 {clipmenu => suckless/clipmenu}/src/x.c | 0 {clipmenu => suckless/clipmenu}/src/x.h | 0 .../clipmenu}/tests/test_store.c | 0 .../clipmenu}/tests/x_integration_tests | 0 {dmenu => suckless/dmenu}/LICENSE | 0 {dmenu => suckless/dmenu}/Makefile | 0 {dmenu => suckless/dmenu}/README | 0 {dmenu => suckless/dmenu}/arg.h | 0 {dmenu => suckless/dmenu}/config.def.h | 0 {dmenu => suckless/dmenu}/config.h | 0 {dmenu => suckless/dmenu}/config.mk | 0 {dmenu => suckless/dmenu}/dmenu.1 | 0 {dmenu => suckless/dmenu}/dmenu.c | 0 {dmenu => suckless/dmenu}/dmenu_path | 0 {dmenu => suckless/dmenu}/dmenu_run | 0 {dmenu => suckless/dmenu}/drw.c | 0 {dmenu => suckless/dmenu}/drw.h | 0 {dmenu => suckless/dmenu}/stest.1 | 0 {dmenu => suckless/dmenu}/stest.c | 0 {dmenu => suckless/dmenu}/util.c | 0 {dmenu => suckless/dmenu}/util.h | 0 {dwm => suckless/dwm}/LICENSE | 0 {dwm => suckless/dwm}/Makefile | 0 {dwm => suckless/dwm}/README | 0 {dwm => suckless/dwm}/config.def.h | 0 {dwm => suckless/dwm}/config.h | 0 {dwm => suckless/dwm}/config.mk | 0 {dwm => suckless/dwm}/drw.c | 0 {dwm => suckless/dwm}/drw.h | 0 {dwm => suckless/dwm}/dwm.1 | 0 {dwm => suckless/dwm}/dwm.c | 0 {dwm => suckless/dwm}/dwm.png | Bin .../dwm}/patches/dwm-fullgaps-6.4.diff | 0 .../patches/dwm-preserveonrestart-6.3.diff | 0 .../patches/dwm-restartsig-20180523-6.2.diff | 0 ...m-statuscmd-nosignal-20210402-67d76bd.diff | 0 .../dwm}/patches/dwm-swallow-6.3.diff | 0 {dwm => suckless/dwm}/transient.c | 0 {dwm => suckless/dwm}/util.c | 0 {dwm => suckless/dwm}/util.h | 0 {scroll => suckless/scroll}/.gitignore | 0 {scroll => suckless/scroll}/LICENSE | 0 {scroll => suckless/scroll}/Makefile | 0 {scroll => suckless/scroll}/README | 0 {scroll => suckless/scroll}/TODO | 0 {scroll => suckless/scroll}/config.def.h | 0 {scroll => suckless/scroll}/config.h | 0 {scroll => suckless/scroll}/config.mk | 0 {scroll => suckless/scroll}/perf.sh | 0 {scroll => suckless/scroll}/ptty.c | 0 {scroll => suckless/scroll}/scroll.1 | 0 {scroll => suckless/scroll}/scroll.c | 0 {scroll => suckless/scroll}/up.log | 0 {scroll => suckless/scroll}/up.sh | 0 {slock => suckless/slock}/LICENSE | 0 {slock => suckless/slock}/Makefile | 0 {slock => suckless/slock}/README | 0 {slock => suckless/slock}/config.def.h | 0 {slock => suckless/slock}/config.h | 0 {slock => suckless/slock}/config.mk | 0 {slock => suckless/slock}/explicit_bzero.c | 0 {slock => suckless/slock}/slock.1 | 0 {slock => suckless/slock}/slock.c | 0 {slock => suckless/slock}/util.h | 0 {slstatus => suckless/slstatus}/LICENSE | 0 {slstatus => suckless/slstatus}/Makefile | 0 {slstatus => suckless/slstatus}/README | 0 {slstatus => suckless/slstatus}/arg.h | 0 .../slstatus}/components/battery.c | 0 .../slstatus}/components/cat.c | 0 .../slstatus}/components/cpu.c | 0 .../slstatus}/components/datetime.c | 0 .../slstatus}/components/disk.c | 0 .../slstatus}/components/entropy.c | 0 .../slstatus}/components/hostname.c | 0 .../slstatus}/components/ip.c | 0 .../slstatus}/components/kernel_release.c | 0 .../components/keyboard_indicators.c | 0 .../slstatus}/components/keymap.c | 0 .../slstatus}/components/load_avg.c | 0 .../slstatus}/components/netspeeds.c | 0 .../slstatus}/components/num_files.c | 0 .../slstatus}/components/ram.c | 0 .../slstatus}/components/run_command.c | 0 .../slstatus}/components/swap.c | 0 .../slstatus}/components/temperature.c | 0 .../slstatus}/components/uptime.c | 0 .../slstatus}/components/user.c | 0 .../slstatus}/components/volume.c | 0 .../slstatus}/components/wifi.c | 0 {slstatus => suckless/slstatus}/config.def.h | 0 {slstatus => suckless/slstatus}/config.h | 0 {slstatus => suckless/slstatus}/config.mk | 0 {slstatus => suckless/slstatus}/slstatus.1 | 0 {slstatus => suckless/slstatus}/slstatus.c | 0 {slstatus => suckless/slstatus}/slstatus.h | 0 {slstatus => suckless/slstatus}/util.c | 0 {slstatus => suckless/slstatus}/util.h | 0 {st => suckless/st}/FAQ | 0 {st => suckless/st}/LEGACY | 0 {st => suckless/st}/LICENSE | 0 {st => suckless/st}/Makefile | 0 {st => suckless/st}/README | 0 {st => suckless/st}/TODO | 0 {st => suckless/st}/arg.h | 0 {st => suckless/st}/config.def.h | 0 {st => suckless/st}/config.h | 0 {st => suckless/st}/config.mk | 0 {st => suckless/st}/hb.c | 0 {st => suckless/st}/hb.h | 0 .../patches/st-alpha-20240814-a0274bc.diff | 0 .../patches/st-anysize-20220718-baa9357.diff | 0 .../patches/st-ligatures-20251007-0.9.3.diff | 0 {st => suckless/st}/st.1 | 0 {st => suckless/st}/st.c | 0 {st => suckless/st}/st.h | 0 {st => suckless/st}/st.info | 0 {st => suckless/st}/win.h | 0 {st => suckless/st}/x.c | 0 148 files changed, 14 insertions(+), 14 deletions(-) rename {clipmenu => suckless/clipmenu}/.clang-format (100%) rename {clipmenu => suckless/clipmenu}/.github/workflows/ci.yml (100%) rename {clipmenu => suckless/clipmenu}/.gitignore (100%) rename {clipmenu => suckless/clipmenu}/LICENSE (100%) rename {clipmenu => suckless/clipmenu}/Makefile (100%) rename {clipmenu => suckless/clipmenu}/README.md (100%) rename {clipmenu => suckless/clipmenu}/init/clipmenud.service.in (100%) rename {clipmenu => suckless/clipmenu}/man/clipctl.1 (100%) rename {clipmenu => suckless/clipmenu}/man/clipdel.1 (100%) rename {clipmenu => suckless/clipmenu}/man/clipdelmenu.1 (100%) rename {clipmenu => suckless/clipmenu}/man/clipmenu.1 (100%) rename {clipmenu => suckless/clipmenu}/man/clipmenu.conf.5 (100%) rename {clipmenu => suckless/clipmenu}/man/clipmenud.1 (100%) rename {clipmenu => suckless/clipmenu}/man/clipserve.1 (100%) rename {clipmenu => suckless/clipmenu}/src/clipctl.c (100%) rename {clipmenu => suckless/clipmenu}/src/clipdel.c (100%) rename {clipmenu => suckless/clipmenu}/src/clipdelmenu.c (100%) rename {clipmenu => suckless/clipmenu}/src/clipmenu.c (100%) rename {clipmenu => suckless/clipmenu}/src/clipmenud.c (100%) rename {clipmenu => suckless/clipmenu}/src/clipserve.c (100%) rename {clipmenu => suckless/clipmenu}/src/config.c (100%) rename {clipmenu => suckless/clipmenu}/src/config.h (100%) rename {clipmenu => suckless/clipmenu}/src/menu_util.c (100%) rename {clipmenu => suckless/clipmenu}/src/menu_util.h (100%) rename {clipmenu => suckless/clipmenu}/src/store.c (100%) rename {clipmenu => suckless/clipmenu}/src/store.h (100%) rename {clipmenu => suckless/clipmenu}/src/util.c (100%) rename {clipmenu => suckless/clipmenu}/src/util.h (100%) rename {clipmenu => suckless/clipmenu}/src/x.c (100%) rename {clipmenu => suckless/clipmenu}/src/x.h (100%) rename {clipmenu => suckless/clipmenu}/tests/test_store.c (100%) rename {clipmenu => suckless/clipmenu}/tests/x_integration_tests (100%) rename {dmenu => suckless/dmenu}/LICENSE (100%) rename {dmenu => suckless/dmenu}/Makefile (100%) rename {dmenu => suckless/dmenu}/README (100%) rename {dmenu => suckless/dmenu}/arg.h (100%) rename {dmenu => suckless/dmenu}/config.def.h (100%) rename {dmenu => suckless/dmenu}/config.h (100%) rename {dmenu => suckless/dmenu}/config.mk (100%) rename {dmenu => suckless/dmenu}/dmenu.1 (100%) rename {dmenu => suckless/dmenu}/dmenu.c (100%) rename {dmenu => suckless/dmenu}/dmenu_path (100%) rename {dmenu => suckless/dmenu}/dmenu_run (100%) rename {dmenu => suckless/dmenu}/drw.c (100%) rename {dmenu => suckless/dmenu}/drw.h (100%) rename {dmenu => suckless/dmenu}/stest.1 (100%) rename {dmenu => suckless/dmenu}/stest.c (100%) rename {dmenu => suckless/dmenu}/util.c (100%) rename {dmenu => suckless/dmenu}/util.h (100%) rename {dwm => suckless/dwm}/LICENSE (100%) rename {dwm => suckless/dwm}/Makefile (100%) rename {dwm => suckless/dwm}/README (100%) rename {dwm => suckless/dwm}/config.def.h (100%) rename {dwm => suckless/dwm}/config.h (100%) rename {dwm => suckless/dwm}/config.mk (100%) rename {dwm => suckless/dwm}/drw.c (100%) rename {dwm => suckless/dwm}/drw.h (100%) rename {dwm => suckless/dwm}/dwm.1 (100%) rename {dwm => suckless/dwm}/dwm.c (100%) rename {dwm => suckless/dwm}/dwm.png (100%) rename {dwm => suckless/dwm}/patches/dwm-fullgaps-6.4.diff (100%) rename {dwm => suckless/dwm}/patches/dwm-preserveonrestart-6.3.diff (100%) rename {dwm => suckless/dwm}/patches/dwm-restartsig-20180523-6.2.diff (100%) rename {dwm => suckless/dwm}/patches/dwm-statuscmd-nosignal-20210402-67d76bd.diff (100%) rename {dwm => suckless/dwm}/patches/dwm-swallow-6.3.diff (100%) rename {dwm => suckless/dwm}/transient.c (100%) rename {dwm => suckless/dwm}/util.c (100%) rename {dwm => suckless/dwm}/util.h (100%) rename {scroll => suckless/scroll}/.gitignore (100%) rename {scroll => suckless/scroll}/LICENSE (100%) rename {scroll => suckless/scroll}/Makefile (100%) rename {scroll => suckless/scroll}/README (100%) rename {scroll => suckless/scroll}/TODO (100%) rename {scroll => suckless/scroll}/config.def.h (100%) rename {scroll => suckless/scroll}/config.h (100%) rename {scroll => suckless/scroll}/config.mk (100%) rename {scroll => suckless/scroll}/perf.sh (100%) rename {scroll => suckless/scroll}/ptty.c (100%) rename {scroll => suckless/scroll}/scroll.1 (100%) rename {scroll => suckless/scroll}/scroll.c (100%) rename {scroll => suckless/scroll}/up.log (100%) rename {scroll => suckless/scroll}/up.sh (100%) rename {slock => suckless/slock}/LICENSE (100%) rename {slock => suckless/slock}/Makefile (100%) rename {slock => suckless/slock}/README (100%) rename {slock => suckless/slock}/config.def.h (100%) rename {slock => suckless/slock}/config.h (100%) rename {slock => suckless/slock}/config.mk (100%) rename {slock => suckless/slock}/explicit_bzero.c (100%) rename {slock => suckless/slock}/slock.1 (100%) rename {slock => suckless/slock}/slock.c (100%) rename {slock => suckless/slock}/util.h (100%) rename {slstatus => suckless/slstatus}/LICENSE (100%) rename {slstatus => suckless/slstatus}/Makefile (100%) rename {slstatus => suckless/slstatus}/README (100%) rename {slstatus => suckless/slstatus}/arg.h (100%) rename {slstatus => suckless/slstatus}/components/battery.c (100%) rename {slstatus => suckless/slstatus}/components/cat.c (100%) rename {slstatus => suckless/slstatus}/components/cpu.c (100%) rename {slstatus => suckless/slstatus}/components/datetime.c (100%) rename {slstatus => suckless/slstatus}/components/disk.c (100%) rename {slstatus => suckless/slstatus}/components/entropy.c (100%) rename {slstatus => suckless/slstatus}/components/hostname.c (100%) rename {slstatus => suckless/slstatus}/components/ip.c (100%) rename {slstatus => suckless/slstatus}/components/kernel_release.c (100%) rename {slstatus => suckless/slstatus}/components/keyboard_indicators.c (100%) rename {slstatus => suckless/slstatus}/components/keymap.c (100%) rename {slstatus => suckless/slstatus}/components/load_avg.c (100%) rename {slstatus => suckless/slstatus}/components/netspeeds.c (100%) rename {slstatus => suckless/slstatus}/components/num_files.c (100%) rename {slstatus => suckless/slstatus}/components/ram.c (100%) rename {slstatus => suckless/slstatus}/components/run_command.c (100%) rename {slstatus => suckless/slstatus}/components/swap.c (100%) rename {slstatus => suckless/slstatus}/components/temperature.c (100%) rename {slstatus => suckless/slstatus}/components/uptime.c (100%) rename {slstatus => suckless/slstatus}/components/user.c (100%) rename {slstatus => suckless/slstatus}/components/volume.c (100%) rename {slstatus => suckless/slstatus}/components/wifi.c (100%) rename {slstatus => suckless/slstatus}/config.def.h (100%) rename {slstatus => suckless/slstatus}/config.h (100%) rename {slstatus => suckless/slstatus}/config.mk (100%) rename {slstatus => suckless/slstatus}/slstatus.1 (100%) rename {slstatus => suckless/slstatus}/slstatus.c (100%) rename {slstatus => suckless/slstatus}/slstatus.h (100%) rename {slstatus => suckless/slstatus}/util.c (100%) rename {slstatus => suckless/slstatus}/util.h (100%) rename {st => suckless/st}/FAQ (100%) rename {st => suckless/st}/LEGACY (100%) rename {st => suckless/st}/LICENSE (100%) rename {st => suckless/st}/Makefile (100%) rename {st => suckless/st}/README (100%) rename {st => suckless/st}/TODO (100%) rename {st => suckless/st}/arg.h (100%) rename {st => suckless/st}/config.def.h (100%) rename {st => suckless/st}/config.h (100%) rename {st => suckless/st}/config.mk (100%) rename {st => suckless/st}/hb.c (100%) rename {st => suckless/st}/hb.h (100%) rename {st => suckless/st}/patches/st-alpha-20240814-a0274bc.diff (100%) rename {st => suckless/st}/patches/st-anysize-20220718-baa9357.diff (100%) rename {st => suckless/st}/patches/st-ligatures-20251007-0.9.3.diff (100%) rename {st => suckless/st}/st.1 (100%) rename {st => suckless/st}/st.c (100%) rename {st => suckless/st}/st.h (100%) rename {st => suckless/st}/st.info (100%) rename {st => suckless/st}/win.h (100%) rename {st => suckless/st}/x.c (100%) 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/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 100% rename from st/Makefile rename to suckless/st/Makefile 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 100% rename from st/config.def.h rename to suckless/st/config.def.h diff --git a/st/config.h b/suckless/st/config.h similarity index 100% rename from st/config.h rename to suckless/st/config.h diff --git a/st/config.mk b/suckless/st/config.mk similarity index 100% rename from st/config.mk rename to suckless/st/config.mk 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/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/st/patches/st-anysize-20220718-baa9357.diff b/suckless/st/patches/st-anysize-20220718-baa9357.diff similarity index 100% rename from st/patches/st-anysize-20220718-baa9357.diff rename to suckless/st/patches/st-anysize-20220718-baa9357.diff 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/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 100% rename from st/st.c rename to suckless/st/st.c diff --git a/st/st.h b/suckless/st/st.h similarity index 100% rename from st/st.h rename to suckless/st/st.h diff --git a/st/st.info b/suckless/st/st.info similarity index 100% rename from st/st.info rename to suckless/st/st.info diff --git a/st/win.h b/suckless/st/win.h similarity index 100% rename from st/win.h rename to suckless/st/win.h diff --git a/st/x.c b/suckless/st/x.c similarity index 100% rename from st/x.c rename to suckless/st/x.c From cb051ead76619089a2214c1ed8bffb91bdfbe642 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 02/11] Squashed 'suckless/clipmenu/' content from commit e0ec3bc6 git-subtree-dir: suckless/clipmenu git-subtree-split: e0ec3bc6608ecc70ed17e8da76e27bf90b1e8e90 --- .clang-format | 11 + .github/workflows/ci.yml | 28 ++ .gitignore | 9 + LICENSE | 21 + Makefile | 135 ++++++ README.md | 97 +++++ init/clipmenud.service.in | 21 + man/clipctl.1 | 56 +++ man/clipdel.1 | 53 +++ man/clipdelmenu.1 | 43 ++ man/clipmenu.1 | 42 ++ man/clipmenu.conf.5 | 124 ++++++ man/clipmenud.1 | 32 ++ man/clipserve.1 | 44 ++ src/clipctl.c | 152 +++++++ src/clipdel.c | 162 ++++++++ src/clipdelmenu.c | 45 ++ src/clipmenu.c | 42 ++ src/clipmenud.c | 765 ++++++++++++++++++++++++++++++++++ src/clipserve.c | 286 +++++++++++++ src/config.c | 500 ++++++++++++++++++++++ src/config.h | 104 +++++ src/menu_util.c | 302 ++++++++++++++ src/menu_util.h | 18 + src/store.c | 847 +++++++++++++++++++++++++++++++++++++ src/store.h | 192 +++++++++ src/util.c | 165 ++++++++ src/util.h | 111 +++++ src/x.c | 121 ++++++ src/x.h | 34 ++ tests/test_store.c | 848 ++++++++++++++++++++++++++++++++++++++ tests/x_integration_tests | 350 ++++++++++++++++ 32 files changed, 5760 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 init/clipmenud.service.in create mode 100644 man/clipctl.1 create mode 100644 man/clipdel.1 create mode 100644 man/clipdelmenu.1 create mode 100644 man/clipmenu.1 create mode 100644 man/clipmenu.conf.5 create mode 100644 man/clipmenud.1 create mode 100644 man/clipserve.1 create mode 100644 src/clipctl.c create mode 100644 src/clipdel.c create mode 100644 src/clipdelmenu.c create mode 100644 src/clipmenu.c create mode 100644 src/clipmenud.c create mode 100644 src/clipserve.c create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/menu_util.c create mode 100644 src/menu_util.h create mode 100644 src/store.c create mode 100644 src/store.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 src/x.c create mode 100644 src/x.h create mode 100644 tests/test_store.c create mode 100755 tests/x_integration_tests diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..076d317 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BinPackArguments: true +BreakStringLiterals: false +ColumnLimit: 80 +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 4 +InsertBraces: true +PenaltyBreakComment: 0 +SortIncludes: true +SpaceBeforeParens: ControlStatementsExceptForEachMacros +UseTab: Never diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b412fc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +jobs: + build_and_test: + name: CI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + - run: sudo apt-get update + + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: gcc-10 clang clang-format clang-tidy cppcheck xvfb xsel libxfixes-dev libx11-dev + version: 1.0 + + - run: gcc --version + + - run: make clean analyse + - run: make tests + - run: make integration_tests + env: + NO_PID_NAMESPACE: 1 + +on: + push: + pull_request: + workflow_dispatch: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3668d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +src/clipmenud +src/clipctl +src/clipdel +src/clipdelmenu +src/clipmenu +src/clipserve +tests/test +tests/test_store +*.o diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9019292 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2014-present Christopher Down + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..934c0d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,135 @@ +CFLAGS := -std=gnu11 -O2 -Wall -Wextra -Wshadow -Wpointer-arith \ + -Wcast-align -Wmissing-prototypes -Wstrict-overflow -Wformat=2 \ + -Wwrite-strings -Warray-bounds -Wstrict-prototypes \ + -Wno-maybe-uninitialized \ + -Werror $(CFLAGS) +CPPFLAGS += -I/usr/X11R6/include -L/usr/X11R6/lib +LDLIBS += -lX11 -lXfixes +PREFIX ?= /usr/local +bindir := $(PREFIX)/bin +datarootdir := $(PREFIX)/share +mandir := $(datarootdir)/man +systemd_user_dir = $(DESTDIR)$(PREFIX)/lib/systemd/user +debug_cflags := -D_FORTIFY_SOURCE=2 -fsanitize=leak -fsanitize=address \ + -fsanitize=undefined -Og -ggdb -fno-omit-frame-pointer \ + -fstack-protector-strong +c_files := $(wildcard src/*.c) +h_files := $(wildcard src/*.h) +libs := $(filter $(c_files:.c=.o), $(h_files:.h=.o)) + +man1_files = clipctl.1 clipdel.1 clipdelmenu.1 clipmenu.1 clipmenud.1 clipserve.1 +man5_files = clipmenu.conf.5 + +bins := clipctl clipmenud clipdel clipdelmenu clipserve clipmenu + +all: $(addprefix src/,$(bins)) + +src/%: src/%.c $(libs) + $(CC) $(CFLAGS) $(CPPFLAGS) $^ $(LDFLAGS) $(LDLIBS) -o $@ + +src/%.o: src/%.c src/%.h + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + +debug: all +debug: CFLAGS+=$(debug_cflags) + +install: all + @for f in $(man1_files); do \ + install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man1/$$f; \ + done + @for f in $(man5_files); do \ + install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man5/$$f; \ + done + + mkdir -p $(DESTDIR)$(bindir)/ + install -pt $(DESTDIR)$(bindir)/ $(addprefix src/,$(bins)) + mkdir -p $(systemd_user_dir) + sed 's|@bindir@|$(bindir)|g' init/clipmenud.service.in > $(systemd_user_dir)/clipmenud.service + +uninstall: + rm -f $(addprefix $(DESTDIR)$(PREFIX)/bin/,$(bins)) + rm -f "$(DESTDIR)${PREFIX}/lib/systemd/user/clipmenud.service" + rm -f $(addprefix $(DESTDIR)$(mandir)/man1/,$(man1_files)) + rm -f $(addprefix $(DESTDIR)$(mandir)/man5/,$(man5_files)) + +clean: + rm -f src/*.o src/*~ $(addprefix src/,$(bins)) tests/test_store + +clang_supports_unsafe_buffer_usage := $(shell clang -x c -c /dev/null -o /dev/null -Werror -Wunsafe-buffer-usage > /dev/null 2>&1; echo $$?) +ifeq ($(clang_supports_unsafe_buffer_usage),0) + extra_clang_flags := -Wno-unsafe-buffer-usage -Wno-missing-include-dirs \ + -Wno-unknown-warning-option \ + -Wno-unused-command-line-argument \ + -Wno-error +else + extra_clang_flags := -Wno-missing-include-dirs \ + -Wno-unknown-warning-option \ + -Wno-unused-command-line-argument \ + -Wno-error +endif + +c_analyse_targets := $(c_files:%=%-analyse) +h_analyse_targets := $(h_files:%=%-analyse) + +analyse: CFLAGS+=$(debug_cflags) +analyse: cppcheck $(c_analyse_targets) $(h_analyse_targets) + +$(c_analyse_targets): %-analyse: % + # -W options here are not clang compatible, so out of generic CFLAGS + gcc $< -o /dev/null -c \ + -std=gnu99 -Ofast -fwhole-program -Wall -Wextra \ + -Wlogical-op -Wduplicated-cond \ + -fanalyzer $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) + clang $< -o /dev/null -c -std=gnu99 -Ofast \ + $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) \ + $(extra_clang_flags) + $(MAKE) $*-shared-analyse + +$(h_analyse_targets): %-analyse: + $(MAKE) $*-shared-analyse + +%-shared-analyse: % + # clang-analyzer-unix.Malloc does not understand _drop_() + clang-tidy $< --quiet -checks=-clang-analyzer-unix.Malloc -- -std=gnu99 + clang-format --dry-run --Werror $< + +# --suppress=missingIncludeSystem: +# +# Without this there's a bunch of noise from cppcheck from the system headers +# themselves. +# +# --suppress=unusedFunction: +# +# cppcheck does not understand _drop_ and marks those as unused. This is +# already well checked by Clang/GCC, just leave it to them. +# +# --suppress=unmatchedSuppression: +# +# We run both locally and on CI, so there may be some suppressions that +# depending on version do not match in one version but do on another. +# +# --suppress=unusedStructMember +# +# Structs are used across translation units and cppcheck gets this wrong. +cppcheck: $(c_files) $(h_files) + cppcheck $(c_files) $(h_files) --std=c99 --quiet --inline-suppr --force \ + --enable=all \ + --suppress=missingIncludeSystem \ + --suppress=unusedFunction \ + --suppress=unmatchedSuppression \ + --suppress=checkersReport \ + --suppress=unusedStructMember \ + --check-level=exhaustive \ + --max-ctu-depth=10 --error-exitcode=1 + +tests: tests/test_store + tests/test_store + +integration_tests: + tests/x_integration_tests + +tests/test_store: tests/test_store.c src/store.o src/util.o + $(CC) $(CFLAGS) $(CPPFLAGS) -I./src -o $@ $^ $(LDLIBS) + +.PHONY: all debug install uninstall clean analyse tests integration_tests \ + cppcheck diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf29fac --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +clipmenu is a simple clipboard manager using [dmenu][], [rofi][] or similar. + +# Demo + +![Demo](https://cloud.githubusercontent.com/assets/660663/24079784/6f76da94-0c88-11e7-8251-40b1f02ebf3c.gif) + +# Usage + +## clipmenud + +Start `clipmenud`, then run `clipmenu` to select something to put on the +clipboard. For systemd users, a user service called `clipmenud` is packaged as +part of the project. + +If you start X via `startx`/`xinit` and use the systemd user service, make sure +`$DISPLAY` is set so clipmenud knows which X server to use. Most distributions +include scripts in `/etc/X11/xinit/xinitrc.d/` for this, so if they are present +you can usually just source them in your `~/.xinitrc`. Alternatively, before +launching the clipmenud service, run: + + systemctl --user import-environment DISPLAY + +## clipmenu + +You may wish to bind a shortcut in your window manager to launch `clipmenu`. + +All args passed to clipmenu are transparently dispatched to dmenu. That is, if +you usually call dmenu with args to set colours and other properties, you can +invoke clipmenu in exactly the same way to get the same effect, like so: + + clipmenu -i -fn Terminus:size=8 -nb '#002b36' -nf '#839496' -sb '#073642' -sf '#93a1a1' + +By default, clipmenu also adds dmenu-compatible defaults such as `-p` and `-l`. +If that gets in the way for a custom launcher, set `launcher_pass_dmenu_args` to +0 in `clipmenu.conf`. + +For a full list of configuration options (and their environment variable +equivalents), please see `man clipmenu.conf`. + +There is also `clipdel` to delete clips, `clipdelmenu` for interactive deletion, +and `clipctl` to enable or disable clipboard monitoring. + +# Features + +The behaviour of `clipmenud` can be customised through a config file. As some +examples of things you can change: + +* Customising the maximum number of clips stored (default 1000) +* Disabling clip collection temporarily with `clipctl disable`, reenabling with + `clipctl enable` +* Deduplicating repeated clips so only the newest remains +* Not storing clipboard changes from certain applications, like password + managers +* Taking direct ownership of the clipboard +* ...and much more. + +See `man clipmenu.conf` to view all possible configuration variables and what +they do. + +# Supported launchers + +Any dmenu-compliant application will work, but here are `CM_LAUNCHER` +configurations that are known to work: + +- `dmenu` (the default) +- `fzf` +- `rofi` + +# Installation + +Several distributions, including Arch and Nix, provide clipmenu as an official +package called `clipmenu`. + +## Manual installation + +If your distribution doesn't provide a package, you can manually install using +`make install` (or better yet, create a package for your distribution!). + +# How does it work? + +## clipmenud + +1. clipmenud passively monitors X11 clipboard selections (PRIMARY, CLIPBOARD, + and SECONDARY) for changes using XFixes (no polling). +2. If `clipmenud` detects changes to the clipboard contents, it writes them out + to storage and indexes using a hash as the filename. + +## clipmenu + +1. `clipmenu` reads the index to find all available clips. +2. `dmenu` (or another configured launcher) is executed to allow the user to + select a clip. +3. After selection, the clip is put onto the PRIMARY and CLIPBOARD X + selections. + +[dmenu]: http://tools.suckless.org/dmenu/ +[rofi]: https://github.com/DaveDavenport/Rofi diff --git a/init/clipmenud.service.in b/init/clipmenud.service.in new file mode 100644 index 0000000..f68267b --- /dev/null +++ b/init/clipmenud.service.in @@ -0,0 +1,21 @@ +[Unit] +Description=Clipmenu daemon + +[Service] +ExecStart=@bindir@/clipmenud +Restart=always +RestartSec=500ms + +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +ProtectControlGroups=yes +ProtectKernelTunables=yes +RestrictAddressFamilies= +RestrictRealtime=yes + +# We don't need to do any clean up, so if something hangs (borked X server, +# etc), it's going to stay that way. Just forcefully kill and get it over with. +TimeoutStopSec=2 + +[Install] +WantedBy=default.target diff --git a/man/clipctl.1 b/man/clipctl.1 new file mode 100644 index 0000000..1b95e05 --- /dev/null +++ b/man/clipctl.1 @@ -0,0 +1,56 @@ +.TH CLIPCTL 1 +.SH NAME +clipctl \- control the clipmenud daemon +.SH SYNOPSIS +.B clipctl +ACTION +.SH DESCRIPTION +.B clipctl +communicates with the clipmenud daemon to control clipboard collection. It +sends a signal to clipmenud so that clipboard monitoring is enabled, disabled, +or toggled, or it simply displays the current status. +.SH OPTIONS +.TP +.B enable +Instruct clipmenud to enable clipboard collection. +.TP +.B disable +Instruct clipmenud to disable clipboard collection. +.TP +.B toggle +Invert the current state of clipboard collection. +.TP +.B status +Print the current state of clipboard collection. +.TP +.B version +Print the major clipmenu version. +.TP +.B cache-dir +Print the cache directory used by clipmenud. + +.TP +clipctl also accepts one dash option: + +.B \-h, \--help +Display the help message (invokes the manual page). +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipctl requires a running instance of the +.BR clipmenud +daemon. +.SH SEE ALSO +.BR clipdel (1), +.BR clipmenu (1), +.BR clipmenud (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipdel.1 b/man/clipdel.1 new file mode 100644 index 0000000..1d93acd --- /dev/null +++ b/man/clipdel.1 @@ -0,0 +1,53 @@ +.TH CLIPDEL 1 +.SH NAME +clipdel \- delete entries from the clip store +.SH SYNOPSIS +.B clipdel +[OPTION...] PATTERN +.SH DESCRIPTION +.B clipdel +removes clipboard entries from the clip store managed by clipmenu. By default, +it performs a dry-run and prints any matching entries. With the -d flag, +matching entries are permanently removed. +.SH OPTIONS +.TP +.B \-d +Real deletion mode. Matching clipboard entries will be removed. +.TP +.B \-F +Perform a literal (fixed-string) match instead of interpreting the pattern as a regular expression. +.TP +.B \-N +Interpret PATTERN as the amount of entries to delete starting from the oldest. +E.g +.I "\-N 1" +deletes the oldest entry. +.TP +.B \-n +Same as +.B \-N +but starting from newest. +.TP +.B \-v +Invert the matching condition; entries that do not match the given pattern are selected for deletion. +.TP +.B \-h, \--help +Display the help message (invokes the manual page). +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipdel requires access to the clip store directory as defined in the configuration. +.SH SEE ALSO +.BR clipctl (1), +.BR clipmenu (1), +.BR clipmenud (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipdelmenu.1 b/man/clipdelmenu.1 new file mode 100644 index 0000000..72f76ee --- /dev/null +++ b/man/clipdelmenu.1 @@ -0,0 +1,43 @@ +.TH CLIPDELMENU 1 +.SH NAME +clipdelmenu \- interactive launcher for deleting clipboard entries +.SH SYNOPSIS +.B clipdelmenu +[OPTION...] +.SH DESCRIPTION +.B clipdelmenu +provides an interactive interface for browsing and deleting clipboard entries +stored by clipmenud. It launches a menu using a configured launcher +(e.g., dmenu, rofi, or another custom command) and displays a numbered list of +stored clips. Once a selection is made, the chosen clip(s) are permanently +deleted from the clip store. Multiple clips can be selected at once using the +launcher's multi-select feature (e.g., Ctrl-Enter in dmenu). +.SH OPTIONS +.TP +.B \-h, \--help +Display the help message (invokes the manual page). + +Other than that, command-line arguments are passed directly to the underlying +launcher. +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipdelmenu requires a launcher capable of a dmenu-like interface (e.g., rofi or a +custom command). Population of the clipstore requires +.BR clipmenud +to be running. +.SH SEE ALSO +.BR clipctl (1), +.BR clipdel (1), +.BR clipmenu (1), +.BR clipmenud (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipmenu.1 b/man/clipmenu.1 new file mode 100644 index 0000000..00cfd00 --- /dev/null +++ b/man/clipmenu.1 @@ -0,0 +1,42 @@ +.TH CLIPMENU 1 +.SH NAME +clipmenu \- interactive launcher for clipboard entry selection +.SH SYNOPSIS +.B clipmenu +[OPTION...] +.SH DESCRIPTION +.B clipmenu +provides an interactive interface for browsing and selecting clipboard entries +stored by clipmenud. It launches a menu using a configured launcher +(e.g., dmenu, rofi, or another custom command) and displays a numbered list of +stored clips. Once a selection is made, clipmenu calls +.BR clipserve +to set the chosen clip as the current X11 selections (PRIMARY and CLIPBOARD). +.SH OPTIONS +.TP +.B \-h, \--help +Display the help message (invokes the manual page). + +Other than that, command-line arguments are passed directly to the underlying +launcher. +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipmenu requires a launcher capable of a dmenu-like interface (e.g., rofi or a +custom command). Population of the clipstore requires +.BR clipmenud +to be running. +.SH SEE ALSO +.BR clipctl (1), +.BR clipdel (1), +.BR clipmenud (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipmenu.conf.5 b/man/clipmenu.conf.5 new file mode 100644 index 0000000..2c93bed --- /dev/null +++ b/man/clipmenu.conf.5 @@ -0,0 +1,124 @@ +.TH CLIPMENU.CONF 5 +.SH NAME +clipmenu.conf \- configuration file for clipmenu applications +.SH DESCRIPTION +clipmenu.conf defines the runtime settings for clipmenu applications (including +clipmenu itself, clipmenud, clipctl, clipdel, clipdelmenu, and clipserve). +.SH FORMAT +Each non-comment line in clipmenu.conf consists of a key followed by one or +more whitespace-separated values. Blank lines and lines starting with '#' are +ignored. +.PP +For example: +.EX +max_clips 1000 +selections clipboard primary +.EE +.SH CONFIGURATION OPTIONS +.TP +.B max_clips +Specifies the maximum number of clipboard entries to retain in the clip store. +Default: 1000. +.TP +.B max_clips_batch +Provides a buffer above max_clips; when the number of entries exceeds +(max_clips + max_clips_batch), the clip store is trimmed back to max_clips +entries. Default: 100. +.TP +.B oneshot +If set to 1, clipmenud processes clipboard selections only once before exiting. +Default: 0. +.TP +.B deduplicate +If set to 1, clipmenud keeps only the newest copy of duplicate clips instead of +storing every duplicate entry. Default: 0. +.TP +.B own_clipboard +Determines whether clipmenud should claim ownership of the X11 clipboard. Works +together with own_selections. Default: 0. +.TP +.B own_selections +Specifies which X11 selections (e.g., "clipboard" or "primary") clipmenud +should actively own when +.B own_clipboard +is enabled. When a clip from one of these selections is stored, clipmenud uses +clipserve to own those selections. This does not affect which selections are +monitored; use +.B selections +for that. Default: "clipboard". +If this is set to an empty value, it is treated as if +.B own_clipboard +was disabled. +.TP +.B selections +Lists the X11 selections to monitor for changes. Valid values include +"clipboard", "primary", and "secondary". Default: "clipboard primary". +.TP +.B ignore_window +Defines a regular expression matching window titles to exclude from clipboard +monitoring. Unset by default. +.TP +.B launcher +Specifies the launcher command to use with clipmenu. Alternative choices +include rofi's dmenu mode or a custom command. Default: "dmenu". +.TP +.B launcher_pass_dmenu_args +When enabled, clipmenu and clipdelmenu add dmenu-compatible defaults (such as +-p and -l) before any user-provided arguments. Default: 1. +.TP +.B touch_on_select +When an entry is selected via +.BR clipmenu (1) +it will also be moved up to the newest slot. +Default: 0. +.TP +.B partial_merge_secs +Specifies the time window in seconds during which consecutive clipboard updates +where the previous entry was a substring of the new one are merged together +instead of being stored separately. This is useful for applications that +incrementally update the clipboard (e.g. progressive text selection in +browsers). Set to 0 to disable partial merging entirely. Default: 2. +.TP +.B cm_dir +Overrides the default base directory for clipmenu runtime data. The clip store +is created at a subdirectory named clipmenu.. inside this +directory. By default, it uses XDG_RUNTIME_DIR, TMPDIR, or /tmp if neither are +set. The path must be absolute. A leading ~ expands to $HOME. +.SH FILE LOCATION +Typically, clipmenu.conf is located at +.BR ~/.config/clipmenu/clipmenu.conf +. +The file location can be overridden with $CM_CONFIG, which must be an absolute +path. A leading ~ expands to $HOME. If $CM_CONFIG is unset, $XDG_CONFIG_HOME is +used (falling back to ~/.config). +.SH ENVIRONMENT +.TP +.B CM_CONFIG +Absolute path (or a path beginning with ~) to the configuration file. This +takes precedence over $XDG_CONFIG_HOME. +.TP +.B XDG_CONFIG_HOME +Base directory for configuration files. If unset, ~/.config is used. +.TP +.B CM_DEBUG +If set to 1, enable debug logging. +.TP +.B Config environment variables +CM_MAX_CLIPS, CM_MAX_CLIPS_BATCH, CM_ONESHOT, CM_DEDUPLICATE, CM_OWN_CLIPBOARD, +CM_SELECTIONS, CM_OWN_SELECTIONS, CM_IGNORE_WINDOW, CM_LAUNCHER, +CM_LAUNCHER_PASS_DMENU_ARGS, and CM_DIR are environment variable equivalents for +the configuration keys of the same name. These override values from the config +file and are provided for compatibility with versions prior to 7. +.SH SEE ALSO +.BR clipctl (1), +.BR clipdel (1), +.BR clipmenu (1), +.BR clipmenud (1), +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipmenud.1 b/man/clipmenud.1 new file mode 100644 index 0000000..65f367a --- /dev/null +++ b/man/clipmenud.1 @@ -0,0 +1,32 @@ +.TH CLIPMENUD 1 +.SH NAME +clipmenud \- daemon for clipboard monitoring and storage +.SH DESCRIPTION +.B clipmenud +runs in the background, monitoring X11 clipboard selections (including PRIMARY, +CLIPBOARD, and SECONDARY). It stores new clipboard entries into a persistent +clip store. clipmenud responds to signals sent by +.BR clipctl +to enable or disable clipboard collection. +.SH OPTIONS +.TP +.B \-h, \--help +Display the help message (invokes the manual page). +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipmenud requires an X11 environment with the XFixes extension and access to the clip store directory as defined in the configuration. +.SH SEE ALSO +.BR clipctl (1), +.BR clipdel (1), +.BR clipmenu (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/man/clipserve.1 b/man/clipserve.1 new file mode 100644 index 0000000..72f6d9b --- /dev/null +++ b/man/clipserve.1 @@ -0,0 +1,44 @@ +.TH CLIPSERVE 1 +.SH NAME +clipserve \- serve a selected clipboard entry to X11 selections +.SH SYNOPSIS +.B clipserve +[\-s selection]... +.SH DESCRIPTION +.B clipserve +serves the clipboard content identified by a hash from the clip store on the +configured X11 selections. By default, it serves both PRIMARY and CLIPBOARD. + +This program is not usually invoked directly, but is instead called from inside +other clipmenu applications. +.SH OPTIONS +.TP +.B \-h, \--help +Display the help message (invokes the manual page). +.TP +.B \-s, \--selection +Specify a selection to serve. Valid values are "primary", "clipboard", and +"secondary". This option may be repeated. If not provided, PRIMARY and +CLIPBOARD are used. +.TP +clipserve requires a hexadecimal hash of the clipboard entry to be served. +.SH CONFIGURATION +See +.BR clipmenu.conf (5). +.SH DEPENDENCIES +clipserve needs a valid clip store and an operational X11 environment to +function correctly. +.SH SEE ALSO +.BR clipctl (1), +.BR clipdel (1), +.BR clipmenu (1), +.BR clipmenud (1), +.BR clipmenu.conf (5) +.SH AUTHOR +Chris Down +.MT chris@chrisdown.name +.ME +.SH REPORTING BUGS +Please send bug reports to +.UR https://github.com/cdown/clipmenu/issues +.UE . diff --git a/src/clipctl.c b/src/clipctl.c new file mode 100644 index 0000000..7f5c961 --- /dev/null +++ b/src/clipctl.c @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "util.h" + +/** + * Check if the clipmenud service is enabled. + */ +static bool is_enabled(struct config *cfg) { + _drop_(fclose) FILE *file = fopen(get_enabled_path(cfg), "r"); + die_on(!file, "Failed to open enabled file: %s\n", strerror(errno)); + return fgetc(file) == '1'; +} + +/** + * Retrieve the process ID of the clipmenud daemon. + * + * -EEXIST is returned if multiple instances are detected. -ENOENT is returned + * if no instances are found. + */ +static pid_t get_clipmenud_pid(struct config *cfg) { + int fd = open(get_session_lock_path(cfg), O_RDWR | O_CLOEXEC); + if (fd < 0) { + return -ENOENT; + } + + if (flock(fd, LOCK_EX | LOCK_NB) == 0) { + close(fd); + return -ENOENT; + } + bool would_block; +#if EWOULDBLOCK == EAGAIN + would_block = errno == EWOULDBLOCK; +#else + would_block = errno == EWOULDBLOCK || errno == EAGAIN; +#endif + if (!would_block) { + close(fd); + return -ENOENT; + } + + char buf[32]; + ssize_t read_sz = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (read_sz <= 0) { + return -ENOENT; + } + buf[read_sz] = '\0'; + char *newline = strchr(buf, '\n'); + if (newline) { + *newline = '\0'; + } + + uint64_t pid; + if (str_to_uint64(buf, &pid) < 0) { + return -ENOENT; + } + + if (kill((pid_t)pid, 0) < 0 && errno == ESRCH) { + return -ENOENT; + } + + return (pid_t)pid; +} + +/** + * Determine if clipmenud should be enabled based on the given mode string and + * clipmenu state. + */ +static bool _nonnull_ should_enable(struct config *cfg, const char *mode_str) { + if (streq(mode_str, "enable")) { + return true; + } else if (streq(mode_str, "disable")) { + return false; + } else if (streq(mode_str, "toggle")) { + return !is_enabled(cfg); + } + + die("Unknown command: %s\n", mode_str); +} + +int main(int argc, char *argv[]) { + _drop_(config_free) struct config cfg = setup("clipctl"); + exec_man_on_help(argc, argv); + die_on(argc != 2, + "Usage: clipctl \n"); + + const char *cmd = argv[1]; + + if (streq(cmd, "cache-dir")) { + printf("%s\n", get_cache_dir(&cfg)); + return 0; + } + if (streq(cmd, "version")) { + printf("%d\n", CLIPMENU_VERSION); + return 0; + } + + pid_t pid = get_clipmenud_pid(&cfg); + die_on(pid == -ENOENT, "clipmenud is not running\n"); + expect(pid > 0); + + if (streq(cmd, "status")) { + printf("%s\n", is_enabled(&cfg) ? "enabled" : "disabled"); + return 0; + } + + bool want_enable = should_enable(&cfg, cmd); + + expect(kill(pid, want_enable ? SIGUSR2 : SIGUSR1) == 0); + dbg("Sent signal to pid %d\n", pid); + + unsigned int delay_ms = 1; + unsigned int total_wait_ms = 0; + const unsigned int max_wait_ms = 1000; + + while (total_wait_ms < max_wait_ms) { + if (is_enabled(&cfg) == want_enable) { + return 0; + } + + struct timespec req = {.tv_sec = delay_ms / 1000, + .tv_nsec = (delay_ms % 1000) * 1000000L}; + struct timespec rem; + + while (nanosleep(&req, &rem) != 0) { + expect(errno == EINTR); + req = rem; + } + + total_wait_ms += delay_ms; + delay_ms *= 2; + } + + if (is_enabled(&cfg) == want_enable) { + return 0; + } + + die("Failed to %s clipmenud within %u ms\n", + want_enable ? "enable" : "disable", max_wait_ms); +} diff --git a/src/clipdel.c b/src/clipdel.c new file mode 100644 index 0000000..30fd0b7 --- /dev/null +++ b/src/clipdel.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "store.h" +#include "util.h" + +/** + * The deletion mode for clipdel operations. + */ +enum delete_mode { + DELETE_DRY_RUN, + DELETE_REAL, +}; + +/** + * The match type for clipdel operations. + */ +enum match_type { + MATCH_REGEX, + MATCH_LITERAL, + MATCH_COUNT_NEWEST, + MATCH_COUNT_OLDEST, +}; + +/** + * Holds the application state for a clipdel operation in preparation for + * passing it as private data to the cs_remove callback. + */ +struct clipdel_state { + enum delete_mode mode; + enum match_type type; + bool invert_match; + union { + regex_t rgx; + const char *needle; + struct { + uint64_t current; + uint64_t num_delete; + } count; + }; +}; + +/** + * Callback for cs_remove. In order for the delete to actually happen, we must + * be running DELETE_REAL. + */ +static enum cs_remove_action _nonnull_ remove_if_match(uint64_t hash _unused_, + const char *line, + void *private) { + struct clipdel_state *state = private; + int ret; + bool matches; + switch (state->type) { + case MATCH_LITERAL: + matches = strstr(line, state->needle) != NULL; + break; + case MATCH_REGEX: + ret = regexec(&state->rgx, line, 0, NULL, 0); + expect(ret == 0 || ret == REG_NOMATCH); + matches = ret == 0; + break; + case MATCH_COUNT_NEWEST: + case MATCH_COUNT_OLDEST: + if (state->count.current < state->count.num_delete) { + matches = true; + ++state->count.current; + } else { + matches = false; + } + break; + default: + die("unreachable\n"); + } + + bool wants_del = state->invert_match ? !matches : matches; + if (wants_del) { + puts(line); + } + + return state->mode == DELETE_REAL && wants_del ? CS_ACTION_REMOVE + : CS_ACTION_KEEP; +} + +int main(int argc, char *argv[]) { + const char usage[] = "Usage: clipdel [-d] [-F] [-N] [-n] [-v] pattern"; + + _drop_(config_free) struct config cfg = setup("clipdel"); + + struct clipdel_state state = {0}; + + int opt; + while ((opt = getopt(argc, argv, "dFNnvh")) != -1) { + switch (opt) { + case 'd': + state.mode = DELETE_REAL; + break; + case 'F': + state.type = MATCH_LITERAL; + break; + case 'N': + state.type = MATCH_COUNT_OLDEST; + break; + case 'n': + state.type = MATCH_COUNT_NEWEST; + break; + case 'v': + state.invert_match = true; + break; + case 'h': + exec_man(); + break; + default: + die("%s\n", usage); + } + } + + die_on(optind >= argc, "%s\n", usage); + + _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + _drop_(cs_destroy) struct clip_store cs; + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + enum cs_iter_direction direction = CS_ITER_OLDEST_FIRST; + switch (state.type) { + case MATCH_REGEX: + die_on(regcomp(&state.rgx, argv[optind], REG_EXTENDED | REG_NOSUB), + "Could not compile regex\n"); + break; + case MATCH_LITERAL: + state.needle = argv[optind]; + break; + case MATCH_COUNT_NEWEST: + case MATCH_COUNT_OLDEST: + die_on(str_to_uint64(argv[optind], &state.count.num_delete) < 0, + "Bad argument, expected integer: %s\n", argv[optind]); + state.count.current = 0; + if (state.type == MATCH_COUNT_NEWEST) { + direction = CS_ITER_NEWEST_FIRST; + } + break; + default: + die("unreachable\n"); + } + + expect(cs_remove(&cs, direction, remove_if_match, &state) == 0); + + if (state.type == MATCH_REGEX) { + regfree(&state.rgx); + } + + return 0; +} diff --git a/src/clipdelmenu.c b/src/clipdelmenu.c new file mode 100644 index 0000000..255c7bf --- /dev/null +++ b/src/clipdelmenu.c @@ -0,0 +1,45 @@ +#include +#include +#include + +#include "config.h" +#include "menu_util.h" +#include "store.h" +#include "util.h" + +struct delete_state { + uint64_t hash_to_delete; +}; + +// cppcheck-suppress-begin constParameterCallback +static enum cs_remove_action _nonnull_ +remove_if_hash_match(uint64_t hash, const char *line _unused_, void *private) { + const struct delete_state *state = private; + return hash == state->hash_to_delete ? CS_ACTION_REMOVE : CS_ACTION_KEEP; +} +// cppcheck-suppress-end constParameterCallback + +static int _nonnull_ clipdelmenu_action(struct config *cfg, uint64_t hash) { + _drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + _drop_(cs_destroy) struct clip_store cs; + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + struct delete_state del_state = {.hash_to_delete = hash}; + expect(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_hash_match, + &del_state) == 0); + + return 0; +} + +int main(int argc, char *argv[]) { + menu_set_user_args(argc, argv); + + _drop_(config_free) struct config cfg = setup("clipdelmenu"); + exec_man_on_help(argc, argv); + + return menu_prompt_and_act(&cfg, "clipdelmenu", clipdelmenu_action); +} diff --git a/src/clipmenu.c b/src/clipmenu.c new file mode 100644 index 0000000..a7e1982 --- /dev/null +++ b/src/clipmenu.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "menu_util.h" +#include "store.h" +#include "util.h" +#include "x.h" + +static int _nonnull_ clipmenu_action(struct config *cfg, uint64_t hash) { + if (cfg->touch_on_select) { + _drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + _drop_(cs_destroy) struct clip_store cs; + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + int ret = cs_make_newest(&cs, hash); + if (ret == -ENOENT) { + fprintf(stderr, "Selected clip no longer exists\n"); + return EXIT_FAILURE; + } + expect(ret == 0); + } + + run_clipserve(hash, NULL); + return 0; +} + +int main(int argc, char *argv[]) { + menu_set_user_args(argc, argv); + + _drop_(config_free) struct config cfg = setup("clipmenu"); + exec_man_on_help(argc, argv); + + return menu_prompt_and_act(&cfg, "clipmenu", clipmenu_action); +} diff --git a/src/clipmenud.c b/src/clipmenud.c new file mode 100644 index 0000000..446a2d7 --- /dev/null +++ b/src/clipmenud.c @@ -0,0 +1,765 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "store.h" +#include "util.h" +#include "x.h" + +static Display *dpy; +static struct clip_store cs; +static struct config cfg; +static Window win; + +static int enabled = 1; +static int sig_pipe[2]; + +static Atom timestamp_atom; +static Atom incr_atom; +static struct incr_transfer *it_list; + +static struct cm_selections sels[CM_SEL_MAX]; + +static Time last_disable_time = 0; +static Time last_enable_time = 0; + +static bool has_owned_selections(void) { + for (size_t i = 0; i < CM_SEL_MAX; i++) { + if (cfg.owned_selections[i].active) { + return true; + } + } + return false; +} + +enum clip_text_source { + CLIP_TEXT_SOURCE_X, + CLIP_TEXT_SOURCE_MALLOC, + CLIP_TEXT_SOURCE_INVALID +}; + +struct clip_text { + char *data; + enum clip_text_source source; +}; + +static struct clip_text last_text[CM_SEL_MAX] = { + {NULL, CLIP_TEXT_SOURCE_MALLOC}, + {NULL, CLIP_TEXT_SOURCE_MALLOC}, + {NULL, CLIP_TEXT_SOURCE_MALLOC}, +}; +static struct timespec last_text_time[CM_SEL_MAX]; + +static void free_clip_text(struct clip_text *ct) { + expect(ct->source != CLIP_TEXT_SOURCE_INVALID); + + if (ct->data) { + if (ct->source == CLIP_TEXT_SOURCE_X) { + XFree(ct->data); + } else { + free(ct->data); + } + ct->data = NULL; + } + + ct->source = CLIP_TEXT_SOURCE_INVALID; +} + +static struct timespec get_monotonic_time(void) { + struct timespec ts; + expect(clock_gettime(CLOCK_MONOTONIC, &ts) == 0); + return ts; +} + +static bool within_partial_merge_window(struct timespec current_time, + struct timespec last_time) { + time_t elapsed_secs = current_time.tv_sec - last_time.tv_sec; + + if (elapsed_secs < cfg.partial_merge_secs) { + return true; + } + if (elapsed_secs > cfg.partial_merge_secs) { + return false; + } + return current_time.tv_nsec <= last_time.tv_nsec; +} + +// cppcheck-suppress [constParameterCallback,unmatchedSuppression] +static Bool is_timestamp_event(Display *display _unused_, XEvent *event, + XPointer arg) { + const void *argp = arg; + return event->type == PropertyNotify && + event->xproperty.atom == *(const Atom *)argp; +} + +/** + * Get the current X server time by triggering a PropertyNotify. + */ +static Time get_current_server_time(void) { + XEvent ev; + XChangeProperty(dpy, win, timestamp_atom, XA_INTEGER, 32, PropModeReplace, + NULL, 0); + XIfEvent(dpy, &ev, is_timestamp_event, (XPointer)×tamp_atom); + return ev.xproperty.time; +} + +/** + * Check if a text s1 is a possible partial of s2. + * + * Chromium and some other badly behaved applications spam PRIMARY during + * selection, so if you're selecting the text "abc", you get three clips: "a", + * "ab", and "abc" (or "c", "bc", "abc" if selecting right to left). Attempt to + * detect these. It's possible we were not fast enough to get all of them, so + * unfortunately we can't check for strlen(s1)+1 either. It's also possible the + * user first expands, and then retracts the selection, so we need to handle + * that too. + */ +static bool is_possible_partial(const char *s1, const char *s2) { + size_t len1 = strlen(s1), len2 = strlen(s2); + + // Is one a prefix of the other? + if (strncmp(s1, s2, len1 < len2 ? len1 : len2) == 0) { + return true; + } + + // Is one a suffix of the other? + if (len1 < len2) { + return strcmp(s1, s2 + len2 - len1) == 0; + } else { + return strcmp(s2, s1 + len1 - len2) == 0; + } +} + +/** + * Retrieve the converted text put into our clip atom. In order for this to + * happen a conversion must have been performed in an earlier iteration with + * XConvertSelection. + */ +static struct clip_text get_clipboard_text(Atom clip_atom) { + struct clip_text ct = {NULL, CLIP_TEXT_SOURCE_X}; + unsigned char *cur_text; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + + int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False, + AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &cur_text); + if (res != Success) { + return ct; + } + + if (actual_type == incr_atom) { + dbg("Unexpected INCR transfer detected\n"); + XFree(cur_text); + return ct; + } + + ct.data = (char *)cur_text; + + return ct; +} + +/** + * Return true if the given string contains any non-whitespace characters. + */ +static bool is_salient_text(const char *str) { + if (!str) { + return false; + } + + for (; *str; str++) { + if (!isspace((unsigned char)*str)) { + return true; + } + } + return false; +} + +/** + * Write the current enabled status to a designated status file. + */ +static void write_status(void) { + _drop_(close) int fd = + open(get_enabled_path(&cfg), O_WRONLY | O_CREAT, 0600); + die_on(fd < 0, "Failed to update status: %s\n", strerror(errno)); + dprintf(fd, "%d", (int)enabled); +} + +/** + * Return true if the given window title matches the title of the clipserve + * window. + */ +static bool is_clipserve(const char *win_title) { + return win_title && streq(win_title, "clipserve"); +} + +/** + * Determine if a window with the given title should be ignored based on user + * configuration. + */ +static bool is_ignored_window(char *win_title) { + if (!win_title || !cfg.ignore_window.set) { + return 0; + } + int ret = regexec(&cfg.ignore_window.rgx, win_title, 0, NULL, 0); + expect(ret == 0 || ret == REG_NOMATCH); + return !ret; +} + +/** + * Disable or enable clip collection based on received signals. + */ +static void handle_signal_event(int signo) { + dbg("Got signal %d\n", signo); + switch (signo) { + case SIGUSR1: + // If we're already disabled, we need to keep the original + // timestamp to properly filter all events that were queued during + // any part of the disabled period. + if (enabled) { + last_disable_time = get_current_server_time(); + } + enabled = 0; + dbg("Clipboard collection disabled by signal at time %lu\n", + (unsigned long)last_disable_time); + break; + case SIGUSR2: + // If we're already enabled, we need to keep the original timestamp + // so we don't mistakenly filter out valid messages. + if (!enabled) { + last_enable_time = get_current_server_time(); + } + enabled = 1; + dbg("Clipboard collection enabled by signal at time %lu\n", + (unsigned long)last_enable_time); + break; + } + write_status(); +} + +static void handle_signal_pipe(void) { + unsigned char sigs[32]; + ssize_t read_sz; + while ((read_sz = read(sig_pipe[0], sigs, sizeof(sigs))) > 0) { + for (ssize_t i = 0; i < read_sz; i++) { + handle_signal_event((int)sigs[i]); + } + } + if (read_sz < 0) { + expect(errno == EAGAIN || errno == EINTR); + } +} + +static void signal_handler(int signo) { + int saved_errno = errno; + unsigned char sig = (unsigned char)signo; + ssize_t written = write(sig_pipe[1], &sig, sizeof(sig)); + (void)written; + errno = saved_errno; +} + +static void set_fd_flags(int fd) { + int flags = fcntl(fd, F_GETFL); + expect(flags >= 0); + expect(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0); + + int fd_flags = fcntl(fd, F_GETFD); + expect(fd_flags >= 0); + expect(fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == 0); +} + +/** + * Something changed about the watched selection, consider converting it to our + * desired property type. + */ +static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) { + if (last_disable_time > 0 && se->timestamp >= last_disable_time && + se->timestamp < last_enable_time) { + dbg("Ignoring selection event from disabled period (event time: %lu, disabled: %lu, enabled: %lu)\n", + (unsigned long)se->timestamp, (unsigned long)last_disable_time, + (unsigned long)last_enable_time); + return; + } + + enum selection_type sel = + selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received XFixesSelectionNotify for unknown sel\n"); + return; + } + + _drop_(XFree) char *win_title = get_window_title(dpy, se->owner); + if (is_clipserve(win_title) || is_ignored_window(win_title)) { + dbg("Ignoring clip from window titled '%s'\n", win_title); + return; + } + + dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n", + cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner); + + struct incr_transfer *it = it_list; + while (it) { + struct incr_transfer *next = it->next; + if (it->property == sels[sel].storage) { + it_dbg(it, "Cleaning up stale INCR transfer\n"); + free(it->data); + it_remove(&it_list, it); + free(it); + } + it = next; + } + + XConvertSelection(dpy, se->selection, + XInternAtom(dpy, "UTF8_STRING", False), sels[sel].storage, + win, CurrentTime); + + return; +} + +/** + * Something changed about the watched selection, but we don't explicitly + * listen for SelectionNotify, so in reality this only happens in response to + * an explicit request to tell us that there is no owner. In that case, return + * -ENOENT. + */ +static int handle_selection_notify(const XSelectionEvent *se) { + if (se->property == None) { + enum selection_type sel = + selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received no owner notification for unknown sel\n"); + return 0; + } + dbg("X reports that %s has no current owner\n", + cfg.selections[sel].name); + return -ENOENT; + } + return 0; +} + +/** + * Trims the clip store if the number of clips exceeds the configured batch + * size. + */ +static void maybe_trim(void) { + size_t cur_clips; + expect(cs_len(&cs, &cur_clips) == 0); + if (cur_clips > (size_t)cfg.max_clips + (size_t)cfg.max_clips_batch) { + expect(cs_trim(&cs, CS_ITER_NEWEST_FIRST, (size_t)cfg.max_clips) == 0); + } +} + +/** + * Store the clipboard text for a selection. If the text is a possible partial + * of the last clip on the same selection and it was received shortly + * afterwards, replace instead of adding. + */ +static uint64_t store_clip(enum selection_type sel, struct clip_text *ct) { + dbg("Clipboard text is considered salient, storing\n"); + struct timespec current_time = get_monotonic_time(); + uint64_t hash; + + if (cfg.partial_merge_secs > 0 && last_text[sel].data && + within_partial_merge_window(current_time, last_text_time[sel]) && + is_possible_partial(last_text[sel].data, ct->data)) { + dbg("Possible partial of last clip on %s, replacing\n", + cfg.selections[sel].name); + expect(cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, ct->data, &hash) == 0); + } else { + expect(cs_add(&cs, ct->data, &hash, + cfg.deduplicate ? CS_DUPE_KEEP_LAST : CS_DUPE_KEEP_ALL) == + 0); + } + + free_clip_text(&last_text[sel]); + last_text[sel] = *ct; + last_text_time[sel] = current_time; + + // The caller no longer owns this data. + ct->data = NULL; + ct->source = CLIP_TEXT_SOURCE_INVALID; + + return hash; +} + +/** + * Process the final data collected during an INCR transfer. + */ +static void incr_receive_finish(struct incr_transfer *it) { + enum selection_type sel = + storage_atom_to_selection_type(it->property, sels); + if (sel == CM_SEL_INVALID) { + it_dbg(it, "Received INCR finish for unknown sel\n"); + return; + } + + it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size); + char *text = malloc(it->data_size + 1); + expect(text); + memcpy(text, it->data, it->data_size); + text[it->data_size] = '\0'; + + struct clip_text ct = {text, CLIP_TEXT_SOURCE_MALLOC}; + + char line[CS_SNIP_LINE_SIZE]; + first_line(ct.data, line); + it_dbg(it, "First line: %s\n", line); + + if (is_salient_text(ct.data)) { + uint64_t hash = store_clip(sel, &ct); + maybe_trim(); + if (cfg.own_clipboard && has_owned_selections()) { + run_clipserve(hash, cfg.owned_selections); + } + } else { + it_dbg(it, "Clipboard text is whitespace only, ignoring\n"); + free_clip_text(&ct); + } + + free(it->data); + it_remove(&it_list, it); + free(it); +} + +#define INCR_DATA_START_BYTES 1024 * 1024 + +/** + * Acknowledge and start an INCR transfer. + */ +static void incr_receive_start(const XPropertyEvent *pe) { + struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); + expect(it); + *it = (struct incr_transfer){ + .property = pe->atom, + .requestor = pe->window, + .data_size = 0, + .data_capacity = INCR_DATA_START_BYTES, + .data = malloc(INCR_DATA_START_BYTES), + }; + expect(it->data); + + it_dbg(it, "Starting transfer\n"); + it_add(&it_list, it); + + // Signal readiness for chunks + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Continue receiving data during an INCR transfer. + */ +static void incr_receive_data(const XPropertyEvent *pe, + struct incr_transfer *it) { + if (pe->state != PropertyNewValue) { + return; + } + + it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size); + + _drop_(XFree) unsigned char *chunk = NULL; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &chunk); + + size_t chunk_size = nitems * (actual_format / 8); + + if (chunk_size == 0) { + it_dbg(it, "Transfer complete\n"); + incr_receive_finish(it); + return; + } + + if (it->data_size + chunk_size > it->data_capacity) { + it->data_capacity = (it->data_size + chunk_size) * 2; + it->data = realloc(it->data, it->data_capacity); + expect(it->data); + it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity); + } + + memcpy(it->data + it->data_size, chunk, chunk_size); + it->data_size += chunk_size; + + // Signal readiness for next chunk + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Something changed in our clip storage atoms. Work out whether we want to + * store the new content as a clipboard entry. + */ +static int handle_property_notify(const XPropertyEvent *pe) { + if (pe->state != PropertyNewValue && pe->state != PropertyDelete) { + return -EINVAL; + } + + enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received PropertyNotify for unknown sel\n"); + return -EINVAL; + } + + // Check if this property corresponds to an INCR transfer in progress + struct incr_transfer *it = it_list; + while (it) { + if (it->property == pe->atom && it->requestor == pe->window) { + break; + } + it = it->next; + } + + if (it) { + incr_receive_data(pe, it); + return 0; + } + + // Not an INCR transfer in progress. Check if this is an INCR transfer + // starting + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + _drop_(XFree) unsigned char *prop = NULL; + + XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &prop); + + if (actual_type == incr_atom) { + incr_receive_start(pe); + } else { + dbg("Received non-INCR PropertyNotify\n"); + + // store_clip will take care of freeing this later when it's gone from + // last_text. + struct clip_text ct = get_clipboard_text(pe->atom); + if (!ct.data) { + dbg("Failed to get clipboard text\n"); + return -EINVAL; + } + char line[CS_SNIP_LINE_SIZE]; + first_line(ct.data, line); + dbg("First line: %s\n", line); + + if (is_salient_text(ct.data)) { + uint64_t hash = store_clip(sel, &ct); + maybe_trim(); + /* We only own CLIPBOARD because otherwise the behaviour is wonky: + * + * 1. When you select in a browser and press ^V, it repastes what + * you have selected instead of the previous content + * 2. urxvt and some other terminal emulators will unhilight on + * PRIMARY ownership being taken away from them + */ + if (cfg.own_clipboard && has_owned_selections()) { + run_clipserve(hash, cfg.owned_selections); + } + } else { + dbg("Clipboard text is whitespace only, ignoring\n"); + free_clip_text(&ct); + } + } + + return 0; +} + +/** + * Process X11 events, returning when we have either processed one clip, or + * have received an indication that the selection is not owned. + * + * The usual sequence is: + * + * 1. Get an XFixesSelectionNotify that we have a new selection. + * 2. Call XConvertSelection() on it to get a string in our prop. + * 3. Wait for a PropertyNotify that says that's ready. + * 4. When it's ready, store it, and return from the function. + * + * Another possible outcome, especially when trying to get the initial state at + * startup, is that we get a SelectionNotify even with owner == None, which + * means the selection is unowned. At that point we also return, since it's + * clear that an explicit request has been nacked. + */ +static int handle_x11_event(int evt_base) { + while (XPending(dpy)) { + XEvent evt; + XNextEvent(dpy, &evt); + + if (!enabled) { + dbg("Got X event, but ignoring as collection is disabled\n"); + continue; + } + + int ret; + if (evt.type == evt_base + XFixesSelectionNotify) { + handle_xfixes_selection_notify((XFixesSelectionNotifyEvent *)&evt); + } else if (evt.type == PropertyNotify) { + ret = handle_property_notify((XPropertyEvent *)&evt); + if (ret == 0) { + return ret; + } + } else if (evt.type == SelectionNotify) { + ret = handle_selection_notify((XSelectionEvent *)&evt); + if (ret < 0) { + return ret; + } + } + } + + return -EINPROGRESS; +} + +/** + * Continuously wait for and process X11 or signal events until we fully + * process success or failure for a clip. + * + * The usual sequence is: + * + * 1. Get an XFixesSelectionNotify that we have a new selection. + * 2. Call XConvertSelection() on it to get a string in our prop. + * 3. Wait for a PropertyNotify that says that's ready. + * 4. When it's ready, store it, and return from the function. + * + * Another possible outcome, especially when trying to get the initial state at + * startup, is that we get a SelectionNotify even with owner == None, which + * means the selection is unowned. At that point we also return, since it's + * clear that an explicit request has been nacked. + */ +static int get_one_clip(int evt_base) { + while (1) { + // It's possible that we have more X events to process, but because of + // the way the protocol works, we won't get told about them until we + // next get an event if we wait for select(). Check for them first. + if (XPending(dpy)) { + return handle_x11_event(evt_base); + } + + fd_set fds; + int x_fd = ConnectionNumber(dpy); + + FD_ZERO(&fds); + FD_SET(sig_pipe[0], &fds); + FD_SET(x_fd, &fds); + + int max_fd = sig_pipe[0] > x_fd ? sig_pipe[0] : x_fd; + int ret = select(max_fd + 1, &fds, NULL, NULL, NULL); + if (ret < 0) { + if (errno == EINTR) { + return 0; + } + expect(ret > 0); + } + + if (FD_ISSET(sig_pipe[0], &fds)) { + handle_signal_pipe(); + } + + if (FD_ISSET(x_fd, &fds)) { + return handle_x11_event(evt_base); + } + } +} + +static int setup_watches(int evt_base) { + XSelectInput(dpy, win, PropertyChangeMask); + + for (size_t i = 0; i < CM_SEL_MAX; i++) { + struct selection sel = cfg.selections[i]; + if (!sel.active) { + continue; + } + Atom sel_atom = sels[i].selection; + XFixesSelectSelectionInput(dpy, win, sel_atom, + XFixesSetSelectionOwnerNotifyMask); + dbg("Getting initial value for selection %s\n", sel.name); + XConvertSelection(dpy, sel_atom, XInternAtom(dpy, "UTF8_STRING", False), + sels[i].storage, win, CurrentTime); + get_one_clip(evt_base); + } + + return 0; +} + +static int _noreturn_ run(int evt_base) { + while (1) { + get_one_clip(evt_base); + } +} + +#ifndef UNIT_TEST +int main(int argc, char *argv[]) { + int evt_base; + + cfg = setup("clipmenud"); + exec_man_on_help(argc, argv); + die_on(argc != 1, "clipmenud doesn't accept any arguments\n"); + + _drop_(close) int session_fd = + open(get_session_lock_path(&cfg), O_RDWR | O_CREAT | O_CLOEXEC, 0600); + die_on(session_fd < 0, "Failed to open session file: %s\n", + strerror(errno)); + die_on(flock(session_fd, LOCK_EX | LOCK_NB) < 0, + "Failed to lock session file -- is another clipmenud running?\n"); + + expect(ftruncate(session_fd, 0) == 0); + expect(lseek(session_fd, 0, SEEK_SET) == 0); + dprintf(session_fd, "%ld\n", (long)getpid()); + + write_status(); + + _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + die_on(!(dpy = XOpenDisplay(NULL)), "Cannot open display\n"); + win = DefaultRootWindow(dpy); + setup_selections(dpy, sels); + + incr_atom = XInternAtom(dpy, "INCR", False); + timestamp_atom = XInternAtom(dpy, "CLIPMENUD_TIMESTAMP", False); + + expect(pipe(sig_pipe) == 0); + set_fd_flags(sig_pipe[0]); + set_fd_flags(sig_pipe[1]); + + struct sigaction sa = {0}; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + expect(sigaction(SIGUSR1, &sa, NULL) == 0); + expect(sigaction(SIGUSR2, &sa, NULL) == 0); + expect(signal(SIGCHLD, SIG_IGN) != SIG_ERR); + + int unused; + die_on(!XFixesQueryExtension(dpy, &evt_base, &unused), "XFixes missing\n"); + + setup_watches(evt_base); + + if (!cfg.oneshot) { + run(evt_base); + } + + expect(cs_destroy(&cs) == 0); + config_free(&cfg); + XCloseDisplay(dpy); + return 0; +} +#endif diff --git a/src/clipserve.c b/src/clipserve.c new file mode 100644 index 0000000..9d490cd --- /dev/null +++ b/src/clipserve.c @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "store.h" +#include "util.h" +#include "x.h" + +static struct incr_transfer *it_list = NULL; +static Display *dpy; +static Atom incr_atom; + +static size_t incr_threshold; +static size_t chunk_size; + +static enum selection_type selection_name_to_type(const char *name) { + if (streq(name, "clipboard")) { + return CM_SEL_CLIPBOARD; + } + if (streq(name, "primary")) { + return CM_SEL_PRIMARY; + } + if (streq(name, "secondary")) { + return CM_SEL_SECONDARY; + } + return CM_SEL_INVALID; +} + +/** + * Start an INCR transfer. + */ +static void incr_send_start(XSelectionRequestEvent *req, + struct cs_content *content) { + long incr_size = content->size; + XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32, + PropModeReplace, (unsigned char *)&incr_size, 1); + + struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); + expect(it); + *it = (struct incr_transfer){ + .requestor = req->requestor, + .property = req->property, + .target = req->target, + .format = 8, + .data = (char *)content->data, + .data_size = content->size, + .offset = 0, + }; + + it_dbg(it, "Starting transfer\n"); + it_add(&it_list, it); + + // Listen for PropertyNotify events on the requestor window + XSelectInput(dpy, it->requestor, PropertyChangeMask); +} + +/** + * Finish an INCR transfer. + */ +static void incr_send_finish(struct incr_transfer *it) { + XChangeProperty(dpy, it->requestor, it->property, it->target, it->format, + PropModeReplace, NULL, 0); + it_dbg(it, "Transfer complete\n"); + it_remove(&it_list, it); + free(it); +} + +/** + * Continue sending data during an INCR transfer. + */ +static void incr_send_chunk(const XPropertyEvent *pe) { + if (pe->state != PropertyDelete) { + return; + } + + struct incr_transfer *it = it_list; + while (it) { + if (it->requestor == pe->window && it->property == pe->atom) { + size_t remaining = it->data_size - it->offset; + size_t this_chunk_size = + (remaining > chunk_size) ? chunk_size : remaining; + + it_dbg(it, + "Sending chunk (bytes sent: %zu, bytes remaining: %zu)\n", + it->offset, remaining); + + if (this_chunk_size > 0) { + XChangeProperty(dpy, it->requestor, it->property, it->target, + it->format, PropModeReplace, + (unsigned char *)(it->data + it->offset), + this_chunk_size); + it->offset += this_chunk_size; + } else { + incr_send_finish(it); + } + break; + } + it = it->next; + } +} + +/** + * Serve clipboard content for all X11 selection requests until all selections + * have been claimed by another application. + */ +static void _nonnull_ serve_clipboard(uint64_t hash, struct cs_content *content, + const bool selection_active[CM_SEL_MAX]) { + bool running = true; + XEvent evt; + Atom targets, utf8_string; + Atom selection_atoms[CM_SEL_MAX]; + Window win; + int remaining_selections; + size_t selection_atoms_len = 0; + + dpy = XOpenDisplay(NULL); + expect(dpy); + + // Threshold and payload size are intentionally different: the server's + // max request size is a reasonable INCR cutoff, but too aggressive as an + // outbound chunk size for some requestors. + incr_threshold = get_incr_threshold(dpy); + chunk_size = get_chunk_size(dpy); + + win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 0, 0, 0); + XStoreName(dpy, win, "clipserve"); + targets = XInternAtom(dpy, "TARGETS", False); + utf8_string = XInternAtom(dpy, "UTF8_STRING", False); + incr_atom = XInternAtom(dpy, "INCR", False); + + if (selection_active[CM_SEL_PRIMARY]) { + selection_atoms[selection_atoms_len++] = XA_PRIMARY; + } + if (selection_active[CM_SEL_CLIPBOARD]) { + selection_atoms[selection_atoms_len++] = + XInternAtom(dpy, "CLIPBOARD", False); + } + if (selection_active[CM_SEL_SECONDARY]) { + selection_atoms[selection_atoms_len++] = XA_SECONDARY; + } + + die_on(selection_atoms_len == 0, "No selections configured\n"); + + for (size_t i = 0; i < selection_atoms_len; i++) { + bool success = false; + for (int attempts = 0; attempts < 5; attempts++) { + XSetSelectionOwner(dpy, selection_atoms[i], win, CurrentTime); + + // According to ICCCM 2.1, a client acquiring a selection should + // confirm success by verifying with GetSelectionOwner. + if (XGetSelectionOwner(dpy, selection_atoms[i]) == win) { + success = true; + break; + } + } + if (!success) { + die("Failed to set selection for %s\n", + XGetAtomName(dpy, selection_atoms[i])); + } + } + remaining_selections = (int)selection_atoms_len; + + while (running) { + XNextEvent(dpy, &evt); + switch (evt.type) { + case SelectionRequest: { + XSelectionRequestEvent *req = &evt.xselectionrequest; + XSelectionEvent sev = {.type = SelectionNotify, + .display = req->display, + .requestor = req->requestor, + .selection = req->selection, + .time = req->time, + .target = req->target, + .property = req->property}; + + // cppcheck-suppress constVariablePointer + // _drop_(XFree) passes this to XFree, so keep it non-const. + _drop_(XFree) char *window_title = + get_window_title(dpy, req->requestor); + dbg("Servicing request to window '%s' (0x%lX) for clip " PRI_HASH + "\n", + strnull(window_title), (unsigned long)req->requestor, hash); + + if (req->target == targets) { + Atom available_targets[] = {utf8_string, XA_STRING}; + XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, + 32, PropModeReplace, + (unsigned char *)&available_targets, + arrlen(available_targets)); + } else if (req->target == utf8_string || + req->target == XA_STRING) { + if (content->size < (off_t)incr_threshold) { + // Data size is small enough, send directly + XChangeProperty(dpy, req->requestor, req->property, + req->target, 8, PropModeReplace, + (unsigned char *)content->data, + (int)content->size); + } else { + // Initiate INCR transfer + incr_send_start(req, content); + } + } else { + sev.property = None; + } + + XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev); + break; + } + case SelectionClear: { + if (--remaining_selections == 0) { + dbg("Finished serving clip " PRI_HASH "\n", hash); + running = false; + } else { + dbg("%d selections remaining to serve for clip " PRI_HASH + "\n", + remaining_selections, hash); + } + break; + } + case PropertyNotify: { + incr_send_chunk(&evt.xproperty); + break; + } + } + } + + XCloseDisplay(dpy); +} + +int main(int argc, char *argv[]) { + _drop_(config_free) struct config cfg = setup("clipserve"); + exec_man_on_help(argc, argv); + + bool selection_active[CM_SEL_MAX] = {0}; + bool selection_set = false; + int argi = 1; + for (; argi < argc; argi++) { + if (streq(argv[argi], "--")) { + argi++; + break; + } + if (streq(argv[argi], "-s") || streq(argv[argi], "--selection")) { + die_on(argi + 1 >= argc, + "Usage: clipserve [-s selection]... \n"); + enum selection_type sel = selection_name_to_type(argv[++argi]); + die_on(sel == CM_SEL_INVALID, "Unknown selection: %s\n", + argv[argi]); + selection_active[sel] = true; + selection_set = true; + continue; + } + break; + } + + die_on(argi != argc - 1, "Usage: clipserve [-s selection]... \n"); + + if (!selection_set) { + selection_active[CM_SEL_PRIMARY] = true; + selection_active[CM_SEL_CLIPBOARD] = true; + } + + uint64_t hash; + expect(str_to_hex64(argv[argi], &hash) == 0); + + _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + _drop_(cs_destroy) struct clip_store cs; + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + _drop_(cs_content_unmap) struct cs_content content; + die_on(cs_content_get(&cs, hash, &content) < 0, + "Hash " PRI_HASH " inaccessible\n", hash); + + serve_clipboard(hash, &content, selection_active); + + return 0; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..10ba62c --- /dev/null +++ b/src/config.c @@ -0,0 +1,500 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "x.h" + +/** + * Determines the runtime directory for storing application data. This is _not_ + * the clip store, but the place to create the directory that will become it + * (unless CM_DIR is set, in which case we use that directly). + */ +static const char *get_runtime_directory(void) { + static const char *runtime_dir = NULL; + const char *env_vars[] = {"XDG_RUNTIME_DIR", "TMPDIR"}; + + if (runtime_dir) { + return runtime_dir; + } + + for (size_t i = 0; i < arrlen(env_vars); i++) { + const char *dir = getenv(env_vars[i]); + if (dir) { + return (runtime_dir = dir); + } + } + + return (runtime_dir = "/tmp"); +} + +/** + * Constructs the path to the clip store directory for clipmenu, creating the + * directory if it does not exist. + */ +char *get_cache_dir(struct config *cfg) { + expect(cfg->ready); + static char cache_dir[PATH_MAX]; + // In case config changed, do the write anyway + snprintf_safe(cache_dir, PATH_MAX, "%s/clipmenu.%d.%ld", cfg->runtime_dir, + CLIPMENU_VERSION, (long)getuid()); + die_on(mkdir(cache_dir, S_IRWXU) != 0 && errno != EEXIST, + "Failed to create directory: %s\n", cache_dir); + return cache_dir; +} + +static bool is_absolute_path(const char *path) { + return path && path[0] == '/'; +} + +static int resolve_path(const char *input, void *output) { + char **out_path = output; + if (!input) { + return -EINVAL; + } + + if (input[0] == '~') { + if (input[1] != '\0' && input[1] != '/') { + return -EINVAL; + } + const char *home = getenv("HOME"); + if (!home) { + return -EINVAL; + } + size_t len = strlen(home) + strlen(input); + char *expanded = malloc(len); + expect(expanded); + snprintf_safe(expanded, len, "%s%s", home, input + 1); + *out_path = expanded; + } else { + char *dup = strdup(input); + expect(dup); + *out_path = dup; + } + + if (!is_absolute_path(*out_path)) { + free(*out_path); + *out_path = NULL; + return -EINVAL; + } + + return 0; +} + +/** + * This whole section consists of conversion functions to go from a string in + * the config file to the type we expect for `struct Config`. + */ + +static int convert_bool(const char *str, void *output) { + const char *const truthy[] = {"1", "y", "yes", "true", "on"}; + const char *const falsy[] = {"0", "n", "no", "false", "off"}; + + for (size_t i = 0; i < arrlen(truthy); i++) { + if (strceq(str, truthy[i])) { + *(bool *)output = true; + return 0; + } + } + + for (size_t i = 0; i < arrlen(falsy); i++) { + if (strceq(str, falsy[i])) { + *(bool *)output = false; + return 0; + } + } + + return -EINVAL; +} + +static int convert_positive_int(const char *str, void *output) { + char *end; + long val = strtol(str, &end, 10); + if (*end != '\0' || end == str || val < 0 || val > INT_MAX) { + return -EINVAL; + } + *(int *)output = (int)val; + return 0; +} + +static int convert_ignore_window(const char *str, void *output) { + struct ignore_window *iw = output; + iw->set = (bool)str; + if (!iw->set) { + return 0; + } + if (regcomp(&iw->rgx, str, REG_EXTENDED | REG_NOSUB)) { + return -EINVAL; + } + return 0; +} + +static int convert_cm_dir(const char *str, void *output) { + if (!str) { + str = get_runtime_directory(); + } + return resolve_path(str, output); +} + +static int _nonnull_ convert_launcher(const char *str, void *output) { + struct launcher *lnch = output; + + lnch->custom = strdup(str); + expect(lnch->custom); + + if (streq(str, "rofi")) { + lnch->ltype = LAUNCHER_ROFI; + } else { + lnch->ltype = LAUNCHER_CUSTOM; + } + + return 0; +} + +#define DEFAULT_SELECTION_STATE(name) \ + (struct selection) { name, 0, NULL } + +static char *skip_whitespace(char *str) { + while (*str != '\0' && isspace((unsigned char)*str)) { + str++; + } + + return str; +} + +static int convert_selections(const char *str, void *output) { + struct selection *sels = malloc(3 * sizeof(struct selection)); + expect(sels); + sels[CM_SEL_CLIPBOARD] = DEFAULT_SELECTION_STATE("clipboard"); + sels[CM_SEL_PRIMARY] = DEFAULT_SELECTION_STATE("primary"); + sels[CM_SEL_SECONDARY] = DEFAULT_SELECTION_STATE("secondary"); + + _drop_(free) char *inner_str = strdup(str); + expect(inner_str); + char *next = skip_whitespace(inner_str); + while (*next != '\0') { + const char *token = next; + while (*next != '\0' && !isspace((unsigned char)*next)) { + next++; + } + if (*next != '\0') { + *next++ = '\0'; + next = skip_whitespace(next); + } + + bool found = false; + for (size_t i = 0; i < CM_SEL_MAX; i++) { + if (streq(token, sels[i].name)) { + sels[i].active = true; + found = true; + break; + } + } + if (!found) { + return -EINVAL; + } + } + + *(struct selection **)output = sels; + + return 0; +} + +/** + * Constructs the path to the clipmenu configuration file. The user can + * manually specify a path with $CM_CONFIG, otherwise, it's inferred based on + * $XDG_CONFIG_HOME or ~/.config. It's typically + * ~/.config/clipmenu/clipmenu.conf. + */ + +static int get_config_file(char *config_path, size_t config_path_len) { + const char *cm_config = getenv("CM_CONFIG"); + const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char *home = getenv("HOME"); + _drop_(free) char *resolved = NULL; + + if (cm_config) { + int ret = resolve_path(cm_config, &resolved); + if (ret != 0) { + fprintf(stderr, + "Invalid config path from $CM_CONFIG: expected absolute " + "path or leading ~\n"); + return ret; + } + snprintf_safe(config_path, config_path_len, "%s", resolved); + } else if (xdg_config_home) { + int ret = resolve_path(xdg_config_home, &resolved); + if (ret != 0) { + fprintf(stderr, + "Invalid config path from $XDG_CONFIG_HOME: expected " + "absolute path or leading ~\n"); + return ret; + } + snprintf_safe(config_path, config_path_len, "%s/clipmenu/clipmenu.conf", + resolved); + } else { + die_on(!home, + "None of $CM_CONFIG, $XDG_CONFIG_HOME, or $HOME is set\n"); + int ret = resolve_path(home, &resolved); + if (ret != 0) { + fprintf(stderr, + "Invalid config path from $HOME: expected absolute path or " + "leading ~\n"); + return ret; + } + snprintf_safe(config_path, config_path_len, + "%s/.config/clipmenu/clipmenu.conf", resolved); + } + + return 0; +} + +static int config_parse_env_vars(struct config_entry entries[], + size_t entries_len) { + for (size_t i = 0; i < entries_len; ++i) { + const char *env_var = entries[i].env_var; + if (!env_var) { + continue; + } + const char *env_value = getenv(env_var); + if (env_value && entries[i].convert(env_value, entries[i].value) != 0) { + fprintf(stderr, "Error parsing environment variable for $%s\n", + env_var); + return -EINVAL; + } else if (env_value) { + dbg("Config entry %s is set to %s by $%s\n", entries[i].config_key, + env_value, env_var); + entries[i].is_set = true; + } + } + + return 0; +} + +static int config_parse_file(FILE *file, struct config_entry entries[], + size_t entries_len) { + if (!file) { + return 0; + } + + char line[256]; + while (fgets(line, sizeof(line), file)) { + char *key = skip_whitespace(line); + if (*key == '\0' || *key == '#') { + continue; + } + + char *value = key; + while (*value != '\0' && !isspace((unsigned char)*value)) { + value++; + } + if (*value == '\0') { + continue; + } + *value++ = '\0'; + value = skip_whitespace(value); + + char *newline = strpbrk(value, "\r\n"); + if (newline) { + *newline = '\0'; + } + + for (size_t i = 0; i < entries_len; ++i) { + if (!entries[i].is_set && streq(entries[i].config_key, key)) { + if (entries[i].convert(value, entries[i].value) != 0) { + fprintf(stderr, "Error parsing config file for %s\n", + entries[i].config_key); + return -EINVAL; + } + dbg("Config entry %s is set to %s by config file\n", + entries[i].config_key, value); + entries[i].is_set = true; + break; + } + } + } + + return 0; +} + +static int config_apply_default_values(struct config_entry entries[], + size_t entries_len) { + for (size_t i = 0; i < entries_len; ++i) { + if (!entries[i].is_set) { + if (entries[i].convert(entries[i].default_value, + entries[i].value) != 0) { + fprintf(stderr, "Error setting default value for %s\n", + entries[i].config_key); + return -EINVAL; + } + dbg("Config entry %s is set to %s by fallback\n", + entries[i].config_key, entries[i].default_value); + } + } + return 0; +} + +/** + * Parse the clipmenu configuration file and environment variables to set up + * the application configuration. + * + * Prior to version 7, clipmenu and friends could only be configured via + * environment variables, so these are supported for backwards compatibility. + * In general, it's more straightforward to use the config file nowadays. + * + * This is generally not expected to be called by applications -- call + * config_setup() instead, which provides the right file for you. + */ +static int config_setup_internal(FILE *file, struct config *cfg) { + struct config_entry entries[] = { + {"max_clips", "CM_MAX_CLIPS", &cfg->max_clips, convert_positive_int, + "1000", 0}, + {"max_clips_batch", "CM_MAX_CLIPS_BATCH", &cfg->max_clips_batch, + convert_positive_int, "100", 0}, + {"oneshot", "CM_ONESHOT", &cfg->oneshot, convert_positive_int, "0", 0}, + {"deduplicate", "CM_DEDUPLICATE", &cfg->deduplicate, convert_bool, "0", + 0}, + {"own_clipboard", "CM_OWN_CLIPBOARD", &cfg->own_clipboard, convert_bool, + "0", 0}, + {"selections", "CM_SELECTIONS", &cfg->selections, convert_selections, + "clipboard primary", 0}, + {"own_selections", "CM_OWN_SELECTIONS", &cfg->owned_selections, + convert_selections, "clipboard", 0}, + {"ignore_window", "CM_IGNORE_WINDOW", &cfg->ignore_window, + convert_ignore_window, NULL, 0}, + {"launcher", "CM_LAUNCHER", &cfg->launcher, convert_launcher, "dmenu", + 0}, + {"launcher_pass_dmenu_args", "CM_LAUNCHER_PASS_DMENU_ARGS", + &cfg->launcher_pass_dmenu_args, convert_bool, "1", 0}, + {"touch_on_select", NULL, &cfg->touch_on_select, convert_bool, "0", 0}, + {"partial_merge_secs", NULL, &cfg->partial_merge_secs, + convert_positive_int, "2", 0}, + {"cm_dir", "CM_DIR", &cfg->runtime_dir, convert_cm_dir, NULL, 0}}; + + size_t entries_len = arrlen(entries); + + int ret = config_parse_env_vars(entries, entries_len); + if (ret < 0) { + return ret; + } + ret = config_parse_file(file, entries, entries_len); + if (ret < 0) { + return ret; + } + ret = config_apply_default_values(entries, entries_len); + if (ret < 0) { + return ret; + } + + cfg->ready = true; + + return 0; +} + +/** + * Frees dynamically allocated memory within the config structure. + */ +void config_free(struct config *cfg) { + free(cfg->runtime_dir); + free(cfg->launcher.custom); + free(cfg->selections); + free(cfg->owned_selections); + if (cfg->ignore_window.set) { + regfree(&cfg->ignore_window.rgx); + } +} + +/** + * Initialise the clipmenu configuration by loading settings from environment + * variables and the configuration file. + */ +static void config_setup(struct config *cfg) { + char config_path[PATH_MAX]; + die_on(get_config_file(config_path, sizeof(config_path)) != 0, + "Invalid config\n"); + _drop_(fclose) FILE *file = fopen(config_path, "r"); + expect(file || errno == ENOENT); + die_on(config_setup_internal(file, cfg) != 0, "Invalid config\n"); +} + +static char stdout_buf[512]; +const char *prog_name = "broken"; + +/** + * Performs initial setup for clipmenu applications, including setting the + * program name (used for dbg()), making sure stdout is line buffered, getting + * the config, and setting up X11 error handling. + */ +struct config setup(const char *inner_prog_name) { + struct config cfg; + prog_name = inner_prog_name; + expect(setvbuf(stdout, stdout_buf, _IOLBF, sizeof(stdout_buf)) == 0); + config_setup(&cfg); + XSetErrorHandler(xerror_handler); + return cfg; +} + +void setup_selections(Display *dpy, struct cm_selections *sels) { + sels[CM_SEL_CLIPBOARD].selection = XInternAtom(dpy, "CLIPBOARD", False); + sels[CM_SEL_CLIPBOARD].storage = + XInternAtom(dpy, "CLIPMENUD_CUR_CLIPBOARD", False); + sels[CM_SEL_PRIMARY].selection = XA_PRIMARY; + sels[CM_SEL_PRIMARY].storage = + XInternAtom(dpy, "CLIPMENUD_CUR_PRIMARY", False); + sels[CM_SEL_SECONDARY].selection = XA_SECONDARY; + sels[CM_SEL_SECONDARY].storage = + XInternAtom(dpy, "CLIPMENUD_CUR_SECONDARY", False); +} + +/** + * Maps an Atom to a selection_type based on the selection atoms in the + * provided cm_selections struct. Returns CM_SEL_INVALID on error if the + * selection type is not found. + */ +enum selection_type +selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { + for (size_t i = 0; i < CM_SEL_MAX; ++i) { + if (sels[i].selection == atom) { + return i; + } + } + return CM_SEL_INVALID; +} + +/** + * Maps an Atom to a selection_type based on the storage atoms in the provided + * cm_selections struct. Returns CM_SEL_INVALID on error if the storage type is + * not found. + */ +enum selection_type +storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { + for (size_t i = 0; i < CM_SEL_MAX; ++i) { + if (sels[i].storage == atom) { + return i; + } + } + return CM_SEL_INVALID; +} + +void exec_man(void) { + execlp("man", "man", prog_name, NULL); + die("Failed to exec man: %s\n", strerror(errno)); +} + +// cppcheck doesn't know this comes from an outside type +// cppcheck-suppress [constParameter,unmatchedSuppression] +void exec_man_on_help(int argc, char *argv[]) { + for (int i = 1; i < argc; i++) { + if (streq(argv[i], "--")) { + break; + } + if (streq(argv[i], "-h") || streq(argv[i], "--help")) { + exec_man(); + } + } +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..949ebd8 --- /dev/null +++ b/src/config.h @@ -0,0 +1,104 @@ +#ifndef CM_CONFIG_H +#define CM_CONFIG_H + +#include +#include +#include +#include +#include + +#include "util.h" + +#define CLIPMENU_VERSION 7 + +struct selection { + const char *name; + bool active; + Atom *atom; +}; +enum selection_type { + CM_SEL_CLIPBOARD, + CM_SEL_PRIMARY, + CM_SEL_SECONDARY, + CM_SEL_MAX, + CM_SEL_INVALID +}; +struct cm_selections { + Atom selection; + Atom storage; +}; +struct ignore_window { + bool set; + regex_t rgx; +}; +enum launcher_known { + LAUNCHER_ROFI, + LAUNCHER_CUSTOM, +}; +struct launcher { + enum launcher_known ltype; + char *custom; +}; +struct config { + bool ready; + bool debug; + char *runtime_dir; + int max_clips; + int max_clips_batch; + int oneshot; + bool deduplicate; + bool own_clipboard; + struct selection *owned_selections; + struct selection *selections; + struct ignore_window ignore_window; + struct launcher launcher; + bool launcher_pass_dmenu_args; + bool touch_on_select; + int partial_merge_secs; +}; +typedef int (*conversion_func_t)(const char *, void *); +struct config_entry { + const char *config_key; + const char *env_var; + void *value; + conversion_func_t convert; + const char *default_value; + bool is_set; +}; + +char *get_cache_dir(struct config *cfg); + +void exec_man(void); +void exec_man_on_help(int argc, char *argv[]); + +/** + * Define a function that generates and caches a path within the application's + * cache directory. + * + * For example, DEFINE_GET_PATH_FUNCTION(foo) defines a function `get_foo_path` + * that returns the path to "foo" within the cache directory. + */ +#define DEFINE_GET_PATH_FUNCTION(name) \ + static inline char *get_##name##_path(struct config *cfg) { \ + static char path[PATH_MAX]; \ + /* Just in case the config changed, do the write anyway */ \ + snprintf_safe(path, PATH_MAX, "%s/" #name, get_cache_dir(cfg)); \ + return path; \ + } + +DEFINE_GET_PATH_FUNCTION(line_cache) +DEFINE_GET_PATH_FUNCTION(enabled) +DEFINE_GET_PATH_FUNCTION(session_lock) + +extern const char *prog_name; +struct config _nonnull_ setup(const char *inner_prog_name); +void _nonnull_ setup_selections(Display *dpy, struct cm_selections *sels); +enum selection_type _nonnull_ +selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels); +enum selection_type _nonnull_ +storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels); + +void config_free(struct config *cfg); +DEFINE_DROP_FUNC_PTR(struct config, config_free) + +#endif diff --git a/src/menu_util.c b/src/menu_util.c new file mode 100644 index 0000000..3358c43 --- /dev/null +++ b/src/menu_util.c @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "menu_util.h" +#include "store.h" +#include "util.h" + +#define MAX_ARGS 32 +/* "[N] " prefix + snip line + " (N lines)\n" suffix + NUL */ +#define LAUNCHER_LINE_MAX \ + (1 + UINT64_MAX_STRLEN + 2 + (CS_SNIP_LINE_SIZE - 1) + 2 + \ + UINT64_MAX_STRLEN + 7 + 1 + 1) + +static int dmenu_user_argc; +static char **dmenu_user_argv; + +/** + * Calculate the base 10 padding length for a number. + */ +static int get_padding_length(size_t num) { + int digits = 1; + while (num /= 10) { + digits++; + } + return digits; +} + +/** + * Execute the launcher. Called after fork() is already done in the new child. + */ +static void _noreturn_ _nonnull_ exec_launcher(struct config *cfg, + const char *prompt, + int *input_pipe, + int *output_pipe) { + dup2(input_pipe[0], STDIN_FILENO); + close(input_pipe[1]); + close(output_pipe[0]); + dup2(output_pipe[1], STDOUT_FILENO); + + const char *dmenu_args[] = {"-p", prompt, "-l", "20"}; + const char **cmd = malloc(MAX_ARGS * sizeof(char *)); + + size_t d_i = 0; + + switch (cfg->launcher.ltype) { + case LAUNCHER_ROFI: + cmd[d_i++] = "rofi"; + cmd[d_i++] = "--"; + cmd[d_i++] = "-dmenu"; + break; + case LAUNCHER_CUSTOM: + cmd[d_i++] = cfg->launcher.custom; + break; + default: + die("Unreachable\n"); + } + + if (cfg->launcher_pass_dmenu_args) { + expect(d_i + arrlen(dmenu_args) < MAX_ARGS); + for (size_t i = 0; i < arrlen(dmenu_args); i++) { + cmd[d_i++] = dmenu_args[i]; + } + } + + for (int i = 1; i < dmenu_user_argc && d_i < MAX_ARGS - 1; i++, d_i++) { + cmd[d_i] = dmenu_user_argv[i]; + } + + cmd[d_i] = NULL; + execvp(cmd[0], (char *const *)cmd); // SUS says cmd unchanged + die("Failed to exec %s: %s\n", cmd[0], strerror(errno)); +} + +static int dprintf_ellipsise_long_snip_line(int fd, const char *line) { + size_t line_len = strlen(line); + if (line_len == CS_SNIP_LINE_SIZE - 1) { + return dprintf(fd, "%.*s...", (int)(CS_SNIP_LINE_SIZE - 4), line); + } else { + return dprintf(fd, "%s", line); + } +} + +static int launcher_write_all(int fd, const char *buf, size_t count) { + while (count > 0) { + ssize_t written = write(fd, buf, count); + if (written < 0) { + if (errno == EINTR) { + continue; + } + return negative_errno(); + } + buf += written; + count -= (size_t)written; + } + return 0; +} + +static int wait_for_pid(pid_t pid, int *status) { + while (waitpid(pid, status, 0) < 0) { + if (errno == EINTR) { + continue; + } + return negative_errno(); + } + return 0; +} + +static int parse_sel_idx(const char *line, size_t cur_clips, size_t *out_idx) { + if (line[0] != '[') { + return -1; + } + const char *start = line + 1; + const char *end = strchr(start, ']'); + if (!end) { + return -1; + } + char idx_str[UINT64_MAX_STRLEN + 1]; + size_t len = (size_t)(end - start); + if (len >= sizeof(idx_str)) { + return -1; + } + memcpy(idx_str, start, len); + idx_str[len] = '\0'; + uint64_t sel_idx; + if (str_to_uint64(idx_str, &sel_idx) < 0 || sel_idx == 0 || + sel_idx > cur_clips) { + return -1; + } + *out_idx = (size_t)(sel_idx - 1); + return 0; +} + +/** + * Writes the available clips to the launcher and reads back the user's + * selection(s), calling action for each selected clip. + */ +static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe, + int *output_pipe, pid_t launcher_pid, + clip_action_fn action) { + close(input_pipe[0]); + close(output_pipe[1]); + + _drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + _drop_(cs_destroy) struct clip_store cs; + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + struct ref_guard guard = cs_ref(&cs); + size_t cur_clips; + expect(cs_len(&cs, &cur_clips) == 0); + _drop_(free) uint64_t *idx_to_hash = malloc(cur_clips * sizeof(uint64_t)); + expect(idx_to_hash); + int pad = get_padding_length(cur_clips); + size_t clip_idx = cur_clips; + bool launcher_closed_stdin = false; + int forced_ret = 0; + + struct sigaction ignore_sa = {0}; + ignore_sa.sa_handler = SIG_IGN; + sigemptyset(&ignore_sa.sa_mask); + struct sigaction old_sa; + expect(sigaction(SIGPIPE, &ignore_sa, &old_sa) == 0); + + struct cs_snip *snip = NULL; + while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { + size_t idx = --clip_idx; + idx_to_hash[idx] = snip->hash; + + if (launcher_closed_stdin) { + continue; + } + + char prefix[64]; + size_t prefix_len = + snprintf_safe(prefix, sizeof(prefix), "[%*zu] ", pad, idx + 1); + int write_ret = launcher_write_all(input_pipe[1], prefix, prefix_len); + if (write_ret == -EPIPE) { + launcher_closed_stdin = true; + continue; + } + if (write_ret < 0) { + forced_ret = EXIT_FAILURE; + break; + } + + write_ret = dprintf_ellipsise_long_snip_line(input_pipe[1], snip->line); + if (write_ret < 0 && errno == EPIPE) { + launcher_closed_stdin = true; + continue; + } + if (write_ret < 0) { + forced_ret = EXIT_FAILURE; + break; + } + + if (snip->nr_lines > 1) { + char lines_suffix[64]; + size_t lines_suffix_len = + snprintf_safe(lines_suffix, sizeof(lines_suffix), + " (%zu lines)", snip->nr_lines); + write_ret = launcher_write_all(input_pipe[1], lines_suffix, + lines_suffix_len); + if (write_ret == -EPIPE) { + launcher_closed_stdin = true; + continue; + } + if (write_ret < 0) { + forced_ret = EXIT_FAILURE; + break; + } + } + + write_ret = launcher_write_all(input_pipe[1], "\n", 1); + if (write_ret == -EPIPE) { + launcher_closed_stdin = true; + continue; + } + if (write_ret < 0) { + forced_ret = EXIT_FAILURE; + break; + } + } + + // We've written everything and have our own map, no need to hold any more + cs_unref(guard.cs); + + close(input_pipe[1]); + expect(sigaction(SIGPIPE, &old_sa, NULL) == 0); + + _drop_(fclose) FILE *output = fdopen(output_pipe[0], "r"); + expect(output != NULL); + char line[LAUNCHER_LINE_MAX]; + while (!forced_ret && fgets(line, sizeof(line), output) != NULL) { + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + len--; + } + if (len == 0) { + continue; + } + size_t idx; + if (parse_sel_idx(line, cur_clips, &idx) == 0) { + int ret = action(cfg, idx_to_hash[idx]); + if (ret != 0) { + forced_ret = ret; + } + } else { + forced_ret = EXIT_FAILURE; + } + } + + int dmenu_status; + int wait_ret = wait_for_pid(launcher_pid, &dmenu_status); + + if (forced_ret || wait_ret < 0 || !WIFEXITED(dmenu_status)) { + return EXIT_FAILURE; + } + + return WEXITSTATUS(dmenu_status); +} + +/** + * Prompts the user to select clip(s) via their configured launcher, and + * executes the provided action on each selected clip. + */ +int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt, + clip_action_fn action) { + int input_pipe[2], output_pipe[2]; + expect(pipe(input_pipe) == 0 && pipe(output_pipe) == 0); + + pid_t pid = fork(); + expect(pid >= 0); + + if (pid == 0) { + exec_launcher(cfg, prompt, input_pipe, output_pipe); + } + + return interact_with_dmenu(cfg, input_pipe, output_pipe, pid, action); +} + +/** + * Set the user-provided command-line arguments for launcher passthrough. + * + * This is janky and just mimics how clipmenu worked by itself, in the long run + * we should not rely on these. + */ +void menu_set_user_args(int argc, char **argv) { + dmenu_user_argc = argc; + dmenu_user_argv = argv; +} diff --git a/src/menu_util.h b/src/menu_util.h new file mode 100644 index 0000000..3bb15de --- /dev/null +++ b/src/menu_util.h @@ -0,0 +1,18 @@ +#ifndef CM_MENU_UTIL_H +#define CM_MENU_UTIL_H + +#include + +#include "config.h" + +/** + * Callback function type for handling selected clips. + */ +typedef int (*clip_action_fn)(struct config *cfg, uint64_t hash); + +void menu_set_user_args(int argc, char **argv); + +int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt, + clip_action_fn action); + +#endif diff --git a/src/store.c b/src/store.c new file mode 100644 index 0000000..fd2d80e --- /dev/null +++ b/src/store.c @@ -0,0 +1,847 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "store.h" + +/** + * TERMINOLOGY + * + * - Clip store: A pairing of a file containing snips and a content directory + * - Clip store entry: A single pair of snip and content entry + * - Snip file: A file which maps lines to content entries using hashes + * - Snip: A (hash, line) pair in the clip store, representing a content entry + * - Content entry: The full data stored from the clipboard + * - Content directory: The directory in which the content entries are stored + * + * KEY FUNCTIONS: + * + * - cs_add - add a clip store entry + * - cs_remove - remove a clip store entry by callback + * - cs_trim - trim to the newest/oldest N entries + * - cs_snip_iter - iterate over snip hashes and lines + * - cs_content_get - get the content for a snip hash + * + * CLIP STORE DESIGN + * + * Our primary focus is on achieving high efficiency in appending new snips, + * iterating over the entire list of snips, and replacing the final snip in the + * snip file. For this reason complexity is avoided in processing deletions, + * since deletions are usually rare. This avoids us having to implement (for + * example) tombstones, which would slow things down and complicate iteration. + * + * CONTENT DIRECTORY DESIGN + * + * The content directory is extremely simple: it contains files with the same + * name as the snip hash. This allows quickly going from the one line summary + * in the snip to the full contents. + * + * SYNCHRONISATION + * + * The clip store's size may be increased or decreased by another program using + * the library, so before each operation we must take a lock and check if the + * header was updated. If it was, we update the size of the mmapped area to + * suit. The lock is implemented using flock() on cs->snip_fd, see cs_ref(), + * cs_ref_no_update(), and cs_unref(). + */ + +/** + * Calculate the needed file size in bytes for @nr_snips snips, adding the + * header. + * + * @nr_snips: The number of snips to calculate for + */ +static size_t _must_use_ cs_file_size(size_t nr_snips) { + return (nr_snips + 1) * CS_SNIP_SIZE; +} + +/** + * Validate the consistency of the clip store's header information. + * + * @cs: The clip store to operate on + * @file_size: The current size of the file + */ +static int _must_use_ _nonnull_ cs_header_validate(const struct clip_store *cs, + size_t file_size) { + if (cs->header->nr_snips > cs->header->nr_snips_alloc || + (cs->header->nr_snips_alloc + 1) * CS_SNIP_SIZE != file_size) { + return -EINVAL; + } + return 0; +} + +static int _must_use_ _nonnull_ cs_remap(struct clip_store *cs, size_t old_size, + size_t new_size) { + struct cs_header *new_header = mmap(NULL, new_size, PROT_READ | PROT_WRITE, + MAP_SHARED, cs->snip_fd, 0); + if (new_header == MAP_FAILED) { + return negative_errno(); + } + if (munmap(cs->header, old_size) < 0) { + int ret = negative_errno(); + munmap(new_header, new_size); + return ret; + } + cs->header = new_header; + cs->snips = (struct cs_snip *)(cs->header + 1); + cs->mapped_file_size = new_size; + return 0; +} + +/** + * Decrease the reference count for the clip store lock, unrefing it if + * the refcount reaches zero. + * + * @cs: The clip store to operate on + */ +void cs_unref(struct clip_store *cs) { + expect(cs->refcount > 0); + cs->refcount--; + if (cs->refcount == 0) { + expect(flock(cs->snip_fd, LOCK_UN) == 0); + } +} + +/** + * Increase the reference count for the clip store lock. + * + * @cs: The clip store to operate on + */ +static struct ref_guard _must_use_ _nonnull_ +cs_ref_no_update(struct clip_store *cs) { + struct ref_guard guard = {.status = 0, .unref = cs_unref, .cs = cs}; + if (cs->refcount == 0) { + expect(flock(cs->snip_fd, LOCK_EX) == 0); + } + static_assert(sizeof(cs->refcount) == sizeof(size_t), + "refcount type wrong"); + expect(cs->refcount < SIZE_MAX); + cs->refcount++; + return guard; +} + +/** + * Increase the reference count for the clip store lock, and remap as needed if + * the header values have changed. + * + * Even if the guard status indicates an error, you must still call cs_unref(). + * + * @cs: The clip store to operate on + */ +struct ref_guard cs_ref(struct clip_store *cs) { + struct ref_guard guard = cs_ref_no_update(cs); + + if (cs->refcount > 1) { + // We're an inner reference, so any necessary remapping has already + // been performed. + return guard; + } + + if (cs->local_nr_snips != cs->header->nr_snips || + cs->local_nr_snips_alloc != cs->header->nr_snips_alloc) { + struct stat st; + if (fstat(cs->snip_fd, &st) < 0) { + guard.status = negative_errno(); + return guard; + } + + int ret = cs_header_validate(cs, (size_t)st.st_size); + if (ret < 0) { + guard.status = ret; + return guard; + } + + // If we shrank, no need to remap, since we'll just use the new bounds. + if (cs->local_nr_snips_alloc < cs->header->nr_snips_alloc) { + int remap_ret = cs_remap(cs, cs->mapped_file_size, + cs_file_size(cs->header->nr_snips_alloc)); + if (remap_ret < 0) { + guard.status = remap_ret; + return guard; + } + } + + cs->local_nr_snips = cs->header->nr_snips; + cs->local_nr_snips_alloc = cs->header->nr_snips_alloc; + } + + return guard; +} + +/** + * _drop_() function for when a `ref_guard` structure goes out of scope. + * + * @guard: The guard lock + */ +void drop_cs_unref(struct ref_guard *guard) { + guard->status = -EBADF; + guard->unref(guard->cs); +} + +/** + * Destroy the clip store, releasing all of its resources. + * + * @cs: The clip store to operate on + */ +int cs_destroy(struct clip_store *cs) { + cs->ready = false; + if (munmap(cs->header, cs->mapped_file_size)) { + return negative_errno(); + } + return 0; +} + +/** + * _drop_() function for when a `clip_store` goes out of scope. + * + * @guard: The guard lock + */ +void drop_cs_destroy(struct clip_store *cs) { expect(cs_destroy(cs) == 0); } + +/** + * Initialise a `struct clip_store` with snip_fd open to a file for snip + * storage and content_fd open to a directory for content entry storage. + * + * The snip file is extended and the header snip is written if the file size is + * zero. The file is mapped into memory until cs_destroy() is called. + * + * @cs: The clip store to initialise + * @snip_fd: Open file descriptor for the snip file + * @content_dir_fd: Open file descriptor for the content directory + */ +int cs_init(struct clip_store *cs, int snip_fd, int content_dir_fd) { + cs->ready = false; + cs->snip_fd = snip_fd; + cs->content_dir_fd = content_dir_fd; + cs->refcount = 0; + _drop_(cs_unref) struct ref_guard guard = cs_ref_no_update(cs); + + struct stat st; + if (fstat(snip_fd, &st) < 0) { + return negative_errno(); + } + if (st.st_size % CS_SNIP_SIZE != 0) { + return -EINVAL; + } + + size_t file_size = (size_t)st.st_size; + if (file_size == 0) { + file_size = CS_SNIP_SIZE; + if (ftruncate(snip_fd, (off_t)file_size) < 0) { + return negative_errno(); + } + } + + cs->header = + mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, snip_fd, 0); + if (cs->header == MAP_FAILED) { + return negative_errno(); + } + + int ret = cs_header_validate(cs, file_size); + if (ret < 0) { + munmap(cs->header, file_size); + return ret; + } + + cs->snips = (struct cs_snip *)(cs->header + 1); + cs->local_nr_snips = cs->header->nr_snips; + cs->local_nr_snips_alloc = cs->header->nr_snips_alloc; + cs->mapped_file_size = file_size; + cs->ready = true; + + (void)guard; // Old clang will complain guard is unused, despite cleanup + + return 0; +} + +/** + * Round up a number to the nearest multiple of a specified step. + * + * @n: The number to round up + * @step: The step size to round up to + */ +static size_t round_up(size_t n, size_t step) { + return ((n + step - 1) / step) * step; +} + +/** + * Adjust the size of the mmapped file used by the clip store to the specified + * new size. It first attempts to resize the file using ftruncate(). If this + * operation is successful, it then remaps the memory mapping to reflect the + * new size of the file. + * + * WARNING: cs->snips and cs->header may move after cs_file_resize(), so + * copied pointers from before invocation must not be used after calling this. + * + * @cs: The clip store to operate on + * @new_nr_snips: The new number of snips in the snip file + */ +static int _must_use_ _nonnull_ cs_file_resize(struct clip_store *cs, + size_t new_nr_snips) { + bool grow = new_nr_snips >= cs->header->nr_snips; + + if (grow && new_nr_snips <= cs->header->nr_snips_alloc) { + cs->header->nr_snips = new_nr_snips; + return 0; + } + + /* If this is a shrink, do it exactly: someone may be deleting sensitive + * snips and so it's best to remove them immediately. Otherwise, batch + * allocate. */ + size_t new_nr_snips_alloc = + grow ? round_up(new_nr_snips, CS_SNIP_ALLOC_BATCH) : new_nr_snips; + + size_t new_size = cs_file_size(new_nr_snips_alloc); + if (ftruncate(cs->snip_fd, (off_t)new_size) < 0) { + return negative_errno(); + } + + if (grow) { + size_t old_file_size = cs_file_size(cs->header->nr_snips_alloc); + int ret = cs_remap(cs, cs->mapped_file_size, new_size); + if (ret < 0) { + expect(ftruncate(cs->snip_fd, (off_t)old_file_size) == 0); + return ret; + } + } + + cs->header->nr_snips = cs->local_nr_snips = new_nr_snips; + cs->header->nr_snips_alloc = cs->local_nr_snips_alloc = new_nr_snips_alloc; + + return 0; +} + +/** + * Update a clip store snip to contain the specified hash and line content. + * + * @snip: Pointer to the snip to modify + * @hash: The new hash value for the snip + * @line: The new line content for the snip + * @nr_lines: The number of lines in the line content + */ +static void _nonnull_ cs_snip_update(struct cs_snip *snip, uint64_t hash, + const char *line, uint64_t nr_lines) { + snip->hash = hash; + snip->doomed = false; + snip->nr_lines = nr_lines; + strncpy(snip->line, line, CS_SNIP_LINE_SIZE - 1); + snip->line[CS_SNIP_LINE_SIZE - 1] = '\0'; +} + +/** + * Extracts the first non-empty line from a given text buffer and copies it to + * the output buffer. Returns the total number of lines. A final line with no + * newline is considered a line for accounting purposes. + * + * @text: The input text buffer + * @out: The output buffer. Must be at least CS_SNIP_LINE_SIZE bytes + */ +size_t first_line(const char *text, char *out) { + bool found = false; + size_t nr_lines = 0; + const char *cur = text; + + out[0] = '\0'; + + for (; *cur; cur++) { + nr_lines += (*cur == '\n'); + if (!found && *cur != '\n') { + found = true; + snprintf(out, CS_SNIP_LINE_SIZE, "%.*s", (int)strcspn(cur, "\n"), + cur); + } + } + + return nr_lines + (found && *--cur != '\n'); +} + +/** + * Add a new snip consisting of a hash value and a line of text to the end of + * the clip store. The snip file size is grown as necessary to accommodate the + * new snip. + * + * @cs: The clip store to operate on + * @hash: The hash value of the snip to add + * @line: The line content of the snip to add + * @nr_lines: The number of lines in the line content + */ +static int _must_use_ _nonnull_ cs_snip_add(struct clip_store *cs, + uint64_t hash, const char *line, + uint64_t nr_lines) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + int ret = cs_file_resize(cs, cs->header->nr_snips + 1); + if (ret < 0) { + return ret; + } + cs_snip_update(cs->snips + cs->header->nr_snips - 1, hash, line, nr_lines); + return 0; +} + +/** + * Add content to the content directory using the hash as the filename. + * + * @cs: The clip store to operate on + * @hash: The hash of the content to add + * @content: The content to add to the file + * @dupe_policy: If set to CS_DUPE_KEEP_LAST, will return with -EEXIST when + * trying to insert duplicate entry. + */ +static int _must_use_ _nonnull_ +cs_content_add(struct clip_store *cs, uint64_t hash, const char *content, + enum cs_dupe_policy dupe_policy) { + bool dupe = false; + size_t content_len = strlen(content); + + char dir_path[CS_HASH_STR_MAX]; + snprintf(dir_path, sizeof(dir_path), PRI_HASH, hash); + + int ret = mkdirat(cs->content_dir_fd, dir_path, 0700); + if (ret < 0) { + if (errno != EEXIST || dupe_policy == CS_DUPE_KEEP_LAST) { + return negative_errno(); + } + dupe = true; + } + + char base_file_path[PATH_MAX]; + snprintf(base_file_path, sizeof(base_file_path), "%s/1", dir_path); + + if (dupe) { + // This clip already exists, just create a link for refcounting + struct stat st; + if (fstatat(cs->content_dir_fd, base_file_path, &st, 0) < 0) { + return negative_errno(); + } + + if ((size_t)st.st_size != content_len) { + // Extremely unlikely with FNV-1a 64-bit outside of artificial + // scenarios, but never hurts to be careful... + return -EFAULT; + } + + size_t link_num = (size_t)st.st_nlink + 1; + char linkpath[PATH_MAX]; + snprintf(linkpath, sizeof(linkpath), "%s/%zu", dir_path, link_num); + if (linkat(cs->content_dir_fd, base_file_path, cs->content_dir_fd, + linkpath, 0) < 0) { + return negative_errno(); + } + + return 0; + } + + // This is a new clip + _drop_(close) int fd = openat(cs->content_dir_fd, base_file_path, + O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + return negative_errno(); + } + + const char *cur = content; + size_t remaining = content_len; + + while (remaining > 0) { + ssize_t written = write(fd, cur, remaining); + if (written < 0) { + return negative_errno(); + } + remaining -= (size_t)written; + cur += written; + } + + return 0; +} + +/** + * Clean up the backing mapped data and file descriptor for a `struct + * cs_content` object. + * + * @content: The content to unmap + */ +static int cs_content_unmap(struct cs_content *content) { + if (content && content->data) { + close(content->fd); + if (munmap(content->data, (size_t)content->size)) { + return negative_errno(); + } + } + return 0; +} + +/** + * _drop_ function for cs_content_unmap() + * + * @content: The content to unmap + */ +void drop_cs_content_unmap(struct cs_content *content) { + int ret = cs_content_unmap(content); + expect(ret == 0); +} + +/** + * Retrieve the content associated with a given hash from the content directory + * and map it into memory. + * + * @cs: The clip store to operate on + * @hash: The hash of the content to retrieve + * @content: A pointer to a `struct cs_content` to populate. The caller must + * call cs_content_unmap() when done to free it + */ +int cs_content_get(struct clip_store *cs, uint64_t hash, + struct cs_content *content) { + memset(content, '\0', sizeof(struct cs_content)); + + char filename[PATH_MAX]; + snprintf(filename, sizeof(filename), PRI_HASH "/1", hash); + + _drop_(close) int fd = openat(cs->content_dir_fd, filename, O_RDONLY); + if (fd < 0) { + return negative_errno(); + } + + struct stat st; + if (fstat(fd, &st) < 0) { + return negative_errno(); + } + + char *data = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (data == MAP_FAILED) { + return negative_errno(); + } + + content->data = data; + content->fd = fd; + content->size = st.st_size; + fd = -1; // Disarm the _drop_, this will be handled by cs_content_unmap() + + return 0; +} + +/** + * Move the entry with the specified hash to the newest slot. + * + * @cs: The clip store to operate on + * @hash: The hash of the entry to move + */ +int cs_make_newest(struct clip_store *cs, uint64_t hash) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + + for (int i = 0; i < (int)cs->local_nr_snips; ++i) { + if (cs->snips[i].hash == hash) { + struct cs_snip tmp = cs->snips[i]; + memmove(cs->snips + i, cs->snips + i + 1, + (cs->local_nr_snips - (i + 1)) * sizeof(*cs->snips)); + cs->snips[cs->local_nr_snips - 1] = tmp; + return 0; + } + } + return -ENOENT; +} + +static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs, + uint64_t hash); + +/** + * Add a new content entry to the clip store and content directory. + * + * @cs: The clip store to operate on + * @content: The content to add + * @out_hash: Output for the generated hash, or NULL + * @dupe_policy: Policy to use for duplicate entries + */ +int cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash, + enum cs_dupe_policy dupe_policy) { + uint64_t hash = fnv1a_64_hash(content); + char line[CS_SNIP_LINE_SIZE]; + size_t nr_lines = first_line(content, line); + + if (out_hash) { + *out_hash = hash; + } + + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + + int ret = cs_content_add(cs, hash, content, dupe_policy); + if (ret == -EEXIST && dupe_policy == CS_DUPE_KEEP_LAST) { + return cs_make_newest(cs, hash); + } + if (ret) { + return ret; + } + ret = cs_snip_add(cs, hash, line, nr_lines); + if (ret) { + int rm_ret = cs_content_remove(cs, hash); + if (rm_ret) { + warn("Failed to roll back leaked content for hash " PRI_HASH + " after cs_snip_add failed (%s): %s\n", + hash, strerror(-ret), strerror(-rm_ret)); + return rm_ret; + } + } + return ret; +} + +/** + * Iterate over the snips in the clip store. The function should be initially + * called with *snip set to NULL, which will set *snip to point to the newest + * snip (excluding the header). On subsequent calls, *snip is updated to point + * to the next snip in the snip file. The iteration stops when there are no + * more snips to process, indicated by the function returning false. + * + * @guard: The guard lock + * @snip: Pointer to a pointer to the current snip being iterated over + * @direction: Whether to iterate from the oldest to newest or vice versa + */ +bool cs_snip_iter(struct ref_guard *guard, enum cs_iter_direction direction, + struct cs_snip **snip) { + if (guard->status < 0 || guard->cs->header->nr_snips == 0) { + return false; + } + + struct cs_snip *oldest = guard->cs->snips; + struct cs_snip *newest = oldest + (guard->cs->header->nr_snips - 1); + const struct cs_snip *stop = + direction == CS_ITER_NEWEST_FIRST ? oldest : newest; + + if (!*snip) { + *snip = direction == CS_ITER_NEWEST_FIRST ? newest : oldest; + return true; + } else if (*snip != stop) { + *snip = *snip + (direction == CS_ITER_NEWEST_FIRST ? -1 : 1); + return true; + } + return false; +} + +/** + * Remove content from the content directory using the hash as the filename. + * + * @cs: The clip store to operate on + * @hash: The hash of the content to remove + */ +static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs, + uint64_t hash) { + + char hash_dir_name[CS_HASH_STR_MAX]; + snprintf(hash_dir_name, sizeof(hash_dir_name), PRI_HASH, hash); + + _drop_(close) int hash_dir_fd = + openat(cs->content_dir_fd, hash_dir_name, O_RDONLY); + if (hash_dir_fd < 0) { + return negative_errno(); + } + + struct stat st; + if (fstatat(hash_dir_fd, "1", &st, 0) < 0) { + return negative_errno(); + } + + char nlink_path[PATH_MAX]; + snprintf(nlink_path, sizeof(nlink_path), "%u", (unsigned)st.st_nlink); + + if (unlinkat(hash_dir_fd, nlink_path, 0) < 0) { + return negative_errno(); + } + + if (st.st_nlink == 1 && + unlinkat(cs->content_dir_fd, hash_dir_name, AT_REMOVEDIR) < 0) { + return negative_errno(); + } + + return 0; +} + +/** + * Compacts the clip store by removing doomed snips, finalising their removal + * after being marked in cs_remove(). + * + * @guard: The guard lock + */ +static size_t _nonnull_ cs_snip_remove_doomed(struct ref_guard *guard) { + size_t nr_doomed = 0; + struct cs_snip *snip = NULL; + + while (cs_snip_iter(guard, CS_ITER_OLDEST_FIRST, &snip)) { + if (snip->doomed) { + nr_doomed++; + } else if (nr_doomed > 0) { + *(snip - nr_doomed) = *snip; + } + } + + return nr_doomed; +} + +/** + * Iterate over the specified number of snips in the clip store from newest + * to oldest and remove those for which the predicate function returns + * CS_ACTION_REMOVE (see `enum cs_remove_action` above). + * + * @cs: The clip store to operate on + * @should_remove: Function pointer to the predicate used to decide removal + * @private: Pointer to user-defined data passed to the predicate function + * @direction: Whether to iterate from the oldest to newest or vice versa + */ +int cs_remove(struct clip_store *cs, enum cs_iter_direction direction, + enum cs_remove_action (*should_remove)(uint64_t, const char *, + void *), + void *private) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + + bool found = false; + struct cs_snip *snip = NULL; + + while (cs_snip_iter(&guard, direction, &snip)) { + enum cs_remove_action action = + should_remove(snip->hash, snip->line, private); + + if (action & CS_ACTION_REMOVE) { + found = true; + int ret = cs_content_remove(cs, snip->hash); + if (ret < 0) { + return ret; + } + snip->doomed = true; + } + if (action & CS_ACTION_STOP) { + break; + } + } + + if (!found) { + return 0; + } + + size_t nr_doomed = cs_snip_remove_doomed(&guard); + int ret = cs_file_resize(cs, cs->header->nr_snips - nr_doomed); + if (ret < 0) { + return ret; + } + + return 0; +} + +/** + * Callback function for deciding which snips to remove during trim operation. + * + * @hash: Unused + * @line: Unused + * @private: The count of remaining entries to trim + */ +static enum cs_remove_action _must_use_ _nonnull_ +trim_callback(uint64_t hash, const char *line, void *private) { + (void)hash; + (void)line; + + size_t *count = private; + if (*count == 0) { + return CS_ACTION_REMOVE; + } + (*count)--; + return CS_ACTION_KEEP; +} + +/** + * Trim the clip store to only retain the specified number of snips. + * + * @cs: The clip store to operate on + * @direction: Whether to remove the N newest or N oldest + * @nr_keep: The number of newest snips to retain + */ +int cs_trim(struct clip_store *cs, enum cs_iter_direction direction, + size_t nr_keep) { + if (nr_keep >= cs->header->nr_snips) { + return 0; // No action needed if we're keeping everything or more + } + + int ret = cs_remove(cs, direction, trim_callback, &nr_keep); + if (ret < 0) { + return ret; + } + + return 0; +} + +/** + * Replace the content and snip for an entry in the clip store, identified by + * its age. + * + * @cs: The clip store to operate on + * @age: The age of the snip to replace, with 0 being the newest + * @direction: Whether to iterate from the oldest to newest or vice versa + * @content: The content to replace this entry with + * @out_hash: Output for the generated hash, or NULL + */ +int cs_replace(struct clip_store *cs, enum cs_iter_direction direction, + size_t age, const char *content, uint64_t *out_hash) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + + if (age >= cs->header->nr_snips) { + return -ERANGE; + } + + size_t idx = direction == CS_ITER_NEWEST_FIRST + ? cs->header->nr_snips - age - 1 + : age; + struct cs_snip *snip = cs->snips + idx; + + char line[CS_SNIP_LINE_SIZE]; + size_t nr_lines = first_line(content, line); + uint64_t old_hash = snip->hash; + uint64_t hash = fnv1a_64_hash(content); + + int ret = cs_content_add(cs, hash, content, CS_DUPE_KEEP_ALL); + if (ret) { + return ret; + } + ret = cs_content_remove(cs, old_hash); + if (ret) { + return ret; + } + cs_snip_update(snip, hash, line, nr_lines); + if (out_hash) { + *out_hash = hash; + } + return 0; +} + +/** + * Get the current number of entries in the clip store. + * + * @cs: The clip store to operate on + * @out_len: Output for the length of the clip store + */ +int cs_len(struct clip_store *cs, size_t *out_len) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + if (guard.status < 0) { + return guard.status; + } + *out_len = cs->header->nr_snips; + return 0; +} diff --git a/src/store.h b/src/store.h new file mode 100644 index 0000000..3a0f3bd --- /dev/null +++ b/src/store.h @@ -0,0 +1,192 @@ +#ifndef CM_STORE_H +#define CM_STORE_H + +#include +#include +#include + +#include "util.h" + +#define CS_SNIP_SIZE 256 /* The size of each struct cs_snip */ +#define CS_SNIP_ALLOC_BATCH 1024 /* How many snips to allocate when growing */ +#define CS_HASH_STR_MAX 17 /* String length of 64bit hex + \0 */ +#define PRI_HASH "%016" PRIX64 + +/** + * A single snip within the clip store. + * + * @hash: A 64-bit hash value associated with the content entry + * @doomed: Used during cs_remove to batch mark entries for removal + * @nr_lines: The number of lines in the content entry + * @line: A character array containing the first salient line, terminated by a + * null byte + */ +#define CS_SNIP_LINE_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2) - sizeof(bool) +struct _packed_ cs_snip { + uint64_t hash; + bool doomed; + uint64_t nr_lines; + char line[CS_SNIP_LINE_SIZE]; +}; + +/** + * The header of the clip store. Must fit within the footprint of a regular + * `cs_snip`. + * + * @nr_snips: The total number of snips in the clip store, excluding the + * header + * @nr_snips_alloc: The total number of allocated snips in the clip store + * that can be used without _cs_file_resize(), excluding the + * header + * @_unused_padding: Padding to match the size of cs_snip + */ +#define CS_HEADER_PADDING_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2) +struct _packed_ cs_header { + uint64_t nr_snips; + uint64_t nr_snips_alloc; + char _unused_padding[CS_HEADER_PADDING_SIZE]; +}; + +static_assert(sizeof(struct cs_snip) == CS_SNIP_SIZE, "cs_snip wrong size"); +static_assert(sizeof(struct cs_snip) == sizeof(struct cs_header), + "cs_header and cs_snip must be the same size"); + +/** + * The main interface to the clip store for the user. + * + * @snip_fd: The file descriptor for the snip file + * @content_dir_fd: The file descriptor for the content directory + * @header: Pointer to the header in the mmapped file + * @snips: Pointer to the beginning of the clip store snips in the mmapped + * file, directly after the header + * @ready: Indicates if the clip store is ready for operations + * @refcount: The reference count for the fd flock + * @local_nr_snips: Our last known header->nr_snips + * @local_nr_snips_alloc: Our last known header->nr_snips_alloc + * @mapped_file_size: The actual size of our current mmap() in bytes + */ +struct clip_store { + /* FDs */ + int snip_fd; + int content_dir_fd; + + /* Pointers inside mmapped snip file */ + struct cs_header *header; + struct cs_snip *snips; + + /* Synchronisation */ + size_t refcount; + size_t local_nr_snips; + size_t local_nr_snips_alloc; + size_t mapped_file_size; + bool ready; +}; + +/** + * Manages the lifecycle of a single clip store lock reference. + * + * @status: The result of trying update our mappings to new header values. If + * less than 0, the lock should not be used. Set to -EBADF on unref. + * @unref: The function to call to unref. This mostly exists to ensure that + * the ref_guard is used and is not optimised by the compiler. + * @cs: A pointer to the clip store structure on which the lock is held. + * + * Usage of this structure involves creating a ref guard instance at the + * beginning of a scope where a lock is needed. Functions which need a lock + * accept a `struct ref_guard *` as their first argument + */ +struct ref_guard { + int status; + struct clip_store *cs; + void (*unref)(struct clip_store *); +}; + +/** + * The memory-mapped content associated with a hash in the content directory. + * + * @data: A pointer to the memory-mapped data + * @fd: The file descriptor of the opened file from which the content is mapped + * @size: The size of the mapped data + */ +struct cs_content { + char *data; + int fd; + off_t size; +}; + +/** + * The direction in which to iterate over snips in the clip store. + * + * @CS_ITER_NEWEST_FIRST: Iterate over the snips starting from the newest. + * @CS_ITER_OLDEST_FIRST: Iterate over the snips starting from the oldest. + */ +enum cs_iter_direction { CS_ITER_NEWEST_FIRST, CS_ITER_OLDEST_FIRST }; + +/** + * Set the bit at position n. + * + * @n: The position of the bit to set + */ +#define BIT(n) (1UL << (n)) + +/** + * The action to take as returned by `cs_remove`'s `should_remove()` predicate + * function. + * + * @CS_ACTION_REMOVE: Remove this snip from the clip store + * @CS_ACTION_KEEP: Keep this snip in the clip store + * @CS_ACTION_STOP: Stop iteration after processing this snip + * + * If neither of CS_ACTION_REMOVE or CS_ACTION_KEEP are specified, the snip is + * kept. + */ +enum cs_remove_action { + CS_ACTION_REMOVE = BIT(0), + CS_ACTION_KEEP = BIT(1), + CS_ACTION_STOP = BIT(2), +}; + +/** + * What to do when there's a duplicate entry. + * + * @CS_DUPE_KEEP_ALL: Keep all duplicate entries. + * @CS_DUPE_KEEP_LAST: Only keep the newest, do not insert duplicate entries. + */ +enum cs_dupe_policy { + CS_DUPE_KEEP_ALL, + CS_DUPE_KEEP_LAST, +}; + +struct ref_guard _must_use_ _nonnull_ cs_ref(struct clip_store *cs); +void _nonnull_ cs_unref(struct clip_store *cs); +void _nonnull_ drop_cs_unref(struct ref_guard *guard); +int _must_use_ _nonnull_ cs_destroy(struct clip_store *cs); +int _must_use_ _nonnull_ cs_init(struct clip_store *cs, int snip_fd, + int content_dir_fd); +void drop_cs_content_unmap(struct cs_content *content); +void drop_cs_destroy(struct clip_store *cs); +int _must_use_ _nonnull_ cs_content_get(struct clip_store *cs, uint64_t hash, + struct cs_content *content); +int _must_use_ _nonnull_n_(1) + cs_make_newest(struct clip_store *cs, uint64_t hash); +int _must_use_ _nonnull_n_(1) + cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash, + enum cs_dupe_policy dupe_policy); +bool _must_use_ _nonnull_ cs_snip_iter(struct ref_guard *guard, + enum cs_iter_direction direction, + struct cs_snip **snip); +int _must_use_ _nonnull_ cs_remove( + struct clip_store *cs, enum cs_iter_direction direction, + enum cs_remove_action (*should_remove)(uint64_t, const char *, void *), + void *private); +int _must_use_ _nonnull_ cs_trim(struct clip_store *cs, + enum cs_iter_direction direction, + size_t nr_keep); +int _must_use_ _nonnull_n_(1, 4) + cs_replace(struct clip_store *cs, enum cs_iter_direction direction, + size_t age, const char *content, uint64_t *out_hash); +int _nonnull_ cs_len(struct clip_store *cs, size_t *out_len); + +size_t _nonnull_ first_line(const char *text, char *out); + +#endif diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..e9cf994 --- /dev/null +++ b/src/util.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include + +#include "config.h" +#include "store.h" +#include "util.h" + +/** + * Write data to a file descriptor, ensuring all bytes are written. + */ +void write_safe(int fd, const char *buf, size_t count) { + while (count > 0) { + ssize_t chunk_size = write(fd, buf, count); + expect(chunk_size >= 0); + buf += chunk_size; + expect(chunk_size <= (ssize_t)count); + count -= (size_t)chunk_size; + } +} + +/** + * Read data from a file descriptor into a buffer safely, ensuring correct + * handling of partial reads. + */ +size_t read_safe(int fd, char *buf, size_t count) { + size_t count_start = count; + while (count > 0) { + ssize_t chunk_size = read(fd, buf, count); + expect(chunk_size >= 0); + if (chunk_size == 0) { // EOF + break; + } + buf += chunk_size; + count -= (size_t)chunk_size; + } + expect(count_start >= count); + return count_start - count; +} + +/** + * Performs safe, bounded string formatting into a buffer. On error or + * truncation, expect() aborts. + */ +size_t snprintf_safe(char *buf, size_t len, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + int needed = vsnprintf(buf, len, fmt, args); + va_end(args); + expect(needed >= 0 && (size_t)needed < len); + return (size_t)needed; +} + +/** + * Runs clipserve to handle selection requests for a hash in the clip store. + */ +#define CLIPSERVE_MAX_ARGS (CM_SEL_MAX * 2 + 3) + +void run_clipserve(uint64_t hash, const struct selection *selections) { + char hash_str[CS_HASH_STR_MAX]; + snprintf(hash_str, sizeof(hash_str), PRI_HASH, hash); + + const char *cmd[CLIPSERVE_MAX_ARGS]; + size_t cmd_idx = 0; + cmd[cmd_idx++] = "clipserve"; + if (selections) { + for (size_t i = 0; i < CM_SEL_MAX; i++) { + if (!selections[i].active) { + continue; + } + expect(cmd_idx + 2 < CLIPSERVE_MAX_ARGS); + cmd[cmd_idx++] = "--selection"; + cmd[cmd_idx++] = selections[i].name; + } + } + cmd[cmd_idx++] = hash_str; + cmd[cmd_idx++] = NULL; + pid_t pid = fork(); + expect(pid >= 0); + + if (pid > 0) { + return; + } + + execvp(cmd[0], (char *const *)cmd); + die("Failed to exec %s: %s\n", cmd[0], strerror(errno)); +} + +/** + * Convert a positive errno value to a negative error code, ensuring a + * non-zero value is returned. + * + * This is needed because clang-tidy and gcc may complain when doing plain + * "return -errno" because the compiler does not know that errno cannot be 0 + * (and thus that later checks with func() == 0 cannot pass in error + * situations). + */ +int negative_errno(void) { return errno > 0 ? -errno : -EINVAL; } + +/** + * Convert a string to an unsigned 64-bit integer in given base, validating the + * format and range of the input. + */ +static int str_to_uint64_base(const char *input, uint64_t *output, int base) { + char *endptr; + errno = 0; + + uint64_t val = strtoull(input, &endptr, base); + if (errno > 0) { + return negative_errno(); + } + if (!endptr || endptr == input || *endptr != 0) { + return -EINVAL; + } + if (val != 0 && input[0] == '-') { + return -ERANGE; + } + + *output = val; + return 0; +} + +/** + * Convert a string to an unsigned 64-bit integer, validating the format and + * range of the input. + */ +int str_to_uint64(const char *input, uint64_t *output) { + return str_to_uint64_base(input, output, 10); +} + +/** + * Convert a hex string to an unsigned 64-bit integer, validating the format + * and range of the input. + */ +int str_to_hex64(const char *input, uint64_t *output) { + return str_to_uint64_base(input, output, 16); +} + +/** + * Check whether debug mode is enabled and cache the result. Used for dbg(). + */ +bool debug_mode_enabled(void) { + static int debug_enabled = -1; + if (debug_enabled == -1) { + const char *dbg_env = getenv("CM_DEBUG"); + debug_enabled = dbg_env && streq(dbg_env, "1"); + } + return debug_enabled; +} + +/** + * Compute a 64-bit FNV-1a hash for a NULL-terminated string. + */ +uint64_t fnv1a_64_hash(const char *buf) { + const uint64_t fnv_offset_basis = 0xcbf29ce484222325ULL; + const uint64_t fnv_prime = 0x100000001b3ULL; + uint64_t hash = fnv_offset_basis; + const uint8_t *src = (const uint8_t *)buf; + while (*src) { + hash ^= *src++; + hash *= fnv_prime; + } + return hash; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..3aa1592 --- /dev/null +++ b/src/util.h @@ -0,0 +1,111 @@ +#ifndef CM_UTIL_H +#define CM_UTIL_H + +#include +#include +#include +#include +#include +#include + +struct selection; + +#define _drop_(x) __attribute__((__cleanup__(drop_##x))) +#define _must_use_ __attribute__((warn_unused_result)) +#define _nonnull_ __attribute__((nonnull)) +#define _nonnull_n_(...) __attribute__((nonnull(__VA_ARGS__))) +#define _noreturn_ __attribute__((noreturn)) +#define _packed_ __attribute__((packed)) +#define _printf_(a, b) __attribute__((__format__(printf, a, b))) +#define _unused_ __attribute__((__unused__)) +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#ifndef __cplusplus + #define static_assert _Static_assert +#endif + +#define UINT64_MAX_STRLEN 20 // (1 << 64) - 1 is 20 digits wide + +#define streq(a, b) (strcmp((a), (b)) == 0) +#define strceq(a, b) (strcasecmp((a), (b)) == 0) +#define strnull(s) (s) ? (s) : "[null]" + +#define arrlen(x) \ + (__builtin_choose_expr( \ + !__builtin_types_compatible_p(typeof(x), typeof(&*(x))), \ + sizeof(x) / sizeof((x)[0]), (void)0 /* decayed, compile error */)) + +#define _msg(level, fmt, ...) fprintf(stderr, level ": " fmt, ##__VA_ARGS__) +#define warn(fmt, ...) _msg("WARN", fmt, ##__VA_ARGS__) +#define _die(dump, fmt, ...) \ + do { \ + _msg("FATAL", fmt, ##__VA_ARGS__); \ + if (dump) { \ + abort(); \ + } \ + exit(1); \ + } while (0) +#define die(fmt, ...) _die(0, fmt, ##__VA_ARGS__) +#define die_on(cond, fmt, ...) \ + do { \ + if (unlikely(cond)) { \ + die(fmt, ##__VA_ARGS__); \ + } \ + } while (0) +#define expect(x) \ + do { \ + if (!likely(x)) { \ + _die(1, "!(%s) at %s:%s:%d\n", #x, __FILE__, __func__, __LINE__); \ + } \ + } while (0) + +#define dbg(fmt, ...) \ + do { \ + if (debug_mode_enabled()) { \ + fprintf(stderr, "%s:%ld:%s:%s:%d: " fmt, prog_name, \ + (long)getpid(), __FILE__, __func__, __LINE__, \ + ##__VA_ARGS__); \ + } \ + } while (0) + +void _nonnull_ write_safe(int fd, const char *buf, size_t count); +size_t _nonnull_ read_safe(int fd, char *buf, size_t count); +size_t _printf_(3, 4) + snprintf_safe(char *buf, size_t len, const char *fmt, ...); + +void run_clipserve(uint64_t hash, const struct selection *selections); +uint64_t _nonnull_ fnv1a_64_hash(const char *buf); + +/** + * __attribute__((cleanup)) functions + */ +#define DEFINE_DROP_FUNC_PTR(type, func) \ + static inline void drop_##func(type *p) { func(p); } +#define DEFINE_DROP_FUNC(type, func) \ + static inline void drop_##func(type *p) { \ + if (*p) \ + func(*p); \ + } +#define DEFINE_DROP_FUNC_VOID(func) \ + static inline void drop_##func(void *p) { \ + void **pp = p; \ + if (*pp) \ + func(*pp); \ + } + +static inline void drop_close(int *fd) { + if (*fd >= 0) { + close(*fd); + } +} + +DEFINE_DROP_FUNC_VOID(free) +DEFINE_DROP_FUNC(FILE *, fclose) +DEFINE_DROP_FUNC(DIR *, closedir) + +int _must_use_ negative_errno(void); +int _nonnull_ str_to_uint64(const char *input, uint64_t *output); +int _nonnull_ str_to_hex64(const char *input, uint64_t *output); +bool debug_mode_enabled(void); + +#endif diff --git a/src/x.c b/src/x.c new file mode 100644 index 0000000..18809f3 --- /dev/null +++ b/src/x.c @@ -0,0 +1,121 @@ +#include +#include +#include + +#include "x.h" + +/** + * Fetch the title of the window with the specified window ID. + */ +char *get_window_title(Display *dpy, Window owner) { + Atom props[] = {XInternAtom(dpy, "_NET_WM_NAME", False), XA_WM_NAME}; + Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); + Atom actual_type; + int format; + unsigned long nr_items, bytes_after; + unsigned char *prop = NULL; + + for (size_t i = 0; i < arrlen(props); i++) { + if (XGetWindowProperty(dpy, owner, props[i], 0, (~0L), False, + (props[i] == XA_WM_NAME) ? AnyPropertyType + : utf8_string, + &actual_type, &format, &nr_items, &bytes_after, + &prop) == Success && + prop) { + return (char *)prop; + } + } + return NULL; +} + +/** + * Certain X11 operations may fail in expected ways. For example, when + * attempting to interact with a window that has been closed. This handler + * avoids the application terminating in such cases. + * + * The cppcheck suppression is for a false positive: this is a callback and + * cannot be changed. + */ +// cppcheck-suppress [constParameterPointer,unmatchedSuppression] +int xerror_handler(Display *dpy _unused_, XErrorEvent *ee) { + if (ee->error_code == BadWindow || + (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) || + (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) || + (ee->request_code == X_PolyFillRectangle && + ee->error_code == BadDrawable) || + (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) || + (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) || + (ee->request_code == X_GrabButton && ee->error_code == BadAccess) || + (ee->request_code == X_GrabKey && ee->error_code == BadAccess) || + (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) { + return 0; + } + die("X error with request code=%d, error code=%d\n", ee->request_code, + ee->error_code); +} + +#define FALLBACK_CHUNK_BYTES 4 * 1024 + +/** + * Return a quarter of the maximum request size in bytes. + * + * XMaxRequestSize and XExtendedMaxRequestSize return units of 4-byte words, so + * the word count already equals 1/4 of the maximum request size in bytes. + */ +size_t get_incr_threshold(Display *dpy) { + size_t max_request_words = XExtendedMaxRequestSize(dpy); + if (max_request_words == 0) { + max_request_words = XMaxRequestSize(dpy); + } + return max_request_words ? max_request_words : FALLBACK_CHUNK_BYTES; +} + +/** + * Calculate an appropriate INCR payload size. + * + * The request size word count is a sensible threshold for deciding when to use + * INCR at all, but using that same value as the payload of each + * XChangeProperty request is too aggressive in practice. + * + * This is an interoperability constraint, not a protocol-limit one, because: + * + * - XMaxRequestSize/XExtendedMaxRequestSize only tell us what request size the + * X server will accept. + * - INCR has no chunk-size negotiation, so if a requestor dislikes large + * payloads it can simply stop deleting the property, leaving the sender + * waiting forever. + * + * Other implementations do not agree on a single rule here either: xclip uses + * a heuristic, some clipboard managers use a fixed cap, and Emacs couples + * large chunks with a timeout. + */ +size_t get_chunk_size(Display *dpy) { + size_t incr_threshold = get_incr_threshold(dpy); + return incr_threshold / 4 ? incr_threshold / 4 : FALLBACK_CHUNK_BYTES; +} + +/** + * Add a new INCR transfer to the active list. + */ +void it_add(struct incr_transfer **it_list, struct incr_transfer *it) { + if (*it_list) { + (*it_list)->prev = it; + } + it->next = *it_list; + it->prev = NULL; + *it_list = it; +} +/** + * Remove an INCR transfer from the active list. + */ +void it_remove(struct incr_transfer **it_list, struct incr_transfer *it) { + if (it->prev) { + it->prev->next = it->next; + } + if (it->next) { + it->next->prev = it->prev; + } + if (*it_list == it) { + *it_list = it->next; + } +} diff --git a/src/x.h b/src/x.h new file mode 100644 index 0000000..a56f6d1 --- /dev/null +++ b/src/x.h @@ -0,0 +1,34 @@ +#ifndef CM_X_H +#define CM_X_H + +#include + +#include "util.h" + +DEFINE_DROP_FUNC_VOID(XFree) + +size_t _nonnull_ get_incr_threshold(Display *dpy); +size_t _nonnull_ get_chunk_size(Display *dpy); +char _nonnull_ *get_window_title(Display *dpy, Window owner); +int xerror_handler(Display *dpy _unused_, XErrorEvent *ee); + +struct incr_transfer { + struct incr_transfer *next; + struct incr_transfer *prev; + Window requestor; + Atom property; + Atom target; + int format; + char *data; + size_t data_size; + size_t data_capacity; + size_t offset; +}; + +#define it_dbg(it, fmt, ...) \ + dbg("[incr 0x%lx] " fmt, (unsigned long)(it)->requestor, ##__VA_ARGS__) +void _nonnull_ it_add(struct incr_transfer **it_list, struct incr_transfer *it); +void _nonnull_ it_remove(struct incr_transfer **it_list, + struct incr_transfer *it); + +#endif diff --git a/tests/test_store.c b/tests/test_store.c new file mode 100644 index 0000000..a1d598e --- /dev/null +++ b/tests/test_store.c @@ -0,0 +1,848 @@ +#undef NDEBUG + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/store.h" +#include "../src/util.h" + +#define COL_NORMAL "\x1B[0m" +#define COL_GREEN "\x1B[32m" +#define COL_RED "\x1B[31m" + +#define t_assert(test) \ + do { \ + if (!(test)) { \ + printf(" %s[FAIL:%d] %s%s\n", COL_RED, __LINE__, #test, \ + COL_NORMAL); \ + return false; \ + } \ + printf(" %s[PASS:%d] %s%s\n", COL_GREEN, __LINE__, #test, \ + COL_NORMAL); \ + } while (0) + +#define t_run(test) \ + do { \ + printf("%s:\n", #test); \ + bool ret = test(); \ + printf("\n"); \ + if (!ret) { \ + return ret; \ + } \ + } while (0) + +#define TEST_SNIP_FILE "/clip_store_snip_test" +#define TEST_CONTENT_DIR "/dev/shm/clip_store_content_dir_test" + +static int create_test_snip_fd(void) { + shm_unlink(TEST_SNIP_FILE); + int snip_fd = shm_open(TEST_SNIP_FILE, O_RDWR | O_CREAT | O_EXCL, 0600); + assert(snip_fd >= 0); + return snip_fd; +} + +static void drop_remove_test_snip_fd(int *snip_fd) { + close(*snip_fd); + int ret = shm_unlink(TEST_SNIP_FILE); + assert(ret == 0); +} + +static void _drop_remove_test_content_dir_fd(int *dir_fd_ptr) { + int dir_fd = *dir_fd_ptr; + DIR *dir = fdopendir(dir_fd); + if (dir == NULL) { + close(dir_fd); + return; + } + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (streq(entry->d_name, ".") || streq(entry->d_name, "..")) { + continue; + } + + if (entry->d_type == DT_REG) { + int ret = unlinkat(dir_fd, entry->d_name, 0); + assert(ret == 0); + } else if (entry->d_type == DT_DIR) { + int subdir_fd = + openat(dir_fd, entry->d_name, O_RDONLY | O_DIRECTORY); + assert(subdir_fd >= 0); + _drop_remove_test_content_dir_fd(&subdir_fd); + int ret = unlinkat(dir_fd, entry->d_name, AT_REMOVEDIR); + assert(ret == 0); + } + } + + closedir(dir); +} + +static void drop_remove_test_content_dir_fd(int *dir_fd_ptr) { + _drop_remove_test_content_dir_fd(dir_fd_ptr); + int ret = rmdir(TEST_CONTENT_DIR); + assert(ret == 0); +} + +static void remove_test_content_dir(const char *path) { + int dir_fd = open(path, O_RDONLY); + if (dir_fd < 0) { + assert(errno == ENOENT); + return; + } + drop_remove_test_content_dir_fd(&dir_fd); +} + +static int create_test_content_dir_fd(void) { + remove_test_content_dir(TEST_CONTENT_DIR); + int ret = mkdir(TEST_CONTENT_DIR, 0700); + assert(ret == 0); + int dir_fd = open(TEST_CONTENT_DIR, O_RDONLY); + assert(dir_fd >= 0); + return dir_fd; +} + +static int count_test_snip_maps(void) { +#ifdef __linux__ + _drop_(fclose) FILE *maps = fopen("/proc/self/maps", "r"); + assert(maps != NULL); + + char line[4096]; + int count = 0; + while (fgets(line, sizeof(line), maps) != NULL) { + if (strstr(line, TEST_SNIP_FILE) != NULL) { + count++; + } + } + + return count; +#else + return -1; +#endif +} + +/* Test callables */ +static enum cs_remove_action remove_if_five(uint64_t hash, const char *line, + void *private) { + (void)hash; + size_t *count = private; + enum cs_remove_action ret = 0; + + if (*count == 0) { + ret |= CS_ACTION_STOP; + } + (*count)--; + + if (line[0] == '5') { + ret |= CS_ACTION_REMOVE; + } + + return ret; +} + +static struct clip_store setup_test(void) { + struct clip_store cs; + int snip_fd = create_test_snip_fd(); + int content_dir_fd = create_test_content_dir_fd(); + int ret = cs_init(&cs, snip_fd, content_dir_fd); + assert(ret == 0); + return cs; +} + +static void drop_teardown_test(struct clip_store *cs) { + int snip_fd = cs->snip_fd; + int content_dir_fd = cs->content_dir_fd; + int ret = cs_destroy(cs); + assert(ret == 0); + close(snip_fd); + shm_unlink(TEST_SNIP_FILE); + close(content_dir_fd); + remove_test_content_dir(TEST_CONTENT_DIR); +} + +static void add_ten_snips(struct clip_store *cs) { + for (char i = 0; i < 10; i++) { + char num[8]; + snprintf(num, sizeof(num), "%d", i); + int ret = cs_add(cs, num, NULL, CS_DUPE_KEEP_ALL); + assert(ret == 0); + } +} + +/* Tests */ + +static bool test__cs_init(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + t_assert(cs.snip_fd >= 0); + t_assert(cs.content_dir_fd >= 0); + + /* Check header fields were set up and are correct */ + struct stat st; + t_assert(fstat(cs.snip_fd, &st) == 0); + t_assert(st.st_size == CS_SNIP_SIZE); + t_assert(cs.header->nr_snips_alloc == 0); + + return true; +} + +static bool test__cs_init__bad_size(void) { + _drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd(); + _drop_(remove_test_content_dir_fd) int content_dir_fd = + create_test_content_dir_fd(); + t_assert(ftruncate(snip_fd, CS_SNIP_SIZE - 1) == 0); + struct clip_store cs; + t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL); + + return true; +} + +static bool test__cs_init__bad_size_aligned(void) { + _drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd(); + _drop_(remove_test_content_dir_fd) int content_dir_fd = + create_test_content_dir_fd(); + t_assert(ftruncate(snip_fd, CS_SNIP_SIZE * 2) == 0); + struct clip_store cs; + t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL); + + return true; +} + +static bool test__cs_add(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + for (char i = 0; i < 10; i++) { + char num[8]; + snprintf(num, sizeof(num), "%d", i); + + uint64_t hash; + int ret = cs_add(&cs, num, &hash, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + + _drop_(cs_content_unmap) struct cs_content content; + ret = cs_content_get(&cs, hash, &content); + t_assert(ret == 0); + t_assert(strncmp(content.data, num, content.size) == 0); + } + + t_assert(cs.header->nr_snips == 10); + + return true; +} + +static bool test__cs_content_get__keeps_fd_open(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash; + int ret = cs_add(&cs, "fd lifecycle", &hash, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + + struct cs_content content; + ret = cs_content_get(&cs, hash, &content); + t_assert(ret == 0); + t_assert(fcntl(content.fd, F_GETFD) == 0); + + int unrelated_fd = open("/dev/null", O_RDONLY); + t_assert(unrelated_fd >= 0); + + drop_cs_content_unmap(&content); + + t_assert(fcntl(unrelated_fd, F_GETFD) == 0); + close(unrelated_fd); + + return true; +} + +static bool test__cs_snip_iter(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + + add_ten_snips(&cs); + + struct cs_snip *snip = NULL; + int last_num = 9; + while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { + _drop_(cs_content_unmap) struct cs_content content; + int ret = cs_content_get(&cs, snip->hash, &content); + t_assert(ret == 0); + + /* add_ten_snips adds from 0-9, we should get the last one first */ + int num = content.data[0] - '0'; + t_assert(num == last_num--); + } + + snip = NULL; + last_num = 0; + while (cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip)) { + _drop_(cs_content_unmap) struct cs_content content; + int ret = cs_content_get(&cs, snip->hash, &content); + t_assert(ret == 0); + + int num = content.data[0] - '0'; + t_assert(num == last_num++); + } + + return true; +} + +static bool test__cs_remove(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + + add_ten_snips(&cs); + + size_t nr_iter = 1; + t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) == + 0); + t_assert(cs.header->nr_snips == 10); + nr_iter = SIZE_MAX; + t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) == + 0); + t_assert(cs.header->nr_snips == 9); + + return true; +} + +static bool test__cs_trim(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + + add_ten_snips(&cs); + + struct cs_snip *snip = NULL; + while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) + ; + uint64_t oldest_hash = snip->hash; + + t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 3) == 0); + t_assert(cs.header->nr_snips == 3); + + /* Check we kept the most recently added ones */ + int last_num = 9; + snip = NULL; + while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { + _drop_(cs_content_unmap) struct cs_content content; + int ret = cs_content_get(&cs, snip->hash, &content); + t_assert(ret == 0); + + int num = content.data[0] - '0'; + t_assert(num == last_num--); + } + + /* Check the oldest snip hash is gone */ + _drop_(cs_content_unmap) struct cs_content content; + t_assert(cs_content_get(&cs, oldest_hash, &content) == -ENOENT); + + return true; +} + +static bool test__cs_replace(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + + add_ten_snips(&cs); + + const char *new = "new"; + + int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 1, new, NULL); + t_assert(ret == 0); + + struct cs_snip *snip = NULL; + bool i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); + t_assert(i_ret == true); + t_assert(streq(snip->line, "9")); + i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); + t_assert(i_ret == true); + t_assert(streq(snip->line, new)); + + _drop_(cs_content_unmap) struct cs_content first_content; + ret = cs_content_get(&cs, snip->hash, &first_content); + t_assert(ret == 0); + t_assert(strncmp(first_content.data, new, first_content.size) == 0); + + /* No other clips should be affected */ + int last_num = 7; + while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { + _drop_(cs_content_unmap) struct cs_content content; + ret = cs_content_get(&cs, snip->hash, &content); + t_assert(ret == 0); + + int num = content.data[0] - '0'; + t_assert(num == last_num--); + } + + return true; +} + +static bool test__reuse_cs(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + add_ten_snips(&cs); + t_assert(cs_destroy(&cs) == 0); + + t_assert(cs_init(&cs, cs.snip_fd, cs.content_dir_fd) == 0); + t_assert(cs.header->nr_snips == 10); + + return true; +} + +static bool test__cs_add__exceeds_snip_line_size(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + /* Construct a string that exceeds CS_SNIP_LINE_SIZE */ + char long_content[CS_SNIP_LINE_SIZE + 100]; + memset(long_content, 'A', sizeof(long_content)); + long_content[sizeof(long_content) - 1] = '\0'; + + int ret = cs_add(&cs, long_content, NULL, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + + struct cs_snip *snip = NULL; + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + bool found = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); + t_assert(found); + t_assert(strlen(snip->line) == CS_SNIP_LINE_SIZE - 1); + + return true; +} + +static bool test__cs_trim__when_empty(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + int ret = cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 0); + + return true; +} + +static bool test__cs_remove___empty(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + /* Attempt to remove any entry, if present */ + size_t dummy = 0; + int ret = cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &dummy); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 0); + + return true; +} + +static bool test__cs_add__around_alloc_batch_threshold(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + for (size_t i = 0; i < CS_SNIP_ALLOC_BATCH - 1; i++) { + int ret = cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL); + assert(ret == 0); + } + t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH - 1); + + /* Add one more entry to exceed the batch threshold */ + t_assert(cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL) == 0); + t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH); + t_assert(cs.header->nr_snips_alloc >= CS_SNIP_ALLOC_BATCH); + + return true; +} + +static bool test__cs_trim__no_remove_when_still_referenced(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash; + for (size_t i = 0; i < 2; i++) { + int ret = cs_add(&cs, "test content", &hash, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + } + + _drop_(cs_content_unmap) struct cs_content content; + + t_assert(cs.header->nr_snips == 2); + t_assert(cs_content_get(&cs, hash, &content) == 0); + + t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 1) == 0); + t_assert(cs.header->nr_snips == 1); + t_assert(cs_content_get(&cs, hash, &content) == 0); + + t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0) == 0); + t_assert(cs.header->nr_snips == 0); + t_assert(cs_content_get(&cs, hash, &content) == -ENOENT); + + return true; +} + +static bool test__cs_replace__out_of_bounds(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + add_ten_snips(&cs); + + int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 10, "test content", NULL); + t_assert(ret == -ERANGE); + + return true; +} + +static bool test__cs_replace__add_fail_keeps_old_entry(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + const char *old_content = "OLD_CONTENT"; + uint64_t old_hash; + int ret = cs_add(&cs, old_content, &old_hash, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + + const char *new_content = "new"; + uint64_t new_hash = fnv1a_64_hash(new_content); + + char hash_dir[CS_HASH_STR_MAX]; + snprintf(hash_dir, sizeof(hash_dir), PRI_HASH, new_hash); + ret = mkdirat(cs.content_dir_fd, hash_dir, 0700); + t_assert(ret == 0); + + char fake_path[CS_HASH_STR_MAX + 3]; + snprintf(fake_path, sizeof(fake_path), "%s/1", hash_dir); + _drop_(close) int fake_fd = openat(cs.content_dir_fd, fake_path, + O_WRONLY | O_CREAT | O_TRUNC, 0600); + t_assert(fake_fd >= 0); + t_assert(write(fake_fd, "X", 1) == 1); + + ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, new_content, NULL); + t_assert(ret == -EFAULT); + + _drop_(cs_content_unmap) struct cs_content old_after; + ret = cs_content_get(&cs, old_hash, &old_after); + t_assert(ret == 0); + t_assert((size_t)old_after.size == strlen(old_content)); + t_assert(strncmp(old_after.data, old_content, (size_t)old_after.size) == 0); + + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + struct cs_snip *snip = NULL; + t_assert(cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)); + t_assert(snip->hash == old_hash); + t_assert(streq(snip->line, old_content)); + + return true; +} + +static bool test__cs_snip__correct_nr_lines(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + add_ten_snips(&cs); + + /* No need to do exhaustive ones, they're done in test__first_line_* */ + uint64_t hash; + struct cs_snip *snip = NULL; + int ret = + cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, "one\ntwo\nthree", &hash); + + t_assert(ret == 0); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + t_assert(cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)); + t_assert(snip->hash == hash); + t_assert(snip->nr_lines == 3); + + return true; +} + +static bool test__first_line__empty(void) { + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("", line); + printf("%s\n", line); + t_assert(streq(line, "")); + t_assert(num_lines == 0); + num_lines = first_line("\n", line); + t_assert(streq(line, "")); + t_assert(num_lines == 1); + + num_lines = first_line("\n\n\n", line); + t_assert(streq(line, "")); + t_assert(num_lines == 3); + return true; +} + +static bool test__first_line__only_one(void) { + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("Foo bar\n", line); + t_assert(streq(line, "Foo bar")); + t_assert(num_lines == 1); + return true; +} + +static bool test__first_line__multiple(void) { + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("Foo bar\nbaz\nqux\n", line); + t_assert(streq(line, "Foo bar")); + t_assert(num_lines == 3); + return true; +} + +static bool test__first_line__no_final_newline(void) { + /* If the last line didn't end with a newline, still count it */ + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("Foo bar", line); + t_assert(streq(line, "Foo bar")); + t_assert(num_lines == 1); + num_lines = first_line("Foo bar\nbaz", line); + t_assert(streq(line, "Foo bar")); + t_assert(num_lines == 2); + return true; +} + +static bool test__first_line__ignore_blank_lines(void) { + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("\n\n\nFoo bar\n\n\n", line); + t_assert(streq(line, "Foo bar")); + t_assert(num_lines == 6); + return true; +} + +static bool test__first_line__unicode(void) { + char line[CS_SNIP_LINE_SIZE]; + size_t num_lines = first_line("道", line); + t_assert(streq(line, "道")); + t_assert(num_lines == 1); + num_lines = first_line("道\n", line); + t_assert(streq(line, "道")); + t_assert(num_lines == 1); + num_lines = first_line("道\n非", line); + t_assert(streq(line, "道")); + t_assert(num_lines == 2); + return true; +} + +static bool test__synchronisation(void) { + _drop_(remove_test_snip_fd) int snip_fd1 = create_test_snip_fd(); + _drop_(remove_test_content_dir_fd) int content_dir_fd1 = + create_test_content_dir_fd(); + _drop_(close) int snip_fd2 = dup(snip_fd1); + _drop_(close) int content_dir_fd2 = dup(content_dir_fd1); + + assert(snip_fd2 >= 0 && content_dir_fd2 >= 0); + + struct clip_store cs1, cs2; + int ret = cs_init(&cs1, snip_fd1, content_dir_fd1); + t_assert(ret == 0); + ret = cs_init(&cs2, snip_fd2, content_dir_fd2); + t_assert(ret == 0); + + uint64_t hash; + ret = cs_add(&cs1, "test content", &hash, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + + bool found = false; + struct cs_snip *snip = NULL; + _drop_(cs_unref) struct ref_guard guard_cs2 = cs_ref(&cs2); + while (cs_snip_iter(&guard_cs2, CS_ITER_NEWEST_FIRST, &snip)) { + if (snip->hash == hash) { + found = true; + break; + } + } + t_assert(found); + + ret = cs_trim(&cs2, CS_ITER_NEWEST_FIRST, 0); + t_assert(ret == 0); + + found = false; + snip = NULL; + _drop_(cs_unref) struct ref_guard guard_cs1 = cs_ref(&cs1); + while (cs_snip_iter(&guard_cs1, CS_ITER_NEWEST_FIRST, &snip)) { + found = true; + } + t_assert(!found); + + t_assert(cs_destroy(&cs1) == 0); + t_assert(cs_destroy(&cs2) == 0); + + return true; +} + +static bool test__cs_trim__regrow_does_not_leak_mapping(void) { + struct clip_store cs = setup_test(); + + bool added = true; + for (int i = 0; i < 1500; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "clip-%d", i); + if (cs_add(&cs, buf, NULL, CS_DUPE_KEEP_ALL) != 0) { + added = false; + break; + } + } + t_assert(added); + + int maps_after_growth = count_test_snip_maps(); + if (maps_after_growth < 0) { + drop_teardown_test(&cs); + return true; + } + t_assert(maps_after_growth == 1); + + t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0) == 0); + t_assert(count_test_snip_maps() == 1); + + t_assert(cs_add(&cs, "regrow", NULL, CS_DUPE_KEEP_ALL) == 0); + t_assert(count_test_snip_maps() == 1); + + int snip_fd = cs.snip_fd; + int content_dir_fd = cs.content_dir_fd; + t_assert(cs_destroy(&cs) == 0); + t_assert(count_test_snip_maps() == 0); + + close(snip_fd); + shm_unlink(TEST_SNIP_FILE); + close(content_dir_fd); + remove_test_content_dir(TEST_CONTENT_DIR); + + return true; +} + +static bool test__cs_add__dupe_keep_all(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash1, hash2; + int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + t_assert(hash1 == hash2); + t_assert(cs.header->nr_snips == 2); + + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + struct cs_snip *snip = NULL; + bool iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip); + t_assert(iter_ret == true); + t_assert(snip->hash == hash1); + iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip); + t_assert(iter_ret == true); + t_assert(snip->hash == hash2); + + return true; +} + +static bool test__cs_add__dupe_keep_last(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash1, hash2, hash3; + int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_LAST); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 1); + ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_LAST); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 1); + ret = cs_add(&cs, "duplicate", &hash3, CS_DUPE_KEEP_LAST); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 1); + t_assert(hash1 == hash2); + t_assert(hash1 == hash3); + + return true; +} + +/* After adding a duplicate entry, ensure the duplicate is moved to the newest + * slot while other entries remain in order. */ +static bool test__cs_add__dupe_keep_last_with_multiple_entries(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash_a, hash_dup; + int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + ret = cs_add(&cs, "duplicate", &hash_dup, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + ret = cs_add(&cs, "B", NULL, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 3); + /* Now add a duplicate entry with KEEP_LAST which should move the duplicate + * to the newest slot */ + ret = cs_add(&cs, "duplicate", NULL, CS_DUPE_KEEP_LAST); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 3); + _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); + struct cs_snip *snip = NULL; + bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); + t_assert(iter_ret == true); + t_assert(snip->hash == hash_dup); + + return true; +} + +static bool check_order(struct clip_store *cs, uint64_t *order, size_t len) { + _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); + struct cs_snip *snip = NULL; + for (size_t i = 0; i < len; ++i) { + bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); + t_assert(iter_ret == true); + t_assert(snip->hash == order[i]); + } + return 0; +} + +/* After calling cs_make_newest make sure the entry is at the newest slot while + * other entries remain in order. */ +static bool test__cs_make_newest(void) { + _drop_(teardown_test) struct clip_store cs = setup_test(); + + uint64_t hash_a, hash_b, hash_c; + int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + ret = cs_add(&cs, "B", &hash_b, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + ret = cs_add(&cs, "C", &hash_c, CS_DUPE_KEEP_ALL); + t_assert(ret == 0); + t_assert(cs.header->nr_snips == 3); + + uint64_t order_before[3] = {hash_c, hash_b, hash_a}; + ret = check_order(&cs, order_before, 3); + t_assert(ret == 0); + /* Now the order should change to ["A", "C", "B"] */ + ret = cs_make_newest(&cs, hash_a); + t_assert(ret == 0); + uint64_t order_after[3] = {hash_a, hash_c, hash_b}; + ret = check_order(&cs, order_after, 3); + t_assert(ret == 0); + + return true; +} + +int main(void) { + t_run(test__cs_init); + t_run(test__cs_init__bad_size); + t_run(test__cs_init__bad_size_aligned); + t_run(test__cs_add); + t_run(test__cs_content_get__keeps_fd_open); + t_run(test__cs_snip_iter); + t_run(test__cs_remove); + t_run(test__cs_trim); + t_run(test__cs_replace); + t_run(test__reuse_cs); + t_run(test__cs_add__exceeds_snip_line_size); + t_run(test__cs_trim__when_empty); + t_run(test__cs_remove___empty); + t_run(test__cs_add__around_alloc_batch_threshold); + t_run(test__cs_replace__out_of_bounds); + t_run(test__cs_replace__add_fail_keeps_old_entry); + t_run(test__synchronisation); + t_run(test__cs_trim__regrow_does_not_leak_mapping); + t_run(test__cs_trim__no_remove_when_still_referenced); + t_run(test__cs_snip__correct_nr_lines); + t_run(test__first_line__empty); + t_run(test__first_line__only_one); + t_run(test__first_line__multiple); + t_run(test__first_line__no_final_newline); + t_run(test__first_line__ignore_blank_lines); + t_run(test__first_line__unicode); + t_run(test__cs_add__dupe_keep_all); + t_run(test__cs_add__dupe_keep_last); + t_run(test__cs_add__dupe_keep_last_with_multiple_entries); + t_run(test__cs_make_newest); + + return 0; +} diff --git a/tests/x_integration_tests b/tests/x_integration_tests new file mode 100755 index 0000000..f24de8b --- /dev/null +++ b/tests/x_integration_tests @@ -0,0 +1,350 @@ +#!/usr/bin/env bash + +set -exE -o errtrace + +dump_cache() { + printf '\nTest failure: contents of cache:\n\n' >&2 + cat "$l_out" >&2 +} + +expect_fail() { + set +e + "$@" + local status=$? + set -e + (( status != 0 )) +} + +if ! (( NO_PID_NAMESPACE )) && (( EUID )); then + export _UNSHARED=1 + exec unshare --user --map-root-user --pid --mount --fork --mount-proc "$0" "$@" +fi + +if (( _UNSHARED )); then + # Get our own tmp + mount -t tmpfs unshared_tmp /tmp +fi + +cd "${0%/*}"/.. +make clean debug +cd src + +export PATH=$PWD:$PATH + +expect_fail env CM_CONFIG=relative CM_DIR="$(mktemp -d)" clipmenu +expect_fail env CM_DIR=relative CM_CONFIG="$(mktemp)" clipmenu + +export CM_CONFIG=$(mktemp) +export CM_DIR=$(mktemp -d) +export CM_DEBUG=1 +export ASAN_OPTIONS='halt_on_error=1:abort_on_error=1' +export UBSAN_OPTIONS='halt_on_error=1:abort_on_error=1' +uid=$(id -u) +[[ "$(clipctl version)" == 7 ]] +[[ "$(clipctl cache-dir)" == "$CM_DIR/clipmenu.7.$uid" ]] + +launcher=$(mktemp) +l_out=$(mktemp) + +trap dump_cache ERR + +cat > "$launcher" << EOF +#!/bin/bash + +cat > "$l_out" + +if [[ \$SELECT ]]; then + printf '%s\\n' "\$SELECT" +fi +EOF +chmod a+x "$launcher" +export CM_LAUNCHER="$launcher" + +tilde_home=$(mktemp -d) +tilde_dir="$tilde_home/clipmenu_tilde" +mkdir -p "$tilde_dir" +HOME="$tilde_home" CM_DIR="~/clipmenu_tilde" CM_CONFIG="$(mktemp)" \ + clipmenu || true +[[ -d "$tilde_dir/clipmenu.7.$uid" ]] +rm -rf "$tilde_home" + +tilde_conf_home=$(mktemp -d) +printf 'max_clips nope\n' > "$tilde_conf_home/bad.conf" +err=$(HOME="$tilde_conf_home" CM_CONFIG="~/bad.conf" CM_DIR="$(mktemp -d)" \ + clipmenu 2>&1 || true) +[[ "$err" == *"for max_clips"* ]] +rm -rf "$tilde_conf_home" + +kill_background_jobs() { + local -a bg + readarray -t bg < <(jobs -p) + (( ${#bg[@]} )) && kill -- "${bg[@]}" 2>/dev/null +} + +primary() { + printf '%s' "${1?}" | xsel -p +} + +trap 'kill_background_jobs' EXIT + +settle() { + sleep 0.2 +} + +long_settle() { + sleep 2 +} + +check_nr_clips() { + if [[ $SELECT ]]; then + clipmenu + else + # On no selection, we exit 1. Avoid masking real problems by only doing + # it when no selection is specified. + clipmenu || true + fi + (( $(wc -l < "$l_out") == "${1?}" )) +} + +wait_nr_clips() { + local want=${1?} + local deadline=$((SECONDS + 5)) + + while (( SECONDS < deadline )); do + if check_nr_clips "$want"; then + return 0 + fi + sleep 0.1 + done + + check_nr_clips "$want" +} + +if ! (( USE_CURRENT_DISPLAY )); then + export DISPLAY=:1911 + Xvfb "$DISPLAY" & + sleep 2 +fi + +# Clear selections +xsel -bc +xsel -pc +xsel -sc + +clipmenud & +clipmenud_pid=$! +settle + +# Should be empty +check_nr_clips 0 + +primary foo +settle + +check_nr_clips 1 + +# Put the same one repeatedly, should ignore it +primary foo +primary foo +primary foo +settle + +settle +check_nr_clips 1 +[[ "$(< "$l_out")" == "[1] foo" ]] + +# Put possible partials, should update it +primary fooa +primary fooab +primary fooabc + +# Should have replaced the old one +settle +check_nr_clips 1 +[[ "$(< "$l_out")" == "[1] fooabc" ]] + +primary fooab +primary fooa +primary foo + +# Should have replaced the old one +settle +check_nr_clips 1 +[[ "$(< "$l_out")" == "[1] foo" ]] + +# Put some more content on the clipboard for testing +primary bar +primary baz +settle + +check_nr_clips 3 + +# Nothing gets deleted, but we recognise the right clips +[[ $(clipdel a) == $'bar\nbaz' ]] +check_nr_clips 3 + +# Likewise but inversion +[[ $(clipdel -v a) == foo ]] +check_nr_clips 3 + +# Real delete with inversion +[[ $(clipdel -dv a) == foo ]] +check_nr_clips 2 + +# Test literal match +primary '*foo' +settle +check_nr_clips 3 + +[[ $(clipdel -dF '*') == '*foo' ]] +check_nr_clips 2 + +# Test -n +primary latest +settle +check_nr_clips 3 + +# Should delete the newest entry +[[ $(clipdel -dn 1) == 'latest' ]] +check_nr_clips 2 + +# Test -N +primary wibble +primary wobble +settle +check_nr_clips 4 + +# Should delete everything other than the oldest 2 entries +[[ $(clipdel -dvN 2) == $'wibble\nwobble' ]] +check_nr_clips 2 + +# Test clipdelmenu +primary test1 +primary test2 +primary test3 +settle +check_nr_clips 5 + +# Should delete the selected entry +SELECT='[4] test2' clipdelmenu +settle +check_nr_clips 4 + +# Verify test2 is gone and test1, test3 remain +clipmenu || true +[[ "$(< "$l_out")" == *"test1"* ]] +[[ "$(< "$l_out")" == *"test3"* ]] +[[ "$(< "$l_out")" != *"test2"* ]] + +# Delete the others +SELECT='[3] test1' clipdelmenu +SELECT='[3] test3' clipdelmenu + +# Test multi-select deletion +primary multi1 +primary multi2 +settle +check_nr_clips 4 + +SELECT=$'[3] multi1\n[4] multi2' clipdelmenu +check_nr_clips 2 + +# Check selecting starts serving +xsel -pc + +SELECT='[1] bar' clipmenu +settle +[[ "$(xsel -po)" == bar ]] + +SELECT='[2] baz' clipmenu +settle +[[ "$(xsel -po)" == baz ]] + +# Disable populates no new clips +clipctl disable +[[ "$(clipctl status)" == disabled ]] +primary wibble +primary wobble +settle +check_nr_clips 2 + +# Enable enables again +clipctl enable +[[ "$(clipctl status)" == enabled ]] +primary wibble +primary wobble +settle +check_nr_clips 4 + +# Toggle works both ways +clipctl toggle +[[ "$(clipctl status)" == disabled ]] +clipctl toggle +[[ "$(clipctl status)" == enabled ]] + +# Test INCR support +set +x +printf '%.0sa' {1..9999999} | xsel -p +set -x +long_settle # This is a big one, give it some time +len_before=$(xsel -po | wc -c) +check_nr_clips 5 +SELECT='[5] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' \ + clipmenu +long_settle +len_after=$(xsel -po | wc -c) + +# Make sure we got the whole thing +(( len_before == len_after )) + +# Issue #241. Put a large clipboard selection via INCR, then put something else +# small immediately after. +xsel -bc +settle +check_nr_clips 5 + +# Large selection (INCR) +printf '%.0sa' {1..4001} | xsel -b +wait_nr_clips 6 + +# After large selection is stored, put something small that's a possible # +# partial (non-INCR) +printf a | xsel -b +settle + +check_nr_clips 6 + +# own_selections: default does not own PRIMARY +kill "$clipmenud_pid" +wait "$clipmenud_pid" || true +pkill -x clipserve 2>/dev/null || true +CM_OWN_CLIPBOARD=1 CM_OWN_SELECTIONS=clipboard clipmenud & +clipmenud_pid=$! +settle +xsel -bc +xsel -pc +printf 'primary_stale' | xsel -p +printf 'clip_default' | xsel -b +settle +[[ "$(xsel -bo)" == clip_default ]] +primary_out=$(xsel -po 2>/dev/null || true) +[[ "$primary_out" != clip_default ]] + +# own_selections: owns all listed selections +kill "$clipmenud_pid" +wait "$clipmenud_pid" || true +pkill -x clipserve 2>/dev/null || true +CM_OWN_CLIPBOARD=1 CM_OWN_SELECTIONS='clipboard primary' clipmenud & +clipmenud_pid=$! +settle +xsel -bc +xsel -pc +printf 'primary_stale' | xsel -p +printf 'own_both' | xsel -b +settle +[[ "$(xsel -bo)" == own_both ]] +[[ "$(xsel -po)" == own_both ]] + +if (( _UNSHARED )); then + umount -l /tmp +fi From 112255cfb09cc360a896392fbf6dc796b205c6e5 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 03/11] Squashed 'suckless/scroll/' content from commit 51ee387 git-subtree-dir: suckless/scroll git-subtree-split: 51ee387dd3860b13baaca5ef23392f7282e9bbdc --- .gitignore | 5 + LICENSE | 16 ++ Makefile | 45 ++++ README | 34 +++ TODO | 3 + config.def.h | 16 ++ config.mk | 12 ++ perf.sh | 29 +++ ptty.c | 156 ++++++++++++++ scroll.1 | 68 ++++++ scroll.c | 594 +++++++++++++++++++++++++++++++++++++++++++++++++++ up.log | 74 +++++++ up.sh | 15 ++ 13 files changed, 1067 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 config.def.h create mode 100644 config.mk create mode 100755 perf.sh create mode 100644 ptty.c create mode 100644 scroll.1 create mode 100644 scroll.c create mode 100644 up.log create mode 100755 up.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93b7ec2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +scroll +ptty +*.swp +*.core +config.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2db5bfe --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +ISC License (ISC) + +Copyright (c) 2020 Jan Klemkow +Copyright (c) 2020 Jochen Sprickerhof + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cacccd2 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +.POSIX: + +include config.mk + +all: scroll + +config.h: + cp config.def.h config.h + +scroll: scroll.c config.h + +install: scroll + mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 + cp -f scroll $(DESTDIR)$(BINDIR) + cp -f scroll.1 $(DESTDIR)$(MANDIR)/man1 + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/scroll $(DESTDIR)$(MANDIR)/man1/scroll.1 + +test: scroll ptty + # check usage + if ./ptty ./scroll -h; then exit 1; fi + # check exit passthrough of child + if ! ./ptty ./scroll true; then exit 1; fi + if ./ptty ./scroll false; then exit 1; fi + ./up.sh + +clean: + rm -f scroll ptty + +distclean: clean + rm -f config.h scroll-$(VERSION).tar.gz + +dist: clean + mkdir -p scroll-$(VERSION) + cp -R README scroll.1 TODO Makefile config.mk config.def.h \ + ptty.c scroll.c up.sh up.log \ + scroll-$(VERSION) + tar -cf - scroll-$(VERSION) | gzip > scroll-$(VERSION).tar.gz + rm -rf scroll-$(VERSION) + +.c: + $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $< -lutil + +.PHONY: all install test clean distclean dist diff --git a/README b/README new file mode 100644 index 0000000..a1837c2 --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +This program provides a scroll back buffer for a terminal like st(1). It +should run on any Unix-like system. + +At the moment it is in an experimental state. Its not recommended for +productive use. + +The initial version of this program is from Roberto E. Vargas Caballero: + https://lists.suckless.org/dev/1703/31256.html + +What is the state of scroll? + +The project is faced with some hard facts, that our original plan is not doable +as we thought in the first place: + + 1. [crtl]+[e] is used in emacs mode (default) on the shell to jump to the end + of the line. But, its also used so signal a scroll down mouse event from + terminal emulators to the shell an other programs. + + - A workaround is to use vi mode in the shell. + - Or to give up mouse support (default behavior) + + 2. scroll could not handle backward cursor jumps and editing of old lines + properly. We just handle current line editing and switching between + alternative screens (curses mode). For a proper end user experience we + would need to write a completely new terminal emulator like screen or tmux. + +What is the performance impact of scroll? + + indirect OpenBSD +------------------------------- + 0x 7.53 s + 1x 10.10 s + 2x 12.00 s + 3x 13.73 s diff --git a/TODO b/TODO new file mode 100644 index 0000000..84ffd33 --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ + * strlen function which is aware of unicode + * handle wrapping lines in scrolling line count correctly + * hotkey to dump buffer to file (like screen hardcopy) diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..536db70 --- /dev/null +++ b/config.def.h @@ -0,0 +1,16 @@ +/* + * Define ESC sequences to use for scroll events. + * Use "cat -v" to figure out favorite key combination. + * + * lines is the number of lines scrolled up or down. + * If lines is negative, it's the fraction of the terminal size. + */ + +struct rule rules[] = { + /* sequence event lines */ + {"\033[5;2~", SCROLL_UP, -1}, /* [Shift] + [PageUP] */ + {"\033[6;2~", SCROLL_DOWN, -1}, /* [Shift] + [PageDown] */ + /* mouse binding shadows ^E and ^Y, so it's disabled by default */ + //{"\031", SCROLL_UP, 1}, /* mouse wheel up */ + //{"\005", SCROLL_DOWN, 1}, /* mouse wheel Down */ +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..5676b85 --- /dev/null +++ b/config.mk @@ -0,0 +1,12 @@ +# scroll version +VERSION = 0.1 + +# paths +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin +MANDIR = $(PREFIX)/share/man + +CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE +# if your system is not POSIX, add -std=c99 to CFLAGS +CFLAGS = -Os +LDFLAGS = -s diff --git a/perf.sh b/perf.sh new file mode 100755 index 0000000..ab1fb21 --- /dev/null +++ b/perf.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -eu + +export POSIXLY_CORRECT=1 +num=1000000 +seq=seq + +if [ -x /usr/bin/jot ]; then + seq=jot +fi + +rm -f perf_*.log + +for i in `$seq 10`; do + /usr/bin/time st -e $seq $num 2>>perf_0.log +done + +for i in `$seq 10`; do + /usr/bin/time st -e ./ptty $seq $num 2>>perf_1.log +done + +for i in `$seq 10`; do + /usr/bin/time st -e ./ptty ./ptty $seq $num 2>>perf_2.log +done + +for i in `$seq 10`; do + /usr/bin/time st -e ./ptty ./ptty ./ptty $seq $num 2>>perf_3.log +done diff --git a/ptty.c b/ptty.c new file mode 100644 index 0000000..bbbb99f --- /dev/null +++ b/ptty.c @@ -0,0 +1,156 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#endif + +void +die(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(EXIT_FAILURE); +} + +void +usage(void) +{ + fputs("ptty [-C] [-c cols] [-r rows] cmd\n", stderr); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + struct winsize ws = {.ws_row = 25, .ws_col = 80, 0, 0}; + int ch; + bool closeflag = false; + + while ((ch = getopt(argc, argv, "c:r:Ch")) != -1) { + switch (ch) { + case 'c': /* cols */ + ws.ws_col = strtoimax(optarg, NULL, 10); + if (errno != 0) + die("strtoimax: %s", optarg); + break; + case 'r': /* lines */ + ws.ws_row = strtoimax(optarg, NULL, 10); + if (errno != 0) + die("strtoimax: %s", optarg); + break; + case 'C': + closeflag = true; + break; + case 'h': + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (argc < 1) + usage(); + + int mfd; + pid_t child = forkpty(&mfd, NULL, NULL, &ws); + switch (child) { + case -1: + die("forkpty"); + case 0: /* child */ + execvp(argv[0], argv); + die("exec"); + } + + /* parent */ + + if (closeflag && close(mfd) == -1) + die("close:"); + + int pfds = 2; + struct pollfd pfd[2] = { + { STDIN_FILENO, POLLIN, 0}, + { mfd, POLLIN, 0} + }; + + for (;;) { + char buf[BUFSIZ]; + ssize_t n; + int r; + + if ((r = poll(pfd, pfds, -1)) == -1) + die("poll:"); + + if (pfd[0].revents & POLLIN) { + if ((n = read(STDIN_FILENO, buf, sizeof buf)) == -1) + die("read:"); + if (n == 0) { + pfd[0].fd = -1; + if (close(mfd) == -1) + die("close:"); + break; + } + if (write(mfd, buf, n) == -1) + die("write:"); + } + + if (pfd[1].revents & POLLIN) { + if ((n = read(mfd, buf, sizeof(buf)-1)) == -1) + die("read:"); + + if (n == 0) break; + + buf[n] = '\0'; + + /* handle cursor position request */ + if (strcmp("\033[6n", buf) == 0) { + dprintf(mfd, "\033[25;1R"); + continue; + } + + if (write(STDOUT_FILENO, buf, n) == -1) + die("write:"); + } + + if (pfd[0].revents & POLLHUP) { + pfd[0].fd = -1; + if (close(mfd) == -1) + die("close:"); + break; + } + if (pfd[1].revents & POLLHUP) + break; + } + + int status; + if (waitpid(child, &status, 0) != child) + die("waitpid:"); + + return WEXITSTATUS(status); +} diff --git a/scroll.1 b/scroll.1 new file mode 100644 index 0000000..b5524ab --- /dev/null +++ b/scroll.1 @@ -0,0 +1,68 @@ +.\" +.\" Copyright (c) 2020 Jan Klemkow +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd April 9, 2020 +.Dt SCROLL 1 +.Os +.Sh NAME +.Nm scroll +.Nd scrollback buffer +.Sh SYNOPSIS +.Nm +.Op Fl Mh +.Op Fl m Ar size +.Op program Op arg ... +.Sh DESCRIPTION +The +.Nm +utility saves output lines from the child +.Ar program +to use them for scrollback. +If +.Ar program +is not set, +.Nm +starts the users default shell. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl h +Shows usage of +.Nm . +.It Fl M +Set memory limit used for scrollbackbuffer to maximum. +.It Fl m Ar size +Set memory limit used for scrollbackbuffer to +.Ar size . +.El +.Sh EXIT STATUS +.Nm +exits with the status code of its the child +.Ar program . +.Sh EXAMPLES +.Nm st +.Fl e +.Nm scroll +.Nm /bin/sh +.Sh SEE ALSO +.Xr screen 1 , +.Xr st 1 , +.Xr tmux 1 +.Sh AUTHORS +.Nm +was written by +.An Jan Klemkow Aq Mt j.klemkow@wemelug.de +and +.An Jochen Sprickerhof Aq Mt git@jochen.sprickerhof.de . diff --git a/scroll.c b/scroll.c new file mode 100644 index 0000000..8f66d54 --- /dev/null +++ b/scroll.c @@ -0,0 +1,594 @@ +/* + * Based on an example code from Roberto E. Vargas Caballero. + * + * See LICENSE file for copyright and license details. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#endif + +#define LENGTH(X) (sizeof (X) / sizeof ((X)[0])) + +const char *argv0; + +TAILQ_HEAD(tailhead, line) head; + +struct line { + TAILQ_ENTRY(line) entries; + size_t size; + size_t len; + char *buf; +} *bottom; + +pid_t child; +int mfd; +struct termios dfl; +struct winsize ws; +static bool altscreen = false; /* is alternative screen active */ +static bool doredraw = false; /* redraw upon sigwinch */ + +struct rule { + const char *seq; + enum {SCROLL_UP, SCROLL_DOWN} event; + short lines; +}; + +#include "config.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(EXIT_FAILURE); +} + +void +sigwinch(int sig) +{ + assert(sig == SIGWINCH); + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + die("ioctl:"); + if (ioctl(mfd, TIOCSWINSZ, &ws) == -1) { + if (errno == EBADF) /* child already exited */ + return; + die("ioctl:"); + } + kill(-child, SIGWINCH); + doredraw = true; +} + +void +reset(void) +{ + if (tcsetattr(STDIN_FILENO, TCSANOW, &dfl) == -1) + die("tcsetattr:"); +} + +/* error avoiding remalloc */ +void * +earealloc(void *ptr, size_t size) +{ + void *mem; + + while ((mem = realloc(ptr, size)) == NULL) { + struct line *line = TAILQ_LAST(&head, tailhead); + + if (line == NULL) + die("realloc:"); + + TAILQ_REMOVE(&head, line, entries); + free(line->buf); + free(line); + } + + return mem; +} + +/* Count string length w/o ansi esc sequences. */ +size_t +strelen(const char *buf, size_t size) +{ + enum {CHAR, BREK, ESC} state = CHAR; + size_t len = 0; + + for (size_t i = 0; i < size; i++) { + char c = buf[i]; + + switch (state) { + case CHAR: + if (c == '\033') + state = BREK; + else + len++; + break; + case BREK: + if (c == '[') { + state = ESC; + } else { + state = CHAR; + len++; + } + break; + case ESC: + if (c >= 64 && c <= 126) + state = CHAR; + break; + } + } + + return len; +} + +/* detect alternative screen switching and clear screen */ +bool +skipesc(char c) +{ + static enum {CHAR, BREK, ESC} state = CHAR; + static char buf[BUFSIZ]; + static size_t i = 0; + + switch (state) { + case CHAR: + if (c == '\033') + state = BREK; + break; + case BREK: + if (c == '[') + state = ESC; + else + state = CHAR; + break; + case ESC: + buf[i++] = c; + if (i == sizeof buf) { + /* TODO: find a better way to handle this situation */ + state = CHAR; + i = 0; + } else if (c >= 64 && c <= 126) { + state = CHAR; + buf[i] = '\0'; + i = 0; + + /* esc seq. enable alternative screen */ + if (strcmp(buf, "?1049h") == 0 || + strcmp(buf, "?1047h") == 0 || + strcmp(buf, "?47h" ) == 0) + altscreen = true; + + /* esc seq. disable alternative screen */ + if (strcmp(buf, "?1049l") == 0 || + strcmp(buf, "?1047l") == 0 || + strcmp(buf, "?47l" ) == 0) + altscreen = false; + + /* don't save cursor move or clear screen */ + /* esc sequences to log */ + switch (c) { + case 'A': + case 'B': + case 'C': + case 'D': + case 'H': + case 'J': + case 'K': + case 'f': + return true; + } + } + break; + } + + return altscreen; +} + +void +getcursorposition(int *x, int *y) +{ + char input[BUFSIZ]; + ssize_t n; + + if (write(STDOUT_FILENO, "\033[6n", 4) == -1) + die("requesting cursor position"); + + do { + if ((n = read(STDIN_FILENO, input, sizeof(input)-1)) == -1) + die("reading cursor position"); + input[n] = '\0'; + } while (sscanf(input, "\033[%d;%dR", y, x) != 2); + + if (*x <= 0 || *y <= 0) + die("invalid cursor position: x=%d y=%d", *x, *y); +} + +void +addline(char *buf, size_t size) +{ + struct line *line = earealloc(NULL, sizeof *line); + + line->size = size; + line->len = strelen(buf, size); + line->buf = earealloc(NULL, size); + memcpy(line->buf, buf, size); + + TAILQ_INSERT_HEAD(&head, line, entries); +} + +void +redraw() +{ + int rows = 0, x, y; + + if (bottom == NULL) + return; + + getcursorposition(&x, &y); + + if (y < ws.ws_row-1) + y--; + + /* wind back bottom pointer by shown history */ + for (; bottom != NULL && TAILQ_NEXT(bottom, entries) != NULL && + rows < y - 1; rows++) + bottom = TAILQ_NEXT(bottom, entries); + + /* clear screen */ + dprintf(STDOUT_FILENO, "\033[2J"); + /* set cursor position to upper left corner */ + write(STDOUT_FILENO, "\033[0;0H", 6); + + /* remove newline of first line as we are at 0,0 already */ + if (bottom->size > 0 && bottom->buf[0] == '\n') + write(STDOUT_FILENO, bottom->buf + 1, bottom->size - 1); + else + write(STDOUT_FILENO, bottom->buf, bottom->size); + + for (rows = ws.ws_row; rows > 0 && + TAILQ_PREV(bottom, tailhead, entries) != NULL; rows--) { + bottom = TAILQ_PREV(bottom, tailhead, entries); + write(STDOUT_FILENO, bottom->buf, bottom->size); + } + + if (bottom == TAILQ_FIRST(&head)) { + /* add new line in front of the shell prompt */ + write(STDOUT_FILENO, "\n", 1); + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + } else + bottom = TAILQ_NEXT(bottom, entries); +} + +void +scrollup(int n) +{ + int rows = 2, x, y, extra = 0; + struct line *scrollend = bottom; + + if (bottom == NULL) + return; + + getcursorposition(&x, &y); + + if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */ + n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; + + /* wind back scrollend pointer by the current screen */ + while (rows < y && TAILQ_NEXT(scrollend, entries) != NULL) { + scrollend = TAILQ_NEXT(scrollend, entries); + rows += (scrollend->len - 1) / ws.ws_col + 1; + } + + if (rows <= 0) + return; + + /* wind back scrollend pointer n lines */ + for (rows = 0; rows + extra < n && + TAILQ_NEXT(scrollend, entries) != NULL; rows++) { + scrollend = TAILQ_NEXT(scrollend, entries); + extra += (scrollend->len - 1) / ws.ws_col; + } + + /* move the text in terminal rows lines down */ + dprintf(STDOUT_FILENO, "\033[%dT", n); + /* set cursor position to upper left corner */ + write(STDOUT_FILENO, "\033[0;0H", 6); + /* hide cursor */ + write(STDOUT_FILENO, "\033[?25l", 6); + + /* remove newline of first line as we are at 0,0 already */ + if (scrollend->size > 0 && scrollend->buf[0] == '\n') + write(STDOUT_FILENO, scrollend->buf + 1, scrollend->size - 1); + else + write(STDOUT_FILENO, scrollend->buf, scrollend->size); + if (y + n >= ws.ws_row) + bottom = TAILQ_NEXT(bottom, entries); + + /* print rows lines and move bottom forward to the new screen bottom */ + for (; rows > 1; rows--) { + scrollend = TAILQ_PREV(scrollend, tailhead, entries); + if (y + n >= ws.ws_row) + bottom = TAILQ_NEXT(bottom, entries); + write(STDOUT_FILENO, scrollend->buf, scrollend->size); + } + /* move cursor from line n to the old bottom position */ + if (y + n < ws.ws_row) { + dprintf(STDOUT_FILENO, "\033[%d;%dH", y + n, x); + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + } else + dprintf(STDOUT_FILENO, "\033[%d;0H", ws.ws_row); +} + +void +scrolldown(char *buf, size_t size, int n) +{ + if (bottom == NULL || bottom == TAILQ_FIRST(&head)) + return; + + if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */ + n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; + + bottom = TAILQ_PREV(bottom, tailhead, entries); + /* print n lines */ + while (n > 0 && bottom != NULL && bottom != TAILQ_FIRST(&head)) { + bottom = TAILQ_PREV(bottom, tailhead, entries); + write(STDOUT_FILENO, bottom->buf, bottom->size); + n -= (bottom->len - 1) / ws.ws_col + 1; + } + if (n > 0 && bottom == TAILQ_FIRST(&head)) { + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + write(STDOUT_FILENO, buf, size); + } else if (bottom != NULL) + bottom = TAILQ_NEXT(bottom, entries); +} + +void +jumpdown(char *buf, size_t size) +{ + int rows = ws.ws_row; + + /* wind back by one page starting from the latest line */ + bottom = TAILQ_FIRST(&head); + for (; TAILQ_NEXT(bottom, entries) != NULL && rows > 0; rows--) + bottom = TAILQ_NEXT(bottom, entries); + + scrolldown(buf, size, ws.ws_row); +} + +void +usage(void) { + die("usage: %s [-Mvh] [-m mem] [program]", argv0); +} + +int +main(int argc, char *argv[]) +{ + int ch; + struct rlimit rlimit; + + argv0 = argv[0]; + + if (getrlimit(RLIMIT_DATA, &rlimit) == -1) + die("getrlimit"); + + const char *optstring = "Mm:vh"; + while ((ch = getopt(argc, argv, optstring)) != -1) { + switch (ch) { + case 'M': + rlimit.rlim_cur = rlimit.rlim_max; + break; + case 'm': + rlimit.rlim_cur = strtoull(optarg, NULL, 0); + if (errno != 0) + die("strtoull: %s", optarg); + break; + case 'v': + die("%s " VERSION, argv0); + break; + case 'h': + default: + usage(); + } + } + argc -= optind; + argv += optind; + + TAILQ_INIT(&head); + + if (isatty(STDIN_FILENO) == 0 || isatty(STDOUT_FILENO) == 0) + die("parent it not a tty"); + + /* save terminal settings for resetting after exit */ + if (tcgetattr(STDIN_FILENO, &dfl) == -1) + die("tcgetattr:"); + if (atexit(reset)) + die("atexit:"); + + /* get window size of the terminal */ + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + die("ioctl:"); + + child = forkpty(&mfd, NULL, &dfl, &ws); + if (child == -1) + die("forkpty:"); + if (child == 0) { /* child */ + if (argc >= 1) { + execvp(argv[0], argv); + } else { + struct passwd *passwd = getpwuid(getuid()); + if (passwd == NULL) + die("getpwid:"); + execlp(passwd->pw_shell, passwd->pw_shell, NULL); + } + + perror("execvp"); + _exit(127); + } + + /* set maximum memory size for scrollback buffer */ + if (setrlimit(RLIMIT_DATA, &rlimit) == -1) + die("setrlimit:"); + +#ifdef __OpenBSD__ + if (pledge("stdio tty proc", NULL) == -1) + die("pledge:"); +#endif + + if (signal(SIGWINCH, sigwinch) == SIG_ERR) + die("signal:"); + + struct termios new = dfl; + cfmakeraw(&new); + new.c_cc[VMIN ] = 1; /* return read if at least one byte in buffer */ + new.c_cc[VTIME] = 0; /* no polling time for read from terminal */ + if (tcsetattr(STDIN_FILENO, TCSANOW, &new) == -1) + die("tcsetattr:"); + + size_t size = BUFSIZ, len = 0, pos = 0; + char *buf = calloc(size, sizeof *buf); + if (buf == NULL) + die("calloc:"); + + struct pollfd pfd[2] = { + {STDIN_FILENO, POLLIN, 0}, + {mfd, POLLIN, 0} + }; + + for (;;) { + char input[BUFSIZ]; + + if (poll(pfd, LENGTH(pfd), -1) == -1 && errno != EINTR) + die("poll:"); + + if (doredraw) { + redraw(); + doredraw = false; + } + + if (pfd[0].revents & POLLHUP || pfd[1].revents & POLLHUP) + break; + + if (pfd[0].revents & POLLIN) { + ssize_t n = read(STDIN_FILENO, input, sizeof(input)-1); + + if (n == -1 && errno != EINTR) + die("read:"); + if (n == 0) + break; + + input[n] = '\0'; + + if (altscreen) + goto noevent; + + for (size_t i = 0; i < LENGTH(rules); i++) { + if (strncmp(rules[i].seq, input, + strlen(rules[i].seq)) == 0) { + if (rules[i].event == SCROLL_UP) + scrollup(rules[i].lines); + if (rules[i].event == SCROLL_DOWN) + scrolldown(buf, len, + rules[i].lines); + goto out; + } + } + noevent: + if (write(mfd, input, n) == -1) + die("write:"); + + if (bottom != TAILQ_FIRST(&head)) + jumpdown(buf, len); + } + out: + if (pfd[1].revents & POLLIN) { + ssize_t n = read(mfd, input, sizeof(input)-1); + + if (n == -1 && errno != EINTR) + die("read:"); + if (n == 0) /* on exit of child we continue here */ + continue; /* let signal handler catch SIGCHLD */ + + input[n] = '\0'; + + /* don't print child output while scrolling */ + if (bottom == TAILQ_FIRST(&head)) + if (write(STDOUT_FILENO, input, n) == -1) + die("write:"); + + /* iterate over the input buffer */ + for (char *c = input; n-- > 0; c++) { + /* don't save alternative screen and */ + /* clear screen esc sequences to scrollback */ + if (skipesc(*c)) + continue; + + if (*c == '\n') { + addline(buf, len); + /* only advance bottom if scroll is */ + /* at the end of the scroll back */ + if (bottom == NULL || + TAILQ_PREV(bottom, tailhead, + entries) == TAILQ_FIRST(&head)) + bottom = TAILQ_FIRST(&head); + + memset(buf, 0, size); + len = pos = 0; + buf[pos++] = '\r'; + } else if (*c == '\r') { + pos = 0; + continue; + } + buf[pos++] = *c; + if (pos > len) + len = pos; + if (len == size) { + size *= 2; + buf = earealloc(buf, size); + } + } + } + } + + if (close(mfd) == -1) + die("close:"); + + int status; + if (waitpid(child, &status, 0) == -1) + die("waitpid:"); + + return WEXITSTATUS(status); +} diff --git a/up.log b/up.log new file mode 100644 index 0000000..5f40226 --- /dev/null +++ b/up.log @@ -0,0 +1,74 @@ +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 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +[?25l1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 \ No newline at end of file diff --git a/up.sh b/up.sh new file mode 100755 index 0000000..fa28a80 --- /dev/null +++ b/up.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eu +export POSIXLY_CORRECT=1 + +i=1 +while test "$i" -lt 50; do + echo "$i" + i=$((i + 1)) +done > tmp.log + +(sleep 1; printf '\033[5;2~'; sleep 1; ) \ + | ./ptty ./scroll tail -fn 50 tmp.log > out.log + +cmp out.log up.log From 9586e3bdb0fa1168f2c467a200d1420f8c47396e Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 04/11] Squashed 'suckless/slock/' content from commit 65cb721 git-subtree-dir: suckless/slock git-subtree-split: 65cb721c9eb7b5d80267378790dee93e9db0223c --- LICENSE | 25 ++++ Makefile | 46 ++++++ README | 24 +++ config.def.h | 12 ++ config.mk | 29 ++++ explicit_bzero.c | 19 +++ slock.1 | 36 +++++ slock.c | 382 +++++++++++++++++++++++++++++++++++++++++++++++ util.h | 2 + 9 files changed, 575 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 explicit_bzero.c create mode 100644 slock.1 create mode 100644 slock.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b44e16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +MIT/X Consortium License + +© 2016-2026 Hiltjo Posthuma +© 2015-2016 Markus Teich +© 2014 Dimitris Papastamos +© 2006-2014 Anselm R Garbe +© 2014-2016 Laslo Hunhold + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c41e57e --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +# slock - simple screen locker +# See LICENSE file for copyright and license details. + +include config.mk + +SRC = slock.c ${COMPATSRC} +OBJ = ${SRC:.c=.o} + +all: slock + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.h config.mk util.h + +config.h: + cp config.def.h $@ + +slock: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + rm -f slock ${OBJ} slock-${VERSION}.tar.gz + +dist: clean + mkdir -p slock-${VERSION} + cp -R LICENSE Makefile README slock.1 config.mk \ + ${SRC} config.def.h util.h slock-${VERSION} + tar -cf slock-${VERSION}.tar slock-${VERSION} + gzip slock-${VERSION}.tar + rm -rf slock-${VERSION} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f slock ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/slock + chmod u+s ${DESTDIR}${PREFIX}/bin/slock + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + sed "s/VERSION/${VERSION}/g" ${DESTDIR}${MANPREFIX}/man1/slock.1 + chmod 644 ${DESTDIR}${MANPREFIX}/man1/slock.1 + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/slock + rm -f ${DESTDIR}${MANPREFIX}/man1/slock.1 + +.PHONY: all clean dist install uninstall diff --git a/README b/README new file mode 100644 index 0000000..dcacd01 --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +slock - simple screen locker +============================ +simple screen locker utility for X. + + +Requirements +------------ +In order to build slock you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (slock is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install slock +(if necessary as root): + + make clean install + + +Running slock +------------- +Simply invoke the 'slock' command. To get out of it, enter your password. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..9855e21 --- /dev/null +++ b/config.def.h @@ -0,0 +1,12 @@ +/* user and group to drop privileges to */ +static const char *user = "nobody"; +static const char *group = "nogroup"; + +static const char *colorname[NUMCOLS] = { + [INIT] = "black", /* after initialization */ + [INPUT] = "#005577", /* during input */ + [FAILED] = "#CC3333", /* wrong password */ +}; + +/* treat a cleared input like a wrong password (color) */ +static const int failonclear = 1; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..9fe5bff --- /dev/null +++ b/config.mk @@ -0,0 +1,29 @@ +# slock version +VERSION = 1.7 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# includes and libs +INCS = -I. -I/usr/include -I${X11INC} +LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr + +# flags +CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -DHAVE_SHADOW_H +CFLAGS = -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS} +LDFLAGS = -s ${LIBS} +COMPATSRC = explicit_bzero.c + +# On OpenBSD and Darwin remove -lcrypt from LIBS +#LIBS = -L/usr/lib -lc -L${X11LIB} -lX11 -lXext -lXrandr +# On *BSD remove -DHAVE_SHADOW_H from CPPFLAGS +# On NetBSD add -D_NETBSD_SOURCE to CPPFLAGS +#CPPFLAGS = -DVERSION=\"${VERSION}\" -D_BSD_SOURCE -D_NETBSD_SOURCE +# On OpenBSD set COMPATSRC to empty +#COMPATSRC = diff --git a/explicit_bzero.c b/explicit_bzero.c new file mode 100644 index 0000000..3e33ca8 --- /dev/null +++ b/explicit_bzero.c @@ -0,0 +1,19 @@ +/* $OpenBSD: explicit_bzero.c,v 1.3 2014/06/21 02:34:26 matthew Exp $ */ +/* + * Public domain. + * Written by Matthew Dempsky. + */ + +#include + +__attribute__((weak)) void +__explicit_bzero_hook(void *buf, size_t len) +{ +} + +void +explicit_bzero(void *buf, size_t len) +{ + memset(buf, 0, len); + __explicit_bzero_hook(buf, len); +} diff --git a/slock.1 b/slock.1 new file mode 100644 index 0000000..e485992 --- /dev/null +++ b/slock.1 @@ -0,0 +1,36 @@ +.Dd September 5, 2026 +.Dt SLOCK 1 +.Os +.Sh NAME +.Nm slock +.Nd simple X screen locker +.Sh SYNOPSIS +.Nm +.Op Fl v +.Sh DESCRIPTION +.Nm +is a simple X screen locker. +The options are as follows: +.Bl -tag -width Ds +.It Fl v +Print version information to stdout and exit. +.El +.Sh EXIT STATUS +.Ex -std +.Sh SECURITY CONSIDERATIONS +To make sure a locked screen can not be bypassed by switching VTs +or killing the X server with Ctrl+Alt+Backspace, it is recommended +to disable both in +.Xr xorg.conf 5 +for maximum security: +.Bd -literal +Section "ServerFlags" + Option "DontVTSwitch" "True" + Option "DontZap" "True" +EndSection +.Ed +.Sh CUSTOMIZATION +.Nm +can be customized by creating a custom config.h from config.def.h and +(re)compiling the source code. +This keeps it fast, secure and simple. diff --git a/slock.c b/slock.c new file mode 100644 index 0000000..9c587e1 --- /dev/null +++ b/slock.c @@ -0,0 +1,382 @@ +/* See LICENSE file for license details. */ +#define _XOPEN_SOURCE 500 +#if HAVE_SHADOW_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +char *argv0; + +enum { + INIT, + INPUT, + FAILED, + NUMCOLS +}; + +struct lock { + int screen; + Window root, win; + Pixmap pmap; + unsigned long colors[NUMCOLS]; +}; + +struct xrandr { + int active; + int evbase; + int errbase; +}; + +#include "config.h" + +static void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +#ifdef __linux__ +#include +#include + +static void +dontkillme(void) +{ + FILE *f; + const char oomfile[] = "/proc/self/oom_score_adj"; + + if (!(f = fopen(oomfile, "w"))) { + if (errno == ENOENT) + return; + die("slock: fopen %s: %s\n", oomfile, strerror(errno)); + } + fprintf(f, "%d", OOM_SCORE_ADJ_MIN); + if (fclose(f)) { + if (errno == EACCES) + die("slock: unable to disable OOM killer. " + "Make sure to suid or sgid slock.\n"); + else + die("slock: fclose %s: %s\n", oomfile, strerror(errno)); + } +} +#endif + +static const char * +gethash(void) +{ + const char *hash; + struct passwd *pw; + + /* Check if the current user has a password entry */ + errno = 0; + if (!(pw = getpwuid(getuid()))) { + if (errno) + die("slock: getpwuid: %s\n", strerror(errno)); + else + die("slock: cannot retrieve password entry\n"); + } + hash = pw->pw_passwd; + +#if HAVE_SHADOW_H + if (!strcmp(hash, "x")) { + struct spwd *sp; + if (!(sp = getspnam(pw->pw_name))) + die("slock: getspnam: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); + hash = sp->sp_pwdp; + } +#else + if (!strcmp(hash, "*")) { +#ifdef __OpenBSD__ + if (!(pw = getpwuid_shadow(getuid()))) + die("slock: getpwnam_shadow: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); + hash = pw->pw_passwd; +#else + die("slock: getpwuid: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); +#endif /* __OpenBSD__ */ + } +#endif /* HAVE_SHADOW_H */ + + return hash; +} + +static void +readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens, + const char *hash) +{ + XRRScreenChangeNotifyEvent *rre; + char buf[32], passwd[256], *inputhash; + int num, screen, running, failure, oldc; + unsigned int len, color; + KeySym ksym; + XEvent ev; + + len = 0; + running = 1; + failure = 0; + oldc = INIT; + + while (running && !XNextEvent(dpy, &ev)) { + if (ev.type == KeyPress) { + explicit_bzero(&buf, sizeof(buf)); + num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0); + if (IsKeypadKey(ksym)) { + if (ksym == XK_KP_Enter) + ksym = XK_Return; + else if (ksym >= XK_KP_0 && ksym <= XK_KP_9) + ksym = (ksym - XK_KP_0) + XK_0; + } + if (IsFunctionKey(ksym) || + IsKeypadKey(ksym) || + IsMiscFunctionKey(ksym) || + IsPFKey(ksym) || + IsPrivateKeypadKey(ksym)) + continue; + switch (ksym) { + case XK_Return: + passwd[len] = '\0'; + errno = 0; + if (!(inputhash = crypt(passwd, hash))) + fprintf(stderr, "slock: crypt: %s\n", strerror(errno)); + else + running = !!strcmp(inputhash, hash); + if (running) { + XBell(dpy, 100); + failure = 1; + } + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + break; + case XK_Escape: + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + break; + case XK_BackSpace: + if (len) + passwd[--len] = '\0'; + break; + default: + if (num && !iscntrl((unsigned char)buf[0]) && + (len + num < sizeof(passwd))) { + memcpy(passwd + len, buf, num); + len += num; + } else if (buf[0] == '\025') { /* ctrl-u clears input */ + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + } + break; + } + color = len ? INPUT : ((failure || failonclear) ? FAILED : INIT); + if (running && oldc != color) { + for (screen = 0; screen < nscreens; screen++) { + XSetWindowBackground(dpy, + locks[screen]->win, + locks[screen]->colors[color]); + XClearWindow(dpy, locks[screen]->win); + } + oldc = color; + } + } else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) { + rre = (XRRScreenChangeNotifyEvent*)&ev; + for (screen = 0; screen < nscreens; screen++) { + if (locks[screen]->win == rre->window) { + if (rre->rotation == RR_Rotate_90 || + rre->rotation == RR_Rotate_270) + XResizeWindow(dpy, locks[screen]->win, + rre->height, rre->width); + else + XResizeWindow(dpy, locks[screen]->win, + rre->width, rre->height); + XClearWindow(dpy, locks[screen]->win); + break; + } + } + } else { + for (screen = 0; screen < nscreens; screen++) + XRaiseWindow(dpy, locks[screen]->win); + } + } +} + +static struct lock * +lockscreen(Display *dpy, struct xrandr *rr, int screen) +{ + char curs[] = {0, 0, 0, 0, 0, 0, 0, 0}; + int i, ptgrab, kbgrab; + struct lock *lock; + XColor color, dummy; + XSetWindowAttributes wa; + Cursor invisible; + + if (dpy == NULL || screen < 0 || !(lock = malloc(sizeof(struct lock)))) + return NULL; + + lock->screen = screen; + lock->root = RootWindow(dpy, lock->screen); + + for (i = 0; i < NUMCOLS; i++) { + XAllocNamedColor(dpy, DefaultColormap(dpy, lock->screen), + colorname[i], &color, &dummy); + lock->colors[i] = color.pixel; + } + + /* init */ + wa.override_redirect = 1; + wa.background_pixel = lock->colors[INIT]; + lock->win = XCreateWindow(dpy, lock->root, 0, 0, + DisplayWidth(dpy, lock->screen), + DisplayHeight(dpy, lock->screen), + 0, DefaultDepth(dpy, lock->screen), + CopyFromParent, + DefaultVisual(dpy, lock->screen), + CWOverrideRedirect | CWBackPixel, &wa); + lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8); + invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap, + &color, &color, 0, 0); + XDefineCursor(dpy, lock->win, invisible); + + /* Try to grab mouse pointer *and* keyboard for 600ms, else fail the lock */ + for (i = 0, ptgrab = kbgrab = -1; i < 6; i++) { + if (ptgrab != GrabSuccess) { + ptgrab = XGrabPointer(dpy, lock->root, False, + ButtonPressMask | ButtonReleaseMask | + PointerMotionMask, GrabModeAsync, + GrabModeAsync, None, invisible, CurrentTime); + } + if (kbgrab != GrabSuccess) { + kbgrab = XGrabKeyboard(dpy, lock->root, True, + GrabModeAsync, GrabModeAsync, CurrentTime); + } + + /* input is grabbed: we can lock the screen */ + if (ptgrab == GrabSuccess && kbgrab == GrabSuccess) { + XMapRaised(dpy, lock->win); + if (rr->active) + XRRSelectInput(dpy, lock->win, RRScreenChangeNotifyMask); + + XSelectInput(dpy, lock->root, SubstructureNotifyMask); + return lock; + } + + /* retry on AlreadyGrabbed but fail on other errors */ + if ((ptgrab != AlreadyGrabbed && ptgrab != GrabSuccess) || + (kbgrab != AlreadyGrabbed && kbgrab != GrabSuccess)) + break; + + usleep(100000); + } + + /* we couldn't grab all input: fail out */ + if (ptgrab != GrabSuccess) + fprintf(stderr, "slock: unable to grab mouse pointer for screen %d\n", + screen); + if (kbgrab != GrabSuccess) + fprintf(stderr, "slock: unable to grab keyboard for screen %d\n", + screen); + return NULL; +} + +static void +usage(void) +{ + die("usage: slock [-v]\n"); +} + +int +main(int argc, char **argv) { + struct xrandr rr; + struct lock **locks; + struct passwd *pwd; + struct group *grp; + uid_t duid; + gid_t dgid; + const char *hash; + Display *dpy; + int s, nlocks, nscreens; + + if (argc > 1 && !strcmp(argv[1], "-v")) { + puts("slock-"VERSION); + return 0; + } else if (argc > 1) { + usage(); + } + + /* validate drop-user and -group */ + errno = 0; + if (!(pwd = getpwnam(user))) + die("slock: getpwnam %s: %s\n", user, + errno ? strerror(errno) : "user entry not found"); + duid = pwd->pw_uid; + errno = 0; + if (!(grp = getgrnam(group))) + die("slock: getgrnam %s: %s\n", group, + errno ? strerror(errno) : "group entry not found"); + dgid = grp->gr_gid; + +#ifdef __linux__ + dontkillme(); +#endif + + hash = gethash(); + errno = 0; + if (!crypt("", hash)) + die("slock: crypt: %s\n", strerror(errno)); + + if (!(dpy = XOpenDisplay(NULL))) + die("slock: cannot open display\n"); + + /* drop privileges */ + if (setgroups(0, NULL) < 0) + die("slock: setgroups: %s\n", strerror(errno)); + if (setgid(dgid) < 0) + die("slock: setgid: %s\n", strerror(errno)); + if (setuid(duid) < 0) + die("slock: setuid: %s\n", strerror(errno)); + + /* check for Xrandr support */ + rr.active = XRRQueryExtension(dpy, &rr.evbase, &rr.errbase); + + /* get number of screens in display "dpy" and blank them */ + nscreens = ScreenCount(dpy); + if (!(locks = calloc(nscreens, sizeof(struct lock *)))) + die("slock: out of memory\n"); + for (nlocks = 0, s = 0; s < nscreens; s++) { + if ((locks[s] = lockscreen(dpy, &rr, s)) != NULL) + nlocks++; + else + break; + } + XSync(dpy, 0); + + /* did we manage to lock everything? */ + if (nlocks != nscreens) + return 1; + + /* everything is now blank. Wait for the correct password */ + readpw(dpy, &rr, locks, nscreens, hash); + + return 0; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..6f748b8 --- /dev/null +++ b/util.h @@ -0,0 +1,2 @@ +#undef explicit_bzero +void explicit_bzero(void *, size_t); From 9ae5fa514268cbd673d5ff8740c10c852f28f160 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 05/11] Squashed 'suckless/slstatus/' content from commit 675c839 git-subtree-dir: suckless/slstatus git-subtree-split: 675c83912ea1aca9834448aaa87a040bc9990c52 --- LICENSE | 46 ++++ Makefile | 69 +++++ README | 65 +++++ arg.h | 33 +++ components/battery.c | 247 ++++++++++++++++++ components/cat.c | 32 +++ components/cpu.c | 157 ++++++++++++ components/datetime.c | 20 ++ components/disk.c | 59 +++++ components/entropy.c | 29 +++ components/hostname.c | 17 ++ components/ip.c | 89 +++++++ components/kernel_release.c | 19 ++ components/keyboard_indicators.c | 50 ++++ components/keymap.c | 86 +++++++ components/load_avg.c | 19 ++ components/netspeeds.c | 129 ++++++++++ components/num_files.c | 32 +++ components/ram.c | 227 +++++++++++++++++ components/run_command.c | 31 +++ components/swap.c | 276 ++++++++++++++++++++ components/temperature.c | 73 ++++++ components/uptime.c | 34 +++ components/user.c | 33 +++ components/volume.c | 224 +++++++++++++++++ components/wifi.c | 417 +++++++++++++++++++++++++++++++ config.def.h | 70 ++++++ config.mk | 22 ++ slstatus.1 | 47 ++++ slstatus.c | 135 ++++++++++ slstatus.h | 85 +++++++ util.c | 158 ++++++++++++ util.h | 18 ++ 33 files changed, 3048 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 arg.h create mode 100644 components/battery.c create mode 100644 components/cat.c create mode 100644 components/cpu.c create mode 100644 components/datetime.c create mode 100644 components/disk.c create mode 100644 components/entropy.c create mode 100644 components/hostname.c create mode 100644 components/ip.c create mode 100644 components/kernel_release.c create mode 100644 components/keyboard_indicators.c create mode 100644 components/keymap.c create mode 100644 components/load_avg.c create mode 100644 components/netspeeds.c create mode 100644 components/num_files.c create mode 100644 components/ram.c create mode 100644 components/run_command.c create mode 100644 components/swap.c create mode 100644 components/temperature.c create mode 100644 components/uptime.c create mode 100644 components/user.c create mode 100644 components/volume.c create mode 100644 components/wifi.c create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 slstatus.1 create mode 100644 slstatus.c create mode 100644 slstatus.h create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..859df8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,46 @@ +ISC License + +Copyright 2016-2026 Aaron Marcher + +Copyright 2016 Roy Freytag +Copyright 2016 Vincent Loupmon +Copyright 2016 Daniel Walter +Copyright 2016-2018 Ali H. Fardan +Copyright 2016 Jody Leonard +Copyright 2016-2018 Quentin Rameau +Copyright 2016 Mike Coddington +Copyright 2016-2018 Ivan J. +Copyright 2017 Tobias Stoeckmann +Copyright 2017-2018 Laslo Hunhold +Copyright 2018 Darron Anderson +Copyright 2018 Josuah Demangeon +Copyright 2018 Tobias Tschinkowitz +Copyright 2018 David Demelier +Copyright 2018-2012 Michael Buch +Copyright 2018 Ian Remmler +Copyright 2016-2019 Joerg Jung +Copyright 2019 Ryan Kes +Copyright 2019 Cem Keylan +Copyright 2019 Spiros Thanasoulas +Copyright 2019-2022 Ingo Feinerer +Copyright 2020 Alexandre Ratchov +Copyright 2020 Mart Lubbers +Copyright 2020 Daniel Moch +Copyright 2022 Nickolas Raymond Kaczynski +Copyright 2022 Patrick Iacob +Copyright 2021-2022 Steven Ward +Copyright 2025 Joakim Sindholt +Copyright 2025 Al +Copyright 2025 sewn + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a18274 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# See LICENSE file for copyright and license details +# slstatus - suckless status monitor +.POSIX: + +include config.mk + +REQ = util +COM =\ + components/battery\ + components/cat\ + components/cpu\ + components/datetime\ + components/disk\ + components/entropy\ + components/hostname\ + components/ip\ + components/kernel_release\ + components/keyboard_indicators\ + components/keymap\ + components/load_avg\ + components/netspeeds\ + components/num_files\ + components/ram\ + components/run_command\ + components/swap\ + components/temperature\ + components/uptime\ + components/user\ + components/volume\ + components/wifi + +all: slstatus + +$(COM:=.o): config.mk $(REQ:=.h) slstatus.h +slstatus.o: slstatus.c slstatus.h arg.h config.h config.mk $(REQ:=.h) + +.c.o: + $(CC) -o $@ -c $(CPPFLAGS) $(CFLAGS) $< + +config.h: + cp config.def.h $@ + +slstatus: slstatus.o $(COM:=.o) $(REQ:=.o) + $(CC) -o $@ $(LDFLAGS) $(COM:=.o) $(REQ:=.o) slstatus.o $(LDLIBS) + +clean: + rm -f slstatus slstatus.o $(COM:=.o) $(REQ:=.o) slstatus-${VERSION}.tar.gz + +dist: + rm -rf "slstatus-$(VERSION)" + mkdir -p "slstatus-$(VERSION)/components" + cp -R LICENSE Makefile README config.mk config.def.h \ + arg.h slstatus.h slstatus.c $(REQ:=.c) $(REQ:=.h) \ + slstatus.1 "slstatus-$(VERSION)" + cp -R $(COM:=.c) "slstatus-$(VERSION)/components" + tar -cf - "slstatus-$(VERSION)" | gzip -c > "slstatus-$(VERSION).tar.gz" + rm -rf "slstatus-$(VERSION)" + +install: all + mkdir -p "$(DESTDIR)$(PREFIX)/bin" + cp -f slstatus "$(DESTDIR)$(PREFIX)/bin" + chmod 755 "$(DESTDIR)$(PREFIX)/bin/slstatus" + mkdir -p "$(DESTDIR)$(MANPREFIX)/man1" + cp -f slstatus.1 "$(DESTDIR)$(MANPREFIX)/man1" + chmod 644 "$(DESTDIR)$(MANPREFIX)/man1/slstatus.1" + +uninstall: + rm -f "$(DESTDIR)$(PREFIX)/bin/slstatus" + rm -f "$(DESTDIR)$(MANPREFIX)/man1/slstatus.1" diff --git a/README b/README new file mode 100644 index 0000000..4d592bb --- /dev/null +++ b/README @@ -0,0 +1,65 @@ +slstatus - suckless status +========================== +slstatus is a small tool for providing system status information to other +programs over the EWMH property of the root window (used by dwm(1)) or +standard input/output. It is designed to be as efficient as possible by +only issuing the minimum of system calls required. + + +Features +-------- +- Battery percentage/state/time left +- Cat (read file) +- CPU usage +- CPU frequency +- Custom shell commands +- Date and time +- Disk status (free storage, percentage, total storage and used storage) +- Available entropy +- Username/GID/UID +- Hostname +- IP address (IPv4 and IPv6), interface status +- Kernel version +- Keyboard indicators +- Keymap +- Load average +- Network speeds (RX and TX) +- Number of files in a directory (hint: Maildir) +- Memory status (free memory, percentage, total memory and used memory) +- Swap status (free swap, percentage, total swap and used swap) +- Temperature +- Uptime +- Volume percentage +- WiFi signal percentage and ESSID + + +Requirements +------------ +Currently slstatus works on FreeBSD, Linux and OpenBSD. +In order to build slstatus you need the Xlib header files. + +- For volume percentage on Linux the kernel module `snd-mixer-oss` must be + loaded. +- For volume percentage on FreeBSD, `sndio` must be installed. + + +Installation +------------ +Edit config.mk to match your local setup (slstatus is installed into the +/usr/local namespace by default). + +Afterwards enter the following command to build and install slstatus (if +necessary as root): + + make clean install + + +Running slstatus +---------------- +See the man page for details. + + +Configuration +------------- +slstatus can be customized by creating a custom config.h and (re)compiling the +source code. This keeps it fast, secure and simple. diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..5f1f408 --- /dev/null +++ b/arg.h @@ -0,0 +1,33 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef ARG_H +#define ARG_H + +extern char *argv0; + +/* int main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, *argv ? (argc--, argv++) : ((void *)0); \ + *argv && (*argv)[0] == '-' && (*argv)[1]; argc--, argv++) { \ + int i_, argused_; \ + if ((*argv)[1] == '-' && !(*argv)[2]) { \ + argc--, argv++; \ + break; \ + } \ + for (i_ = 1, argused_ = 0; (*argv)[i_]; i_++) { \ + switch ((*argv)[i_]) +#define ARGEND if (argused_) { \ + if ((*argv)[i_ + 1]) { \ + break; \ + } else { \ + argc--, argv++; \ + break; \ + } \ + } \ + } \ + } +#define ARGC() ((*argv)[i_]) +#define ARGF_(x) (((*argv)[i_ + 1]) ? (argused_ = 1, &((*argv)[i_ + 1])) : \ + (*(argv + 1)) ? (argused_ = 1, *(argv + 1)) : (x)) +#define EARGF(x) ARGF_(((x), exit(1), (char *)0)) +#define ARGF() ARGF_((char *)0) + +#endif diff --git a/components/battery.c b/components/battery.c new file mode 100644 index 0000000..f2dc393 --- /dev/null +++ b/components/battery.c @@ -0,0 +1,247 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) +/* + * https://www.kernel.org/doc/html/latest/power/power_supply_class.html + */ + #include + #include + #include + + #define POWER_SUPPLY_CAPACITY "/sys/class/power_supply/%s/capacity" + #define POWER_SUPPLY_STATUS "/sys/class/power_supply/%s/status" + #define POWER_SUPPLY_CHARGE "/sys/class/power_supply/%s/charge_now" + #define POWER_SUPPLY_ENERGY "/sys/class/power_supply/%s/energy_now" + #define POWER_SUPPLY_CURRENT "/sys/class/power_supply/%s/current_now" + #define POWER_SUPPLY_POWER "/sys/class/power_supply/%s/power_now" + + static const char * + pick(const char *bat, const char *f1, const char *f2, char *path, + size_t length) + { + if (esnprintf(path, length, f1, bat) > 0 && + access(path, R_OK) == 0) + return f1; + + if (esnprintf(path, length, f2, bat) > 0 && + access(path, R_OK) == 0) + return f2; + + return NULL; + } + + const char * + battery_perc(const char *bat) + { + int cap_perc; + char path[PATH_MAX]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_CAPACITY, bat) < 0) + return NULL; + if (pscanf(path, "%d", &cap_perc) != 1) + return NULL; + + return bprintf("%d", cap_perc); + } + + const char * + battery_state(const char *bat) + { + static struct { + char *state; + char *symbol; + } map[] = { + { "Charging", "+" }, + { "Discharging", "-" }, + { "Full", "o" }, + { "Not charging", "o" }, + }; + size_t i; + char path[PATH_MAX], state[13]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0) + return NULL; + if (pscanf(path, "%12[a-zA-Z ]", state) != 1) + return NULL; + + for (i = 0; i < LEN(map); i++) + if (!strcmp(map[i].state, state)) + break; + + return (i == LEN(map)) ? "?" : map[i].symbol; + } + + const char * + battery_remaining(const char *bat) + { + uintmax_t charge_now, current_now, m, h; + double timeleft; + char path[PATH_MAX], state[13]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0) + return NULL; + if (pscanf(path, "%12[a-zA-Z ]", state) != 1) + return NULL; + + if (!pick(bat, POWER_SUPPLY_CHARGE, POWER_SUPPLY_ENERGY, path, + sizeof(path)) || + pscanf(path, "%ju", &charge_now) < 0) + return NULL; + + if (!strcmp(state, "Discharging")) { + if (!pick(bat, POWER_SUPPLY_CURRENT, POWER_SUPPLY_POWER, path, + sizeof(path)) || + pscanf(path, "%ju", ¤t_now) < 0) + return NULL; + + if (current_now == 0) + return NULL; + + timeleft = (double)charge_now / (double)current_now; + h = timeleft; + m = (timeleft - (double)h) * 60; + + return bprintf("%juh %jum", h, m); + } + + return ""; + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + static int + load_apm_power_info(struct apm_power_info *apm_info) + { + int fd; + + fd = open("/dev/apm", O_RDONLY); + if (fd < 0) { + warn("open '/dev/apm':"); + return 0; + } + + memset(apm_info, 0, sizeof(struct apm_power_info)); + if (ioctl(fd, APM_IOC_GETPOWER, apm_info) < 0) { + warn("ioctl 'APM_IOC_GETPOWER':"); + close(fd); + return 0; + } + return close(fd), 1; + } + + const char * + battery_perc(const char *unused) + { + struct apm_power_info apm_info; + + if (load_apm_power_info(&apm_info)) + return bprintf("%d", apm_info.battery_life); + + return NULL; + } + + const char * + battery_state(const char *unused) + { + struct { + unsigned int state; + char *symbol; + } map[] = { + { APM_AC_ON, "+" }, + { APM_AC_OFF, "-" }, + }; + struct apm_power_info apm_info; + size_t i; + + if (load_apm_power_info(&apm_info)) { + for (i = 0; i < LEN(map); i++) + if (map[i].state == apm_info.ac_state) + break; + + return (i == LEN(map)) ? "?" : map[i].symbol; + } + + return NULL; + } + + const char * + battery_remaining(const char *unused) + { + struct apm_power_info apm_info; + unsigned int h, m; + + if (load_apm_power_info(&apm_info)) { + if (apm_info.ac_state != APM_AC_ON) { + h = apm_info.minutes_left / 60; + m = apm_info.minutes_left % 60; + return bprintf("%uh %02um", h, m); + } else { + return ""; + } + } + + return NULL; + } +#elif defined(__FreeBSD__) + #include + + #define BATTERY_LIFE "hw.acpi.battery.life" + #define BATTERY_STATE "hw.acpi.battery.state" + #define BATTERY_TIME "hw.acpi.battery.time" + + const char * + battery_perc(const char *unused) + { + int cap_perc; + size_t len; + + len = sizeof(cap_perc); + if (sysctlbyname(BATTERY_LIFE, &cap_perc, &len, NULL, 0) < 0 || !len) + return NULL; + + return bprintf("%d", cap_perc); + } + + const char * + battery_state(const char *unused) + { + int state; + size_t len; + + len = sizeof(state); + if (sysctlbyname(BATTERY_STATE, &state, &len, NULL, 0) < 0 || !len) + return NULL; + + switch (state) { + case 0: /* FALLTHROUGH */ + case 2: + return "+"; + case 1: + return "-"; + default: + return "?"; + } + } + + const char * + battery_remaining(const char *unused) + { + int rem; + size_t len; + + len = sizeof(rem); + if (sysctlbyname(BATTERY_TIME, &rem, &len, NULL, 0) < 0 || !len + || rem < 0) + return NULL; + + return bprintf("%uh %02um", rem / 60, rem % 60); + } +#endif diff --git a/components/cat.c b/components/cat.c new file mode 100644 index 0000000..07944cc --- /dev/null +++ b/components/cat.c @@ -0,0 +1,32 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +cat(const char *path) +{ + char *f; + FILE *fp; + + if (!(fp = fopen(path, "r"))) { + warn("fopen '%s':", path); + return NULL; + } + + f = fgets(buf, sizeof(buf) - 1, fp); + if (fclose(fp) < 0) { + warn("fclose '%s':", path); + return NULL; + } + if (!f) + return NULL; + + if ((f = strrchr(buf, '\n'))) + f[0] = '\0'; + + return buf[0] ? buf : NULL; +} + diff --git a/components/cpu.c b/components/cpu.c new file mode 100644 index 0000000..d0d03c7 --- /dev/null +++ b/components/cpu.c @@ -0,0 +1,157 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + #define CPU_FREQ "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" + + const char * + cpu_freq(const char *unused) + { + uintmax_t freq; + + /* in kHz */ + if (pscanf(CPU_FREQ, "%ju", &freq) != 1) + return NULL; + + return fmt_human(freq * 1000, 1000); + } + + const char * + cpu_perc(const char *unused) + { + static long double a[7]; + long double b[7], sum; + + memcpy(b, a, sizeof(b)); + /* cpu user nice system idle iowait irq softirq */ + if (pscanf("/proc/stat", "%*s %Lf %Lf %Lf %Lf %Lf %Lf %Lf", + &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6]) + != 7) + return NULL; + + if (b[0] == 0) + return NULL; + + sum = (b[0] + b[1] + b[2] + b[3] + b[4] + b[5] + b[6]) - + (a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6]); + + if (sum == 0) + return NULL; + + return bprintf("%d", (int)(100 * + ((b[0] + b[1] + b[2] + b[5] + b[6]) - + (a[0] + a[1] + a[2] + a[5] + a[6])) / sum)); + } +#elif defined(__OpenBSD__) + #include + #include + #include + + const char * + cpu_freq(const char *unused) + { + int freq, mib[2]; + size_t size; + + mib[0] = CTL_HW; + mib[1] = HW_CPUSPEED; + + size = sizeof(freq); + + /* in MHz */ + if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { + warn("sysctl 'HW_CPUSPEED':"); + return NULL; + } + + return fmt_human(freq * 1E6, 1000); + } + + const char * + cpu_perc(const char *unused) + { + int mib[2]; + static uintmax_t a[CPUSTATES]; + uintmax_t b[CPUSTATES], sum; + size_t size; + + mib[0] = CTL_KERN; + mib[1] = KERN_CPTIME; + + size = sizeof(a); + + memcpy(b, a, sizeof(b)); + if (sysctl(mib, 2, &a, &size, NULL, 0) < 0) { + warn("sysctl 'KERN_CPTIME':"); + return NULL; + } + if (b[0] == 0) + return NULL; + + sum = (a[CP_USER] + a[CP_NICE] + a[CP_SYS] + a[CP_INTR] + a[CP_IDLE]) - + (b[CP_USER] + b[CP_NICE] + b[CP_SYS] + b[CP_INTR] + b[CP_IDLE]); + + if (sum == 0) + return NULL; + + return bprintf("%d", 100 * + ((a[CP_USER] + a[CP_NICE] + a[CP_SYS] + + a[CP_INTR]) - + (b[CP_USER] + b[CP_NICE] + b[CP_SYS] + + b[CP_INTR])) / sum); + } +#elif defined(__FreeBSD__) + #include + #include + #include + + const char * + cpu_freq(const char *unused) + { + int freq; + size_t size; + + size = sizeof(freq); + /* in MHz */ + if (sysctlbyname("hw.clockrate", &freq, &size, NULL, 0) < 0 || !size) { + warn("sysctlbyname 'hw.clockrate':"); + return NULL; + } + + return fmt_human(freq * 1E6, 1000); + } + + const char * + cpu_perc(const char *unused) + { + size_t size; + static long a[CPUSTATES]; + long b[CPUSTATES], sum; + + size = sizeof(a); + memcpy(b, a, sizeof(b)); + if (sysctlbyname("kern.cp_time", &a, &size, NULL, 0) < 0 || !size) { + warn("sysctlbyname 'kern.cp_time':"); + return NULL; + } + if (b[0] == 0) + return NULL; + + sum = (a[CP_USER] + a[CP_NICE] + a[CP_SYS] + a[CP_INTR] + a[CP_IDLE]) - + (b[CP_USER] + b[CP_NICE] + b[CP_SYS] + b[CP_INTR] + b[CP_IDLE]); + + if (sum == 0) + return NULL; + + return bprintf("%d", 100 * + ((a[CP_USER] + a[CP_NICE] + a[CP_SYS] + + a[CP_INTR]) - + (b[CP_USER] + b[CP_NICE] + b[CP_SYS] + + b[CP_INTR])) / sum); + } +#endif diff --git a/components/datetime.c b/components/datetime.c new file mode 100644 index 0000000..5b10daf --- /dev/null +++ b/components/datetime.c @@ -0,0 +1,20 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +datetime(const char *fmt) +{ + time_t t; + + t = time(NULL); + if (!strftime(buf, sizeof(buf), fmt, localtime(&t))) { + warn("strftime: Result string exceeds buffer size"); + return NULL; + } + + return buf; +} diff --git a/components/disk.c b/components/disk.c new file mode 100644 index 0000000..e19a693 --- /dev/null +++ b/components/disk.c @@ -0,0 +1,59 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +disk_free(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * fs.f_bavail, 1024); +} + +const char * +disk_perc(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return bprintf("%d", (int)(100 * + (1 - ((double)fs.f_bavail / (double)fs.f_blocks)))); +} + +const char * +disk_total(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * fs.f_blocks, 1024); +} + +const char * +disk_used(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * (fs.f_blocks - fs.f_bfree), 1024); +} diff --git a/components/entropy.c b/components/entropy.c new file mode 100644 index 0000000..65010b0 --- /dev/null +++ b/components/entropy.c @@ -0,0 +1,29 @@ +/* See LICENSE file for copyright and license details. */ +#include "../slstatus.h" +#if defined(__linux__) + #include + #include + + #include "../util.h" + + #define ENTROPY_AVAIL "/proc/sys/kernel/random/entropy_avail" + + const char * + entropy(const char *unused) + { + uintmax_t num; + + if (pscanf(ENTROPY_AVAIL, "%ju", &num) != 1) + return NULL; + + return bprintf("%ju", num); + } +#elif defined(__OpenBSD__) | defined(__FreeBSD__) + const char * + entropy(const char *unused) + { + // https://www.unicode.org/charts/PDF/U2200.pdf + /* Unicode Character 'INFINITY' (U+221E) */ + return "\u221E"; + } +#endif diff --git a/components/hostname.c b/components/hostname.c new file mode 100644 index 0000000..dab8b63 --- /dev/null +++ b/components/hostname.c @@ -0,0 +1,17 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +hostname(const char *unused) +{ + if (gethostname(buf, sizeof(buf)) < 0) { + warn("gethostbyname:"); + return NULL; + } + + return buf; +} diff --git a/components/ip.c b/components/ip.c new file mode 100644 index 0000000..ac75661 --- /dev/null +++ b/components/ip.c @@ -0,0 +1,89 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#if defined(__OpenBSD__) + #include + #include +#elif defined(__FreeBSD__) + #include + #include +#endif + +#include "../slstatus.h" +#include "../util.h" + +static const char * +ip(const char *interface, unsigned short sa_family) +{ + struct ifaddrs *ifaddr, *ifa; + int s; + char host[NI_MAXHOST]; + + if (getifaddrs(&ifaddr) < 0) { + warn("getifaddrs:"); + return NULL; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + if (strcmp(ifa->ifa_name, interface) || + ifa->ifa_addr->sa_family != sa_family) + continue; + + s = getnameinfo(ifa->ifa_addr, + (sa_family == AF_INET) ? sizeof(struct sockaddr_in) + : sizeof(struct sockaddr_in6), + host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST); + freeifaddrs(ifaddr); + if (s != 0) { + warn("getnameinfo: %s", gai_strerror(s)); + return NULL; + } + return bprintf("%s", host); + } + + freeifaddrs(ifaddr); + + return NULL; +} + +const char * +ipv4(const char *interface) +{ + return ip(interface, AF_INET); +} + +const char * +ipv6(const char *interface) +{ + return ip(interface, AF_INET6); +} + +const char * +up(const char *interface) +{ + struct ifaddrs *ifaddr, *ifa; + + if (getifaddrs(&ifaddr) < 0) { + warn("getifaddrs:"); + return NULL; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + + if (!strcmp(ifa->ifa_name, interface)) { + freeifaddrs(ifaddr); + return ifa->ifa_flags & IFF_UP ? "up" : "down"; + } + } + + freeifaddrs(ifaddr); + + return NULL; +} diff --git a/components/kernel_release.c b/components/kernel_release.c new file mode 100644 index 0000000..36a6a44 --- /dev/null +++ b/components/kernel_release.c @@ -0,0 +1,19 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +kernel_release(const char *unused) +{ + struct utsname udata; + + if (uname(&udata) < 0) { + warn("uname:"); + return NULL; + } + + return bprintf("%s", udata.release); +} diff --git a/components/keyboard_indicators.c b/components/keyboard_indicators.c new file mode 100644 index 0000000..5f62bb7 --- /dev/null +++ b/components/keyboard_indicators.c @@ -0,0 +1,50 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +/* + * fmt consists of uppercase or lowercase 'c' for caps lock and/or 'n' for num + * lock, each optionally followed by '?', in the order of indicators desired. + * If followed by '?', the letter with case preserved is included in the output + * if the corresponding indicator is on. Otherwise, the letter is always + * included, lowercase when off and uppercase when on. + */ +const char * +keyboard_indicators(const char *fmt) +{ + Display *dpy; + XKeyboardState state; + size_t fmtlen, i, n; + int togglecase, isset; + char key; + + if (!(dpy = XOpenDisplay(NULL))) { + warn("XOpenDisplay: Failed to open display"); + return NULL; + } + XGetKeyboardControl(dpy, &state); + XCloseDisplay(dpy); + + fmtlen = strnlen(fmt, 4); + for (i = n = 0; i < fmtlen; i++) { + key = tolower(fmt[i]); + if (key != 'c' && key != 'n') + continue; + + togglecase = (i + 1 >= fmtlen || fmt[i + 1] != '?'); + isset = (state.led_mask & (1 << (key == 'n'))); + + if (togglecase) + buf[n++] = isset ? toupper(key) : key; + else if (isset) + buf[n++] = fmt[i]; + } + + buf[n] = 0; + return buf; +} diff --git a/components/keymap.c b/components/keymap.c new file mode 100644 index 0000000..22224f3 --- /dev/null +++ b/components/keymap.c @@ -0,0 +1,86 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +static int +valid_layout_or_variant(char *sym) +{ + size_t i; + /* invalid symbols from xkb rules config */ + static const char *invalid[] = { "evdev", "inet", "pc", "base" }; + + for (i = 0; i < LEN(invalid); i++) + if (!strncmp(sym, invalid[i], strlen(invalid[i]))) + return 0; + + return 1; +} + +static char * +get_layout(char *syms, int grp_num) +{ + char *tok, *layout; + int grp; + + layout = NULL; + tok = strtok(syms, "+:_"); + for (grp = 0; tok && grp <= grp_num; tok = strtok(NULL, "+:_")) { + if (!valid_layout_or_variant(tok)) { + continue; + } else if (strlen(tok) == 1 && isdigit(tok[0])) { + /* ignore :2, :3, :4 (additional layout groups) */ + continue; + } + layout = tok; + grp++; + } + + return layout; +} + +const char * +keymap(const char *unused) +{ + Display *dpy; + XkbDescRec *desc; + XkbStateRec state; + char *symbols; + const char *layout; + + layout = NULL; + + if (!(dpy = XOpenDisplay(NULL))) { + warn("XOpenDisplay: Failed to open display"); + return NULL; + } + if (!(desc = XkbAllocKeyboard())) { + warn("XkbAllocKeyboard: Failed to allocate keyboard"); + goto end; + } + if (XkbGetNames(dpy, XkbSymbolsNameMask, desc)) { + warn("XkbGetNames: Failed to retrieve key symbols"); + goto end; + } + if (XkbGetState(dpy, XkbUseCoreKbd, &state)) { + warn("XkbGetState: Failed to retrieve keyboard state"); + goto end; + } + if (!(symbols = XGetAtomName(dpy, desc->names->symbols))) { + warn("XGetAtomName: Failed to get atom name"); + goto end; + } + layout = bprintf("%s", get_layout(symbols, state.group)); + XFree(symbols); +end: + XkbFreeKeyboard(desc, XkbSymbolsNameMask, 1); + if (XCloseDisplay(dpy)) + warn("XCloseDisplay: Failed to close display"); + + return layout; +} diff --git a/components/load_avg.c b/components/load_avg.c new file mode 100644 index 0000000..f278a40 --- /dev/null +++ b/components/load_avg.c @@ -0,0 +1,19 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +load_avg(const char *unused) +{ + double avgs[3]; + + if (getloadavg(avgs, 3) < 0) { + warn("getloadavg: Failed to obtain load average"); + return NULL; + } + + return bprintf("%.2f %.2f %.2f", avgs[0], avgs[1], avgs[2]); +} diff --git a/components/netspeeds.c b/components/netspeeds.c new file mode 100644 index 0000000..cde6fa9 --- /dev/null +++ b/components/netspeeds.c @@ -0,0 +1,129 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + #include + + #define NET_RX_BYTES "/sys/class/net/%s/statistics/rx_bytes" + #define NET_TX_BYTES "/sys/class/net/%s/statistics/tx_bytes" + + const char * + netspeed_rx(const char *interface) + { + uintmax_t oldrxbytes; + static uintmax_t rxbytes; + extern const unsigned int interval; + char path[PATH_MAX]; + + oldrxbytes = rxbytes; + + if (esnprintf(path, sizeof(path), NET_RX_BYTES, interface) < 0) + return NULL; + if (pscanf(path, "%ju", &rxbytes) != 1) + return NULL; + if (oldrxbytes == 0) + return NULL; + + return fmt_human((rxbytes - oldrxbytes) * 1000 / interval, + 1024); + } + + const char * + netspeed_tx(const char *interface) + { + uintmax_t oldtxbytes; + static uintmax_t txbytes; + extern const unsigned int interval; + char path[PATH_MAX]; + + oldtxbytes = txbytes; + + if (esnprintf(path, sizeof(path), NET_TX_BYTES, interface) < 0) + return NULL; + if (pscanf(path, "%ju", &txbytes) != 1) + return NULL; + if (oldtxbytes == 0) + return NULL; + + return fmt_human((txbytes - oldtxbytes) * 1000 / interval, + 1024); + } +#elif defined(__OpenBSD__) | defined(__FreeBSD__) + #include + #include + #include + #include + #include + + const char * + netspeed_rx(const char *interface) + { + struct ifaddrs *ifal, *ifa; + struct if_data *ifd; + uintmax_t oldrxbytes; + static uintmax_t rxbytes; + extern const unsigned int interval; + int if_ok = 0; + + oldrxbytes = rxbytes; + + if (getifaddrs(&ifal) < 0) { + warn("getifaddrs failed"); + return NULL; + } + rxbytes = 0; + for (ifa = ifal; ifa; ifa = ifa->ifa_next) + if (!strcmp(ifa->ifa_name, interface) && + (ifd = (struct if_data *)ifa->ifa_data)) + rxbytes += ifd->ifi_ibytes, if_ok = 1; + + freeifaddrs(ifal); + if (!if_ok) { + warn("reading 'if_data' failed"); + return NULL; + } + if (oldrxbytes == 0) + return NULL; + + return fmt_human((rxbytes - oldrxbytes) * 1000 / interval, + 1024); + } + + const char * + netspeed_tx(const char *interface) + { + struct ifaddrs *ifal, *ifa; + struct if_data *ifd; + uintmax_t oldtxbytes; + static uintmax_t txbytes; + extern const unsigned int interval; + int if_ok = 0; + + oldtxbytes = txbytes; + + if (getifaddrs(&ifal) < 0) { + warn("getifaddrs failed"); + return NULL; + } + txbytes = 0; + for (ifa = ifal; ifa; ifa = ifa->ifa_next) + if (!strcmp(ifa->ifa_name, interface) && + (ifd = (struct if_data *)ifa->ifa_data)) + txbytes += ifd->ifi_obytes, if_ok = 1; + + freeifaddrs(ifal); + if (!if_ok) { + warn("reading 'if_data' failed"); + return NULL; + } + if (oldtxbytes == 0) + return NULL; + + return fmt_human((txbytes - oldtxbytes) * 1000 / interval, + 1024); + } +#endif diff --git a/components/num_files.c b/components/num_files.c new file mode 100644 index 0000000..df0acd1 --- /dev/null +++ b/components/num_files.c @@ -0,0 +1,32 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +num_files(const char *path) +{ + struct dirent *dp; + DIR *dir; + int num; + + if (!(dir = opendir(path))) { + warn("opendir '%s':", path); + return NULL; + } + + num = 0; + while ((dp = readdir(dir))) { + if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) + continue; /* skip self and parent */ + + num++; + } + + closedir(dir); + + return bprintf("%d", num); +} diff --git a/components/ram.c b/components/ram.c new file mode 100644 index 0000000..bf71dcf --- /dev/null +++ b/components/ram.c @@ -0,0 +1,227 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + #include + + const char * + ram_free(const char *unused) + { + uintmax_t free; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemFree:", "%ju kB", &free) != 1) { + fclose(fp); + return NULL; + } + + fclose(fp); + return fmt_human(free * 1024, 1024); + } + + const char * + ram_perc(const char *unused) + { + uintmax_t total, free, buffers, cached, shmem, sreclaimable; + int percent; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemTotal:", "%ju kB", &total) != 1 || + lscanf(fp, "MemFree:", "%ju kB", &free) != 1 || + lscanf(fp, "Buffers:", "%ju kB", &buffers) != 1 || + lscanf(fp, "Cached:", "%ju kB", &cached) != 1 || + lscanf(fp, "Shmem:", "%ju kB", &shmem) != 1 || + lscanf(fp, "SReclaimable:", "%ju kB", &sreclaimable) != 1) { + fclose(fp); + return NULL; + } + fclose(fp); + + if (total == 0) + return NULL; + + percent = 100 * (total - free - buffers - cached - sreclaimable + shmem) / total; + return bprintf("%d", percent); + } + + const char * + ram_total(const char *unused) + { + uintmax_t total; + + if (pscanf("/proc/meminfo", "MemTotal: %ju kB\n", &total) + != 1) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + ram_used(const char *unused) + { + uintmax_t total, free, buffers, cached, used, shmem, sreclaimable; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemTotal:", "%ju kB", &total) != 1 || + lscanf(fp, "MemFree:", "%ju kB", &free) != 1 || + lscanf(fp, "Buffers:", "%ju kB", &buffers) != 1 || + lscanf(fp, "Cached:", "%ju kB", &cached) != 1 || + lscanf(fp, "Shmem:", "%ju kB", &shmem) != 1 || + lscanf(fp, "SReclaimable:", "%ju kB", &sreclaimable) != 1) { + fclose(fp); + return NULL; + } + fclose(fp); + + used = total - free - buffers - cached - sreclaimable + shmem; + return fmt_human(used * 1024, 1024); + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + #define LOG1024 10 + #define pagetok(size, pageshift) (size_t)(size << (pageshift - LOG1024)) + + inline int + load_uvmexp(struct uvmexp *uvmexp) + { + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; + size_t size; + + size = sizeof(*uvmexp); + + if (sysctl(uvmexp_mib, 2, uvmexp, &size, NULL, 0) >= 0) + return 1; + + return 0; + } + + const char * + ram_free(const char *unused) + { + struct uvmexp uvmexp; + int free_pages; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + free_pages = uvmexp.npages - uvmexp.active; + return fmt_human(pagetok(free_pages, uvmexp.pageshift) * + 1024, 1024); + } + + const char * + ram_perc(const char *unused) + { + struct uvmexp uvmexp; + int percent; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + percent = uvmexp.active * 100 / uvmexp.npages; + return bprintf("%d", percent); + } + + const char * + ram_total(const char *unused) + { + struct uvmexp uvmexp; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + return fmt_human(pagetok(uvmexp.npages, + uvmexp.pageshift) * 1024, 1024); + } + + const char * + ram_used(const char *unused) + { + struct uvmexp uvmexp; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + return fmt_human(pagetok(uvmexp.active, + uvmexp.pageshift) * 1024, 1024); + } +#elif defined(__FreeBSD__) + #include + #include + #include + #include + + const char * + ram_free(const char *unused) { + struct vmtotal vm_stats; + int mib[] = {CTL_VM, VM_TOTAL}; + size_t len; + + len = sizeof(struct vmtotal); + if (sysctl(mib, 2, &vm_stats, &len, NULL, 0) < 0 + || !len) + return NULL; + + return fmt_human(vm_stats.t_free * getpagesize(), 1024); + } + + const char * + ram_total(const char *unused) { + unsigned int npages; + size_t len; + + len = sizeof(npages); + if (sysctlbyname("vm.stats.vm.v_page_count", + &npages, &len, NULL, 0) < 0 || !len) + return NULL; + + return fmt_human(npages * getpagesize(), 1024); + } + + const char * + ram_perc(const char *unused) { + unsigned int npages; + unsigned int active; + size_t len; + + len = sizeof(npages); + if (sysctlbyname("vm.stats.vm.v_page_count", + &npages, &len, NULL, 0) < 0 || !len) + return NULL; + + if (sysctlbyname("vm.stats.vm.v_active_count", + &active, &len, NULL, 0) < 0 || !len) + return NULL; + + return bprintf("%d", active * 100 / npages); + } + + const char * + ram_used(const char *unused) { + unsigned int active; + size_t len; + + len = sizeof(active); + if (sysctlbyname("vm.stats.vm.v_active_count", + &active, &len, NULL, 0) < 0 || !len) + return NULL; + + return fmt_human(active * getpagesize(), 1024); + } +#endif diff --git a/components/run_command.c b/components/run_command.c new file mode 100644 index 0000000..93bf6da --- /dev/null +++ b/components/run_command.c @@ -0,0 +1,31 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +run_command(const char *cmd) +{ + char *p; + FILE *fp; + + if (!(fp = popen(cmd, "r"))) { + warn("popen '%s':", cmd); + return NULL; + } + + p = fgets(buf, sizeof(buf) - 1, fp); + if (pclose(fp) < 0) { + warn("pclose '%s':", cmd); + return NULL; + } + if (!p) + return NULL; + + if ((p = strrchr(buf, '\n'))) + p[0] = '\0'; + + return buf[0] ? buf : NULL; +} diff --git a/components/swap.c b/components/swap.c new file mode 100644 index 0000000..dbd2747 --- /dev/null +++ b/components/swap.c @@ -0,0 +1,276 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + static int + get_swap_info(long *s_total, long *s_free, long *s_cached) + { + FILE *fp; + struct { + const char *name; + const size_t len; + long *var; + } ent[] = { + { "SwapTotal", sizeof("SwapTotal") - 1, s_total }, + { "SwapFree", sizeof("SwapFree") - 1, s_free }, + { "SwapCached", sizeof("SwapCached") - 1, s_cached }, + }; + size_t line_len = 0, i, left; + char *line = NULL; + + /* get number of fields we want to extract */ + for (i = 0, left = 0; i < LEN(ent); i++) + if (ent[i].var) + left++; + + if (!(fp = fopen("/proc/meminfo", "r"))) { + warn("fopen '/proc/meminfo':"); + return 1; + } + + /* read file line by line and extract field information */ + while (left > 0 && getline(&line, &line_len, fp) >= 0) { + for (i = 0; i < LEN(ent); i++) { + if (ent[i].var && + !strncmp(line, ent[i].name, ent[i].len)) { + sscanf(line + ent[i].len + 1, + "%ld kB\n", ent[i].var); + left--; + break; + } + } + } + free(line); + if (ferror(fp)) { + warn("getline '/proc/meminfo':"); + return 1; + } + + fclose(fp); + return 0; + } + + const char * + swap_free(const char *unused) + { + long free; + + if (get_swap_info(NULL, &free, NULL)) + return NULL; + + return fmt_human(free * 1024, 1024); + } + + const char * + swap_perc(const char *unused) + { + long total, free, cached; + + if (get_swap_info(&total, &free, &cached) || total == 0) + return NULL; + + return bprintf("%d", 100 * (total - free - cached) / total); + } + + const char * + swap_total(const char *unused) + { + long total; + + if (get_swap_info(&total, NULL, NULL)) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + swap_used(const char *unused) + { + long total, free, cached; + + if (get_swap_info(&total, &free, &cached)) + return NULL; + + return fmt_human((total - free - cached) * 1024, 1024); + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + static int + getstats(int *total, int *used) + { + struct swapent *sep, *fsep; + int rnswap, nswap, i; + + if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) < 1) { + warn("swapctl 'SWAP_NSWAP':"); + return 1; + } + if (!(fsep = sep = calloc(nswap, sizeof(*sep)))) { + warn("calloc 'nswap':"); + return 1; + } + if ((rnswap = swapctl(SWAP_STATS, (void *)sep, nswap)) < 0) { + warn("swapctl 'SWAP_STATS':"); + free(fsep); + return 1; + } + if (nswap != rnswap) { + warn("getstats: SWAP_STATS != SWAP_NSWAP"); + free(fsep); + return 1; + } + + *total = 0; + *used = 0; + + for (i = 0; i < rnswap; i++) { + *total += sep[i].se_nblks >> 1; + *used += sep[i].se_inuse >> 1; + } + + free(fsep); + + return 0; + } + + const char * + swap_free(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human((total - used) * 1024, 1024); + } + + const char * + swap_perc(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + if (total == 0) + return NULL; + + return bprintf("%d", 100 * used / total); + } + + const char * + swap_total(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + swap_used(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human(used * 1024, 1024); + } +#elif defined(__FreeBSD__) + #include + #include + #include + #include + #include + + static int getswapinfo(struct kvm_swap *swap_info, size_t size) + { + kvm_t *kd; + + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, NULL); + if (kd == NULL) { + warn("kvm_openfiles '/dev/null':"); + return 0; + } + + if (kvm_getswapinfo(kd, swap_info, size, 0 /* Unused flags */) < 0) { + warn("kvm_getswapinfo:"); + kvm_close(kd); + return 0; + } + + kvm_close(kd); + return 1; + } + + const char * + swap_free(const char *unused) + { + struct kvm_swap swap_info[1]; + long used, total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + used = swap_info[0].ksw_used; + + return fmt_human((total - used) * getpagesize(), 1024); + } + + const char * + swap_perc(const char *unused) + { + struct kvm_swap swap_info[1]; + long used, total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + used = swap_info[0].ksw_used; + + return bprintf("%d", used * 100 / total); + } + + const char * + swap_total(const char *unused) + { + struct kvm_swap swap_info[1]; + long total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + + return fmt_human(total * getpagesize(), 1024); + } + + const char * + swap_used(const char *unused) + { + struct kvm_swap swap_info[1]; + long used; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + used = swap_info[0].ksw_used; + + return fmt_human(used * getpagesize(), 1024); + } +#endif diff --git a/components/temperature.c b/components/temperature.c new file mode 100644 index 0000000..7cf1394 --- /dev/null +++ b/components/temperature.c @@ -0,0 +1,73 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include "../slstatus.h" +#include "../util.h" + + +#if defined(__linux__) + #include + + const char * + temp(const char *file) + { + uintmax_t temp; + + if (pscanf(file, "%ju", &temp) != 1) + return NULL; + + return bprintf("%ju", temp / 1000); + } +#elif defined(__OpenBSD__) + #include + #include /* before for struct timeval */ + #include + #include + + const char * + temp(const char *unused) + { + int mib[5]; + size_t size; + struct sensor temp; + + mib[0] = CTL_HW; + mib[1] = HW_SENSORS; + mib[2] = 0; /* cpu0 */ + mib[3] = SENSOR_TEMP; + mib[4] = 0; /* temp0 */ + + size = sizeof(temp); + + if (sysctl(mib, 5, &temp, &size, NULL, 0) < 0) { + warn("sysctl 'SENSOR_TEMP':"); + return NULL; + } + + /* kelvin to celsius */ + return bprintf("%d", (int)((float)(temp.value-273150000) / 1E6)); + } +#elif defined(__FreeBSD__) + #include + #include + #include + + #define ACPI_TEMP "hw.acpi.thermal.%s.temperature" + + const char * + temp(const char *zone) + { + char buf[256]; + int temp; + size_t len; + + len = sizeof(temp); + snprintf(buf, sizeof(buf), ACPI_TEMP, zone); + if (sysctlbyname(buf, &temp, &len, NULL, 0) < 0 + || !len) + return NULL; + + /* kelvin to decimal celcius */ + return bprintf("%d.%d", (temp - 2731) / 10, abs((temp - 2731) % 10)); + } +#endif diff --git a/components/uptime.c b/components/uptime.c new file mode 100644 index 0000000..6227f73 --- /dev/null +++ b/components/uptime.c @@ -0,0 +1,34 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(CLOCK_BOOTTIME) + #define UPTIME_FLAG CLOCK_BOOTTIME +#elif defined(CLOCK_UPTIME) + #define UPTIME_FLAG CLOCK_UPTIME +#else + #define UPTIME_FLAG CLOCK_MONOTONIC +#endif + +const char * +uptime(const char *unused) +{ + char warn_buf[256]; + uintmax_t h, m; + struct timespec uptime; + + if (clock_gettime(UPTIME_FLAG, &uptime) < 0) { + snprintf(warn_buf, sizeof(warn_buf), "clock_gettime %d", UPTIME_FLAG); + warn(warn_buf); + return NULL; + } + + h = uptime.tv_sec / 3600; + m = uptime.tv_sec % 3600 / 60; + + return bprintf("%juh %jum", h, m); +} diff --git a/components/user.c b/components/user.c new file mode 100644 index 0000000..3517495 --- /dev/null +++ b/components/user.c @@ -0,0 +1,33 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +gid(const char *unused) +{ + return bprintf("%d", getgid()); +} + +const char * +username(const char *unused) +{ + struct passwd *pw; + + if (!(pw = getpwuid(geteuid()))) { + warn("getpwuid '%d':", geteuid()); + return NULL; + } + + return bprintf("%s", pw->pw_name); +} + +const char * +uid(const char *unused) +{ + return bprintf("%d", geteuid()); +} diff --git a/components/volume.c b/components/volume.c new file mode 100644 index 0000000..20171da --- /dev/null +++ b/components/volume.c @@ -0,0 +1,224 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__OpenBSD__) | defined(__FreeBSD__) + #include + #include + #include + #include + + struct control { + LIST_ENTRY(control) next; + unsigned int addr; + #define CTRL_NONE 0 + #define CTRL_LEVEL 1 + #define CTRL_MUTE 2 + unsigned int type; + unsigned int maxval; + unsigned int val; + }; + + static LIST_HEAD(, control) controls = LIST_HEAD_INITIALIZER(controls); + static struct pollfd *pfds; + static struct sioctl_hdl *hdl; + static int initialized; + + /* + * Call-back to obtain the description of all audio controls. + */ + static void + ondesc(void *unused, struct sioctl_desc *desc, int val) + { + struct control *c, *ctmp; + unsigned int type = CTRL_NONE; + + if (desc == NULL) + return; + + /* Delete existing audio control with the same address. */ + LIST_FOREACH_SAFE(c, &controls, next, ctmp) { + if (desc->addr == c->addr) { + LIST_REMOVE(c, next); + free(c); + break; + } + } + + /* Only match output.level and output.mute audio controls. */ + if (desc->group[0] != 0 || + strcmp(desc->node0.name, "output") != 0) + return; + if (desc->type == SIOCTL_NUM && + strcmp(desc->func, "level") == 0) + type = CTRL_LEVEL; + else if (desc->type == SIOCTL_SW && + strcmp(desc->func, "mute") == 0) + type = CTRL_MUTE; + else + return; + + c = malloc(sizeof(struct control)); + if (c == NULL) { + warn("sndio: failed to allocate audio control\n"); + return; + } + + c->addr = desc->addr; + c->type = type; + c->maxval = desc->maxval; + c->val = val; + LIST_INSERT_HEAD(&controls, c, next); + } + + /* + * Call-back invoked whenever an audio control changes. + */ + static void + onval(void *unused, unsigned int addr, unsigned int val) + { + struct control *c; + + LIST_FOREACH(c, &controls, next) { + if (c->addr == addr) + break; + } + if (c == NULL) + return; + c->val = val; + } + + static void + cleanup(void) + { + struct control *c; + + if (hdl) { + sioctl_close(hdl); + hdl = NULL; + } + + free(pfds); + pfds = NULL; + + while (!LIST_EMPTY(&controls)) { + c = LIST_FIRST(&controls); + LIST_REMOVE(c, next); + free(c); + } + } + + static int + init(void) + { + hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0); + if (hdl == NULL) { + warn("sndio: cannot open device"); + goto failed; + } + + if (!sioctl_ondesc(hdl, ondesc, NULL)) { + warn("sndio: cannot set control description call-back"); + goto failed; + } + + if (!sioctl_onval(hdl, onval, NULL)) { + warn("sndio: cannot set control values call-back"); + goto failed; + } + + pfds = calloc(sioctl_nfds(hdl), sizeof(struct pollfd)); + if (pfds == NULL) { + warn("sndio: cannot allocate pollfd structures"); + goto failed; + } + + return 1; + failed: + cleanup(); + return 0; + } + + const char * + vol_perc(const char *unused) + { + struct control *c; + int n, v, value; + + if (!initialized) + initialized = init(); + + if (hdl == NULL) + return NULL; + + n = sioctl_pollfd(hdl, pfds, POLLIN); + if (n > 0) { + n = poll(pfds, n, 0); + if (n > 0) { + if (sioctl_revents(hdl, pfds) & POLLHUP) { + warn("sndio: disconnected"); + cleanup(); + initialized = 0; + return NULL; + } + } + } + + value = 100; + LIST_FOREACH(c, &controls, next) { + if (c->type == CTRL_MUTE && c->val == 1) + value = 0; + else if (c->type == CTRL_LEVEL) { + v = (c->val * 100 + c->maxval / 2) / c->maxval; + /* For multiple channels return the minimum. */ + if (v < value) + value = v; + } + } + + return bprintf("%d", value); + } +#else + #include + + const char * + vol_perc(const char *card) + { + size_t i; + int v, afd, devmask; + char *vnames[] = SOUND_DEVICE_NAMES; + + if ((afd = open(card, O_RDONLY | O_NONBLOCK)) < 0) { + warn("open '%s':", card); + return NULL; + } + + if (ioctl(afd, (int)SOUND_MIXER_READ_DEVMASK, &devmask) < 0) { + warn("ioctl 'SOUND_MIXER_READ_DEVMASK':"); + close(afd); + return NULL; + } + for (i = 0; i < LEN(vnames); i++) { + if (devmask & (1 << i) && !strcmp("vol", vnames[i])) { + if (ioctl(afd, MIXER_READ(i), &v) < 0) { + warn("ioctl 'MIXER_READ(%ld)':", i); + close(afd); + return NULL; + } + close(afd); + return bprintf("%d", v & 0xff); + } + } + + close(afd); + warn("vol_perc: no 'vol' mixer device"); + + return NULL; + } +#endif diff --git a/components/wifi.c b/components/wifi.c new file mode 100644 index 0000000..806a7ad --- /dev/null +++ b/components/wifi.c @@ -0,0 +1,417 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#define RSSI_TO_PERC(rssi) \ + rssi >= -50 ? 100 : \ + (rssi <= -100 ? 0 : \ + (2 * (rssi + 100))) + +#if defined(__linux__) + #include + #include + #include + #include + #include + + static int nlsock = -1; + static uint32_t seq = 1; + static char resp[4096 + 1]; + + static char * + findattr(int attr, const char *p, const char *e, size_t *len) + { + while (p < e) { + struct nlattr nla; + memcpy(&nla, p, sizeof(nla)); + if (nla.nla_type == attr) { + *len = nla.nla_len - NLA_HDRLEN; + return (char *)(p + NLA_HDRLEN); + } + p += NLA_ALIGN(nla.nla_len); + } + return NULL; + } + + static uint16_t + nl80211fam(void) + { + static const char family[] = "nl80211"; + static uint16_t id; + ssize_t r; + size_t len; + char ctrl[NLMSG_HDRLEN+GENL_HDRLEN+NLA_HDRLEN+NLA_ALIGN(sizeof(family))] = {0}, *p = ctrl; + + if (id) + return id; + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(ctrl), + .nlmsg_type = GENL_ID_CTRL, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = CTRL_CMD_GETFAMILY, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN+sizeof(family), + .nla_type = CTRL_ATTR_FAMILY_NAME, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, family, sizeof(family)); + + if (nlsock < 0) + nlsock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); + if (nlsock < 0) { + warn("socket 'AF_NETLINK':"); + return 0; + } + if (send(nlsock, ctrl, sizeof(ctrl), 0) != sizeof(ctrl)) { + warn("send 'AF_NETLINK':"); + return 0; + } + r = recv(nlsock, resp, sizeof(resp), 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return 0; + } + if ((size_t)r <= sizeof(ctrl)) + return 0; + p = findattr(CTRL_ATTR_FAMILY_ID, resp + sizeof(ctrl), resp + r, &len); + if (p && len == 2) + memcpy(&id, p, 2); + + return id; + } + + static int + ifindex(const char *interface) + { + static struct ifreq ifr; + static int ifsock = -1; + + if (ifsock < 0) + ifsock = socket(AF_UNIX, SOCK_DGRAM, 0); + if (ifsock < 0) { + warn("socket 'AF_UNIX':"); + return -1; + } + if (strcmp(ifr.ifr_name, interface) != 0) { + if (snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), + "%s", interface) >= (int)sizeof(ifr.ifr_name)) { + warn("interface name too long: '%s'", interface); + return -1; + } + } + if (ioctl(ifsock, SIOCGIFINDEX, &ifr) != 0) { + warn("ioctl 'SIOCGIFINDEX':"); + return -1; + } + return ifr.ifr_ifindex; + } + + const char * + wifi_essid(const char *interface) + { + uint16_t fam = nl80211fam(); + ssize_t r; + size_t len; + char req[NLMSG_HDRLEN+GENL_HDRLEN+NLA_HDRLEN+NLA_ALIGN(4)] = {0}, *p = req; + int idx = ifindex(interface); + if (!fam) { + fprintf(stderr, "nl80211 family not found\n"); + return NULL; + } + if (idx < 0) { + fprintf(stderr, "interface %s not found\n", interface); + return NULL; + } + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(req), + .nlmsg_type = fam, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = NL80211_CMD_GET_INTERFACE, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN+4, + .nla_type = NL80211_ATTR_IFINDEX, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, &(uint32_t){idx}, 4); + + if (send(nlsock, req, sizeof(req), 0) != sizeof(req)) { + warn("send 'AF_NETLINK':"); + return NULL; + } + r = recv(nlsock, resp, sizeof(resp) - 1, 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return NULL; + } + + if ((size_t)r <= NLMSG_HDRLEN + GENL_HDRLEN) + return NULL; + p = findattr(NL80211_ATTR_SSID, resp + NLMSG_HDRLEN + GENL_HDRLEN, resp + r, &len); + if (p) + p[len] = 0; + + return p; + } + + const char * + wifi_perc(const char *interface) + { + static char strength[4]; + struct nlmsghdr hdr; + uint16_t fam = nl80211fam(); + ssize_t r; + size_t len; + char req[NLMSG_HDRLEN + GENL_HDRLEN + NLA_HDRLEN + NLA_ALIGN(4)] = {0}, *p = req, *e; + int idx = ifindex(interface); + + if (idx < 0) { + fprintf(stderr, "interface %s not found\n", interface); + return NULL; + } + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(req), + .nlmsg_type = fam, + .nlmsg_flags = NLM_F_REQUEST|NLM_F_DUMP, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = NL80211_CMD_GET_STATION, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN + 4, + .nla_type = NL80211_ATTR_IFINDEX, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, &idx, 4); + + if (send(nlsock, req, sizeof(req), 0) != sizeof(req)) { + warn("send 'AF_NETLINK':"); + return NULL; + } + + *strength = 0; + while (1) { + r = recv(nlsock, resp, sizeof(resp), 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return NULL; + } + if ((size_t)r < sizeof(hdr)) + return NULL; + + for (p = resp; p != resp + r && (size_t)(resp + r-p) >= sizeof(hdr); p = e) { + memcpy(&hdr, p, sizeof(hdr)); + e = resp + r - p < hdr.nlmsg_len ? resp + r : p + hdr.nlmsg_len; + + if (!*strength && hdr.nlmsg_len > NLMSG_HDRLEN+GENL_HDRLEN) { + p += NLMSG_HDRLEN+GENL_HDRLEN; + p = findattr(NL80211_ATTR_STA_INFO, p, e, &len); + if (p) + p = findattr(NL80211_STA_INFO_SIGNAL_AVG, p, e, &len); + if (p && len == 1) + snprintf(strength, sizeof(strength), "%d", RSSI_TO_PERC(*p)); + } + if (hdr.nlmsg_type == NLMSG_DONE) + return *strength ? strength : NULL; + } + } + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include /* before for NBBY */ + #include + #include + #include + + static int + load_ieee80211_nodereq(const char *interface, struct ieee80211_nodereq *nr) + { + struct ieee80211_bssid bssid; + int sockfd; + uint8_t zero_bssid[IEEE80211_ADDR_LEN]; + + memset(&bssid, 0, sizeof(bssid)); + memset(nr, 0, sizeof(struct ieee80211_nodereq)); + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return 0; + } + strlcpy(bssid.i_name, interface, sizeof(bssid.i_name)); + if ((ioctl(sockfd, SIOCG80211BSSID, &bssid)) < 0) { + warn("ioctl 'SIOCG80211BSSID':"); + close(sockfd); + return 0; + } + memset(&zero_bssid, 0, sizeof(zero_bssid)); + if (memcmp(bssid.i_bssid, zero_bssid, + IEEE80211_ADDR_LEN) == 0) { + close(sockfd); + return 0; + } + strlcpy(nr->nr_ifname, interface, sizeof(nr->nr_ifname)); + memcpy(&nr->nr_macaddr, bssid.i_bssid, sizeof(nr->nr_macaddr)); + if ((ioctl(sockfd, SIOCG80211NODE, nr)) < 0 && nr->nr_rssi) { + warn("ioctl 'SIOCG80211NODE':"); + close(sockfd); + return 0; + } + + return close(sockfd), 1; + } + + const char * + wifi_perc(const char *interface) + { + struct ieee80211_nodereq nr; + int q; + + if (load_ieee80211_nodereq(interface, &nr)) { + if (nr.nr_max_rssi) + q = IEEE80211_NODEREQ_RSSI(&nr); + else + q = RSSI_TO_PERC(nr.nr_rssi); + + return bprintf("%d", q); + } + + return NULL; + } + + const char * + wifi_essid(const char *interface) + { + struct ieee80211_nodereq nr; + + if (load_ieee80211_nodereq(interface, &nr)) + return bprintf("%s", nr.nr_nwid); + + return NULL; + } +#elif defined(__FreeBSD__) + #include + #include + + int + load_ieee80211req(int sock, const char *interface, void *data, int type, size_t *len) + { + char warn_buf[256]; + struct ieee80211req ireq; + memset(&ireq, 0, sizeof(ireq)); + ireq.i_type = type; + ireq.i_data = (caddr_t) data; + ireq.i_len = *len; + + strlcpy(ireq.i_name, interface, sizeof(ireq.i_name)); + if (ioctl(sock, SIOCG80211, &ireq) < 0) { + snprintf(warn_buf, sizeof(warn_buf), + "ioctl: 'SIOCG80211': %d", type); + warn(warn_buf); + return 0; + } + + *len = ireq.i_len; + return 1; + } + + const char * + wifi_perc(const char *interface) + { + union { + struct ieee80211req_sta_req sta; + uint8_t buf[24 * 1024]; + } info; + uint8_t bssid[IEEE80211_ADDR_LEN]; + int rssi_dbm; + int sockfd; + size_t len; + const char *fmt; + + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return NULL; + } + + /* Retreive MAC address of interface */ + len = IEEE80211_ADDR_LEN; + fmt = NULL; + if (load_ieee80211req(sockfd, interface, &bssid, IEEE80211_IOC_BSSID, &len)) + { + /* Retrieve info on station with above BSSID */ + memset(&info, 0, sizeof(info)); + memcpy(info.sta.is_u.macaddr, bssid, sizeof(bssid)); + + len = sizeof(info); + if (load_ieee80211req(sockfd, interface, &info, IEEE80211_IOC_STA_INFO, &len)) { + rssi_dbm = info.sta.info[0].isi_noise + + info.sta.info[0].isi_rssi / 2; + + fmt = bprintf("%d", RSSI_TO_PERC(rssi_dbm)); + } + } + + close(sockfd); + return fmt; + } + + const char * + wifi_essid(const char *interface) + { + char ssid[IEEE80211_NWID_LEN + 1]; + size_t len; + int sockfd; + const char *fmt; + + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return NULL; + } + + fmt = NULL; + len = sizeof(ssid); + memset(&ssid, 0, len); + if (load_ieee80211req(sockfd, interface, &ssid, IEEE80211_IOC_SSID, &len)) { + if (len < sizeof(ssid)) + len += 1; + else + len = sizeof(ssid); + + ssid[len - 1] = '\0'; + fmt = bprintf("%s", ssid); + } + + close(sockfd); + return fmt; + } +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..100093e --- /dev/null +++ b/config.def.h @@ -0,0 +1,70 @@ +/* See LICENSE file for copyright and license details. */ + +/* interval between updates (in ms) */ +const unsigned int interval = 1000; + +/* text to show if no value can be retrieved */ +static const char unknown_str[] = "n/a"; + +/* maximum output string length */ +#define MAXLEN 2048 + +/* + * function description argument (example) + * + * battery_perc battery percentage battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * battery_remaining battery remaining HH:MM battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * battery_state battery charging state battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * cat read arbitrary file path + * cpu_freq cpu frequency in MHz NULL + * cpu_perc cpu usage in percent NULL + * datetime date and time format string (%F %T) + * disk_free free disk space in GB mountpoint path (/) + * disk_perc disk usage in percent mountpoint path (/) + * disk_total total disk space in GB mountpoint path (/) + * disk_used used disk space in GB mountpoint path (/) + * entropy available entropy NULL + * gid GID of current user NULL + * hostname hostname NULL + * ipv4 IPv4 address interface name (eth0) + * ipv6 IPv6 address interface name (eth0) + * kernel_release `uname -r` NULL + * keyboard_indicators caps/num lock indicators format string (c?n?) + * see keyboard_indicators.c + * keymap layout (variant) of current NULL + * keymap + * load_avg load average NULL + * netspeed_rx receive network speed interface name (wlan0) + * netspeed_tx transfer network speed interface name (wlan0) + * num_files number of files in a directory path + * (/home/foo/Inbox/cur) + * ram_free free memory in GB NULL + * ram_perc memory usage in percent NULL + * ram_total total memory size in GB NULL + * ram_used used memory in GB NULL + * run_command custom shell command command (echo foo) + * swap_free free swap in GB NULL + * swap_perc swap usage in percent NULL + * swap_total total swap size in GB NULL + * swap_used used swap in GB NULL + * temp temperature in degree celsius sensor file + * (/sys/class/thermal/...) + * NULL on OpenBSD + * thermal zone on FreeBSD + * (tz0, tz1, etc.) + * uid UID of current user NULL + * up interface is running interface name (eth0) + * uptime system uptime NULL + * username username of current user NULL + * vol_perc OSS/ALSA volume in percent mixer file (/dev/mixer) + * NULL on OpenBSD/FreeBSD + * wifi_essid WiFi ESSID interface name (wlan0) + * wifi_perc WiFi signal in percent interface name (wlan0) + */ +static const struct arg args[] = { + /* function format argument */ + { datetime, "%s", "%F %T" }, +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..a8f5c23 --- /dev/null +++ b/config.mk @@ -0,0 +1,22 @@ +# slstatus version +VERSION = 1.1 + +# customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# flags +CPPFLAGS = -I$(X11INC) -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" +CFLAGS = -std=c99 -pedantic -Wall -Wextra -Wno-unused-parameter -Os +LDFLAGS = -L$(X11LIB) -s +# OpenBSD: add -lsndio +# FreeBSD: add -lkvm -lsndio +LDLIBS = -lX11 + +# compiler and linker +CC = cc diff --git a/slstatus.1 b/slstatus.1 new file mode 100644 index 0000000..73e7a60 --- /dev/null +++ b/slstatus.1 @@ -0,0 +1,47 @@ +.Dd 2023-04-23 +.Dt SLSTATUS 1 +.Os +.Sh NAME +.Nm slstatus +.Nd suckless status +.Sh SYNOPSIS +.Nm +.Op Fl s +.Op Fl 1 +.Sh DESCRIPTION +.Nm +is a small tool for providing system status information to other programs +over the EWMH +.Em WM_NAME +property of the root window (used by +.Xr dwm 1 ) or standard input/output. It is designed to be as efficient as possible by +only issuing the minimum of system calls required. +.P +By default, +.Nm +outputs to WM_NAME. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl v +Print version information to stderr, then exit. +.It Fl s +Write to stdout instead of WM_NAME. +.It Fl 1 +Write once to stdout and quit. +.El +.Sh CUSTOMIZATION +.Nm +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.Sh SIGNALS +.Nm +responds to the following signals: +.Pp +.Bl -tag -width TERM -compact +.It USR1 +Triggers an instant redraw. +.El +.Sh AUTHORS +See the LICENSE file for the authors. +.Sh SEE ALSO +.Xr dwm 1 diff --git a/slstatus.c b/slstatus.c new file mode 100644 index 0000000..16d88fe --- /dev/null +++ b/slstatus.c @@ -0,0 +1,135 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#include "arg.h" +#include "slstatus.h" +#include "util.h" + +struct arg { + const char *(*func)(const char *); + const char *fmt; + const char *args; +}; + +char buf[1024]; +static volatile sig_atomic_t done; +static Display *dpy; + +#include "config.h" + +static void +terminate(const int signo) +{ + if (signo != SIGUSR1) + done = 1; +} + +static void +difftimespec(struct timespec *res, struct timespec *a, struct timespec *b) +{ + res->tv_sec = a->tv_sec - b->tv_sec - (a->tv_nsec < b->tv_nsec); + res->tv_nsec = a->tv_nsec - b->tv_nsec + + (a->tv_nsec < b->tv_nsec) * 1E9; +} + +static void +usage(void) +{ + die("usage: %s [-v] [-s] [-1]", argv0); +} + +int +main(int argc, char *argv[]) +{ + struct sigaction act; + struct timespec start, current, diff, intspec, wait; + size_t i, len; + int sflag, ret; + char status[MAXLEN]; + const char *res; + + sflag = 0; + ARGBEGIN { + case 'v': + die("slstatus-"VERSION); + break; + case '1': + done = 1; + /* FALLTHROUGH */ + case 's': + sflag = 1; + break; + default: + usage(); + } ARGEND + + if (argc) + usage(); + + memset(&act, 0, sizeof(act)); + act.sa_handler = terminate; + sigaction(SIGINT, &act, NULL); + sigaction(SIGTERM, &act, NULL); + act.sa_flags |= SA_RESTART; + sigaction(SIGUSR1, &act, NULL); + + if (!sflag && !(dpy = XOpenDisplay(NULL))) + die("XOpenDisplay: Failed to open display"); + + do { + if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) + die("clock_gettime:"); + + status[0] = '\0'; + for (i = len = 0; i < LEN(args); i++) { + if (!(res = args[i].func(args[i].args))) + res = unknown_str; + + if ((ret = esnprintf(status + len, sizeof(status) - len, + args[i].fmt, res)) < 0) + break; + + len += ret; + } + + if (sflag) { + puts(status); + fflush(stdout); + if (ferror(stdout)) + die("puts:"); + } else { + if (XStoreName(dpy, DefaultRootWindow(dpy), status) < 0) + die("XStoreName: Allocation failed"); + XFlush(dpy); + } + + if (!done) { + if (clock_gettime(CLOCK_MONOTONIC, ¤t) < 0) + die("clock_gettime:"); + difftimespec(&diff, ¤t, &start); + + intspec.tv_sec = interval / 1000; + intspec.tv_nsec = (interval % 1000) * 1E6; + difftimespec(&wait, &intspec, &diff); + + if (wait.tv_sec >= 0 && + nanosleep(&wait, NULL) < 0 && + errno != EINTR) + die("nanosleep:"); + } + } while (!done); + + if (!sflag) { + XStoreName(dpy, DefaultRootWindow(dpy), NULL); + if (XCloseDisplay(dpy) < 0) + die("XCloseDisplay: Failed to close display"); + } + + return 0; +} diff --git a/slstatus.h b/slstatus.h new file mode 100644 index 0000000..394281c --- /dev/null +++ b/slstatus.h @@ -0,0 +1,85 @@ +/* See LICENSE file for copyright and license details. */ + +/* battery */ +const char *battery_perc(const char *); +const char *battery_remaining(const char *); +const char *battery_state(const char *); + +/* cat */ +const char *cat(const char *path); + +/* cpu */ +const char *cpu_freq(const char *unused); +const char *cpu_perc(const char *unused); + +/* datetime */ +const char *datetime(const char *fmt); + +/* disk */ +const char *disk_free(const char *path); +const char *disk_perc(const char *path); +const char *disk_total(const char *path); +const char *disk_used(const char *path); + +/* entropy */ +const char *entropy(const char *unused); + +/* hostname */ +const char *hostname(const char *unused); + +/* ip */ +const char *ipv4(const char *interface); +const char *ipv6(const char *interface); +const char *up(const char *interface); + +/* kernel_release */ +const char *kernel_release(const char *unused); + +/* keyboard_indicators */ +const char *keyboard_indicators(const char *fmt); + +/* keymap */ +const char *keymap(const char *unused); + +/* load_avg */ +const char *load_avg(const char *unused); + +/* netspeeds */ +const char *netspeed_rx(const char *interface); +const char *netspeed_tx(const char *interface); + +/* num_files */ +const char *num_files(const char *path); + +/* ram */ +const char *ram_free(const char *unused); +const char *ram_perc(const char *unused); +const char *ram_total(const char *unused); +const char *ram_used(const char *unused); + +/* run_command */ +const char *run_command(const char *cmd); + +/* swap */ +const char *swap_free(const char *unused); +const char *swap_perc(const char *unused); +const char *swap_total(const char *unused); +const char *swap_used(const char *unused); + +/* temperature */ +const char *temp(const char *); + +/* uptime */ +const char *uptime(const char *unused); + +/* user */ +const char *gid(const char *unused); +const char *uid(const char *unused); +const char *username(const char *unused); + +/* volume */ +const char *vol_perc(const char *card); + +/* wifi */ +const char *wifi_essid(const char *interface); +const char *wifi_perc(const char *interface); diff --git a/util.c b/util.c new file mode 100644 index 0000000..d33cf9d --- /dev/null +++ b/util.c @@ -0,0 +1,158 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include + +#include "util.h" + +char *argv0; + +static void +verr(const char *fmt, va_list ap) +{ + vfprintf(stderr, fmt, ap); + + if (fmt[0] && fmt[strlen(fmt) - 1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } +} + +void +warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); + + exit(1); +} + +static int +evsnprintf(char *str, size_t size, const char *fmt, va_list ap) +{ + int ret; + + ret = vsnprintf(str, size, fmt, ap); + + if (ret < 0) { + warn("vsnprintf:"); + return -1; + } else if ((size_t)ret >= size) { + warn("vsnprintf: Output truncated"); + return -1; + } + + return ret; +} + +int +esnprintf(char *str, size_t size, const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = evsnprintf(str, size, fmt, ap); + va_end(ap); + + return ret; +} + +const char * +bprintf(const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = evsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + return (ret < 0) ? NULL : buf; +} + +const char * +fmt_human(uintmax_t num, int base) +{ + double scaled; + size_t i, prefixlen; + const char **prefix; + const char *prefix_1000[] = { "", "k", "M", "G", "T", "P", "E", "Z", + "Y" }; + const char *prefix_1024[] = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", + "Zi", "Yi" }; + + switch (base) { + case 1000: + prefix = prefix_1000; + prefixlen = LEN(prefix_1000); + break; + case 1024: + prefix = prefix_1024; + prefixlen = LEN(prefix_1024); + break; + default: + warn("fmt_human: Invalid base"); + return NULL; + } + + scaled = num; + for (i = 0; i < prefixlen && scaled >= base; i++) + scaled /= base; + + return bprintf("%.1f %s", scaled, prefix[i]); +} + +int +pscanf(const char *path, const char *fmt, ...) +{ + FILE *fp; + va_list ap; + int n; + + if (!(fp = fopen(path, "r"))) { + warn("fopen '%s':", path); + return -1; + } + va_start(ap, fmt); + n = vfscanf(fp, fmt, ap); + va_end(ap); + fclose(fp); + + return (n == EOF) ? -1 : n; +} + +int +lscanf(FILE *fp, const char *key, const char *fmt, void *res) +{ + int n; + char line[256]; + + n = -1; + while (fgets(line, sizeof(line), fp)) + if (strncmp(line, key, strlen(key)) == 0) { + n = sscanf(line + strlen(key), fmt, res); + break; + } + + rewind(fp); + return (n == 1) ? 1 : -1; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..7a960aa --- /dev/null +++ b/util.h @@ -0,0 +1,18 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +extern char buf[1024]; + +#define LEN(x) (sizeof(x) / sizeof((x)[0])) + +extern char *argv0; + +void warn(const char *, ...); +void die(const char *, ...); + +int esnprintf(char *str, size_t size, const char *fmt, ...); +const char *bprintf(const char *fmt, ...); +const char *fmt_human(uintmax_t num, int base); +int pscanf(const char *path, const char *fmt, ...); +int lscanf(FILE *fp, const char *key, const char *fmt, void *res); From c888bafafb28d6bebfca75ea446d24d0cb736749 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 06/11] Squashed 'suckless/dmenu/' content from commit 7175c48 git-subtree-dir: suckless/dmenu git-subtree-split: 7175c4880bac3d2a2d4a6262b59193f0a38e2fdb --- LICENSE | 30 ++ Makefile | 58 ++++ README | 24 ++ arg.h | 49 ++++ config.def.h | 23 ++ config.mk | 32 +++ dmenu.1 | 204 +++++++++++++ dmenu.c | 799 +++++++++++++++++++++++++++++++++++++++++++++++++++ dmenu_path | 13 + dmenu_run | 2 + drw.c | 471 ++++++++++++++++++++++++++++++ drw.h | 60 ++++ stest.1 | 90 ++++++ stest.c | 109 +++++++ util.c | 37 +++ util.h | 9 + 16 files changed, 2010 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 arg.h create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 dmenu.1 create mode 100644 dmenu.c create mode 100755 dmenu_path create mode 100755 dmenu_run create mode 100644 drw.c create mode 100644 drw.h create mode 100644 stest.1 create mode 100644 stest.c create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a64b28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +MIT/X Consortium License + +© 2006-2019 Anselm R Garbe +© 2006-2008 Sander van Dijk +© 2006-2007 Michał Janeczek +© 2007 Kris Maglione +© 2009 Gottox +© 2009 Markus Schnalke +© 2009 Evan Gates +© 2010-2012 Connor Lane Smith +© 2014-2022 Hiltjo Posthuma +© 2015-2019 Quentin Rameau + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..458c524 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# dmenu - dynamic menu +# See LICENSE file for copyright and license details. + +include config.mk + +SRC = drw.c dmenu.c stest.c util.c +OBJ = $(SRC:.c=.o) + +all: dmenu stest + +.c.o: + $(CC) -c $(CFLAGS) $< + +config.h: + cp config.def.h $@ + +$(OBJ): arg.h config.h config.mk drw.h + +dmenu: dmenu.o drw.o util.o + $(CC) -o $@ dmenu.o drw.o util.o $(LDFLAGS) + +stest: stest.o + $(CC) -o $@ stest.o $(LDFLAGS) + +clean: + rm -f dmenu stest $(OBJ) dmenu-$(VERSION).tar.gz + +dist: clean + mkdir -p dmenu-$(VERSION) + cp LICENSE Makefile README arg.h config.def.h config.mk dmenu.1\ + drw.h util.h dmenu_path dmenu_run stest.1 $(SRC)\ + dmenu-$(VERSION) + tar -cf dmenu-$(VERSION).tar dmenu-$(VERSION) + gzip dmenu-$(VERSION).tar + rm -rf dmenu-$(VERSION) + +install: all + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f dmenu dmenu_path dmenu_run stest $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu + chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_path + chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_run + chmod 755 $(DESTDIR)$(PREFIX)/bin/stest + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < dmenu.1 > $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 + sed "s/VERSION/$(VERSION)/g" < stest.1 > $(DESTDIR)$(MANPREFIX)/man1/stest.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/stest.1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/dmenu\ + $(DESTDIR)$(PREFIX)/bin/dmenu_path\ + $(DESTDIR)$(PREFIX)/bin/dmenu_run\ + $(DESTDIR)$(PREFIX)/bin/stest\ + $(DESTDIR)$(MANPREFIX)/man1/dmenu.1\ + $(DESTDIR)$(MANPREFIX)/man1/stest.1 + +.PHONY: all clean dist install uninstall diff --git a/README b/README new file mode 100644 index 0000000..a8fcdfe --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +dmenu - dynamic menu +==================== +dmenu is an efficient dynamic menu for X. + + +Requirements +------------ +In order to build dmenu you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (dmenu is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install dmenu +(if necessary as root): + + make clean install + + +Running dmenu +------------- +See the man page for details. diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..e94e02b --- /dev/null +++ b/arg.h @@ -0,0 +1,49 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..1edb647 --- /dev/null +++ b/config.def.h @@ -0,0 +1,23 @@ +/* See LICENSE file for copyright and license details. */ +/* Default settings; can be overriden by command line. */ + +static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */ +/* -fn option overrides fonts[0]; default X11 font or font set */ +static const char *fonts[] = { + "monospace:size=10" +}; +static const char *prompt = NULL; /* -p option; prompt to the left of input field */ +static const char *colors[SchemeLast][2] = { + /* fg bg */ + [SchemeNorm] = { "#bbbbbb", "#222222" }, + [SchemeSel] = { "#eeeeee", "#005577" }, + [SchemeOut] = { "#000000", "#00ffff" }, +}; +/* -l option; if nonzero, dmenu uses vertical list with given number of lines */ +static unsigned int lines = 0; + +/* + * Characters not considered part of a word while deleting words + * for example: " /?\"&[]" + */ +static const char worddelimiters[] = " "; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..dcc5bb3 --- /dev/null +++ b/config.mk @@ -0,0 +1,32 @@ +# dmenu version +VERSION = 5.4 + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# Xinerama, comment if you don't want it +XINERAMALIBS = -lXinerama +XINERAMAFLAGS = -DXINERAMA + +# freetype +FREETYPELIBS = -lfontconfig -lXft +FREETYPEINC = /usr/include/freetype2 +# OpenBSD (uncomment) +#FREETYPEINC = $(X11INC)/freetype2 +#MANPREFIX = ${PREFIX}/man + +# includes and libs +INCS = -I$(X11INC) -I$(FREETYPEINC) +LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS) +CFLAGS = -std=c99 -pedantic -Wall -Os $(INCS) $(CPPFLAGS) +LDFLAGS = $(LIBS) + +# compiler and linker +CC = cc diff --git a/dmenu.1 b/dmenu.1 new file mode 100644 index 0000000..ed0e442 --- /dev/null +++ b/dmenu.1 @@ -0,0 +1,204 @@ +.TH DMENU 1 dmenu\-VERSION +.SH NAME +dmenu \- dynamic menu +.SH SYNOPSIS +.B dmenu +.RB [ \-bfiv ] +.RB [ \-l +.IR lines ] +.RB [ \-m +.IR monitor ] +.RB [ \-p +.IR prompt ] +.RB [ \-fn +.IR font ] +.RB [ \-nb +.IR color ] +.RB [ \-nf +.IR color ] +.RB [ \-sb +.IR color ] +.RB [ \-sf +.IR color ] +.RB [ \-ob +.IR color ] +.RB [ \-of +.IR color ] +.RB [ \-w +.IR windowid ] +.P +.BR dmenu_run " ..." +.SH DESCRIPTION +.B dmenu +is a dynamic menu for X, which reads a list of newline\-separated items from +stdin. When the user selects an item and presses Return, their choice is printed +to stdout and dmenu terminates. Entering text will narrow the items to those +matching the tokens in the input. +.P +.B dmenu_run +is a script used by +.IR dwm (1) +which lists programs in the user's $PATH and runs the result in their $SHELL. +.SH OPTIONS +.TP +.B \-b +dmenu appears at the bottom of the screen. +.TP +.B \-f +dmenu grabs the keyboard before reading stdin if not reading from a tty. This +is faster, but will lock up X until stdin reaches end\-of\-file. +.TP +.B \-i +dmenu matches menu items case insensitively. +.TP +.BI \-l " lines" +dmenu lists items vertically, with the given number of lines. +.TP +.BI \-m " monitor" +dmenu is displayed on the monitor number supplied. Monitor numbers are starting +from 0. +.TP +.BI \-p " prompt" +defines the prompt to be displayed to the left of the input field. +.TP +.BI \-fn " font" +defines the font or font set used. +.TP +.BI \-nb " color" +defines the normal background color. +.IR #RGB , +.IR #RRGGBB , +and X color names are supported. +.TP +.BI \-nf " color" +defines the normal foreground color. +.TP +.BI \-sb " color" +defines the selected background color. +.TP +.BI \-sf " color" +defines the selected foreground color. +.TP +.BI \-ob " color" +defines the outline background color (for multiple selection). +.TP +.BI \-of " color" +defines the outline foreground color (for multiple selection). +.TP +.B \-v +prints version information to stdout, then exits. +.TP +.BI \-w " windowid" +embed into windowid. +.SH USAGE +dmenu is completely controlled by the keyboard. Items are selected using the +arrow keys, page up, page down, home, and end. +.TP +.B Tab +Copy the selected item to the input field. +.TP +.B Return +Confirm selection. Prints the selected item to stdout and exits, returning +success. +.TP +.B Ctrl-Return +Confirm selection. Prints the selected item to stdout and continues. +.TP +.B Shift\-Return +Confirm input. Prints the input text to stdout and exits, returning success. +.TP +.B Escape +Exit without selecting an item, returning failure. +.TP +.B Ctrl-Left +Move cursor to the start of the current word +.TP +.B Ctrl-Right +Move cursor to the end of the current word +.TP +.B C\-a +Home +.TP +.B C\-b +Left +.TP +.B C\-c +Escape +.TP +.B C\-d +Delete +.TP +.B C\-e +End +.TP +.B C\-f +Right +.TP +.B C\-g +Escape +.TP +.B C\-h +Backspace +.TP +.B C\-i +Tab +.TP +.B C\-j +Return +.TP +.B C\-J +Shift-Return +.TP +.B C\-k +Delete line right +.TP +.B C\-m +Return +.TP +.B C\-M +Shift-Return +.TP +.B C\-n +Down +.TP +.B C\-p +Up +.TP +.B C\-u +Delete line left +.TP +.B C\-w +Delete word left +.TP +.B C\-y +Paste from primary X selection +.TP +.B C\-Y +Paste from X clipboard +.TP +.B M\-b +Move cursor to the start of the current word +.TP +.B M\-f +Move cursor to the end of the current word +.TP +.B M\-g +Home +.TP +.B M\-G +End +.TP +.B M\-h +Up +.TP +.B M\-j +Page down +.TP +.B M\-k +Page up +.TP +.B M\-l +Down +.SH SEE ALSO +.IR dwm (1), +.IR stest (1) diff --git a/dmenu.c b/dmenu.c new file mode 100644 index 0000000..363d19f --- /dev/null +++ b/dmenu.c @@ -0,0 +1,799 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#ifdef XINERAMA +#include +#endif +#include + +#include "drw.h" +#include "util.h" + +/* macros */ +#define INTERSECT(x,y,w,h,r) (MAX(0, MIN((x)+(w),(r).x_org+(r).width) - MAX((x),(r).x_org)) \ + * MAX(0, MIN((y)+(h),(r).y_org+(r).height) - MAX((y),(r).y_org))) +#define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) + +/* enums */ +enum { SchemeNorm, SchemeSel, SchemeOut, SchemeLast }; /* color schemes */ + +struct item { + char *text; + struct item *left, *right; + int out; +}; + +static char text[BUFSIZ] = ""; +static char *embed; +static int bh, mw, mh; +static int inputw = 0, promptw; +static int lrpad; /* sum of left and right padding */ +static size_t cursor; +static struct item *items = NULL; +static struct item *matches, *matchend; +static struct item *prev, *curr, *next, *sel; +static int mon = -1, screen; + +static Atom clip, utf8; +static Display *dpy; +static Window root, parentwin, win; +static XIC xic; + +static Drw *drw; +static Clr *scheme[SchemeLast]; + +#include "config.h" + +static int (*fstrncmp)(const char *, const char *, size_t) = strncmp; +static char *(*fstrstr)(const char *, const char *) = strstr; + +static unsigned int +textw_clamp(const char *str, unsigned int n) +{ + unsigned int w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad; + return MIN(w, n); +} + +static void +appenditem(struct item *item, struct item **list, struct item **last) +{ + if (*last) + (*last)->right = item; + else + *list = item; + + item->left = *last; + item->right = NULL; + *last = item; +} + +static void +calcoffsets(void) +{ + int i, n; + + if (lines > 0) + n = lines * bh; + else + n = mw - (promptw + inputw + TEXTW("<") + TEXTW(">")); + /* calculate which items will begin the next page and previous page */ + for (i = 0, next = curr; next; next = next->right) + if ((i += (lines > 0) ? bh : textw_clamp(next->text, n)) > n) + break; + for (i = 0, prev = curr; prev && prev->left; prev = prev->left) + if ((i += (lines > 0) ? bh : textw_clamp(prev->left->text, n)) > n) + break; +} + +static void +cleanup(void) +{ + size_t i; + + XUngrabKeyboard(dpy, CurrentTime); + for (i = 0; i < SchemeLast; i++) + drw_scm_free(drw, scheme[i], 2); + for (i = 0; items && items[i].text; ++i) + free(items[i].text); + free(items); + drw_free(drw); + XSync(dpy, False); + XCloseDisplay(dpy); +} + +static char * +cistrstr(const char *h, const char *n) +{ + size_t i; + + if (!n[0]) + return (char *)h; + + for (; *h; ++h) { + for (i = 0; n[i] && tolower((unsigned char)n[i]) == + tolower((unsigned char)h[i]); ++i) + ; + if (n[i] == '\0') + return (char *)h; + } + return NULL; +} + +static int +drawitem(struct item *item, int x, int y, int w) +{ + if (item == sel) + drw_setscheme(drw, scheme[SchemeSel]); + else if (item->out) + drw_setscheme(drw, scheme[SchemeOut]); + else + drw_setscheme(drw, scheme[SchemeNorm]); + + return drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0); +} + +static void +drawmenu(void) +{ + unsigned int curpos; + struct item *item; + int x = 0, y = 0, w; + + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, 0, 0, mw, mh, 1, 1); + + if (prompt && *prompt) { + drw_setscheme(drw, scheme[SchemeSel]); + x = drw_text(drw, x, 0, promptw, bh, lrpad / 2, prompt, 0); + } + /* draw input field */ + w = (lines > 0 || !matches) ? mw - x : inputw; + drw_setscheme(drw, scheme[SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, text, 0); + + curpos = TEXTW(text) - TEXTW(&text[cursor]); + if ((curpos += lrpad / 2 - 1) < w) { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, x + curpos, 2, 2, bh - 4, 1, 0); + } + + if (lines > 0) { + /* draw vertical list */ + for (item = curr; item != next; item = item->right) + drawitem(item, x, y += bh, mw - x); + } else if (matches) { + /* draw horizontal list */ + x += inputw; + w = TEXTW("<"); + if (curr->left) { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, "<", 0); + } + x += w; + for (item = curr; item != next; item = item->right) + x = drawitem(item, x, 0, textw_clamp(item->text, mw - x - TEXTW(">"))); + if (next) { + w = TEXTW(">"); + drw_setscheme(drw, scheme[SchemeNorm]); + drw_text(drw, mw - w, 0, w, bh, lrpad / 2, ">", 0); + } + } + drw_map(drw, win, 0, 0, mw, mh); +} + +static void +grabfocus(void) +{ + struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 }; + Window focuswin; + int i, revertwin; + + for (i = 0; i < 100; ++i) { + XGetInputFocus(dpy, &focuswin, &revertwin); + if (focuswin == win) + return; + XSetInputFocus(dpy, win, RevertToParent, CurrentTime); + nanosleep(&ts, NULL); + } + die("cannot grab focus"); +} + +static void +grabkeyboard(void) +{ + struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 }; + int i; + + if (embed) + return; + /* try to grab keyboard, we may have to wait for another process to ungrab */ + for (i = 0; i < 1000; i++) { + if (XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, + GrabModeAsync, CurrentTime) == GrabSuccess) + return; + nanosleep(&ts, NULL); + } + die("cannot grab keyboard"); +} + +static void +match(void) +{ + static char **tokv = NULL; + static int tokn = 0; + + char buf[sizeof text], *s; + int i, tokc = 0; + size_t len, textsize; + struct item *item, *lprefix, *lsubstr, *prefixend, *substrend; + + strcpy(buf, text); + /* separate input text into tokens to be matched individually */ + for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " ")) + if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv))) + die("cannot realloc %zu bytes:", tokn * sizeof *tokv); + len = tokc ? strlen(tokv[0]) : 0; + + matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL; + textsize = strlen(text) + 1; + for (item = items; item && item->text; item++) { + for (i = 0; i < tokc; i++) + if (!fstrstr(item->text, tokv[i])) + break; + if (i != tokc) /* not all tokens match */ + continue; + /* exact matches go first, then prefixes, then substrings */ + if (!tokc || !fstrncmp(text, item->text, textsize)) + appenditem(item, &matches, &matchend); + else if (!fstrncmp(tokv[0], item->text, len)) + appenditem(item, &lprefix, &prefixend); + else + appenditem(item, &lsubstr, &substrend); + } + if (lprefix) { + if (matches) { + matchend->right = lprefix; + lprefix->left = matchend; + } else + matches = lprefix; + matchend = prefixend; + } + if (lsubstr) { + if (matches) { + matchend->right = lsubstr; + lsubstr->left = matchend; + } else + matches = lsubstr; + matchend = substrend; + } + curr = sel = matches; + calcoffsets(); +} + +static void +insert(const char *str, ssize_t n) +{ + if (strlen(text) + n > sizeof text - 1) + return; + /* move existing text out of the way, insert new text, and update cursor */ + memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0)); + if (n > 0) + memcpy(&text[cursor], str, n); + cursor += n; + match(); +} + +static size_t +nextrune(int inc) +{ + ssize_t n; + + /* return location of next utf8 rune in the given direction (+1 or -1) */ + for (n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc) + ; + return n; +} + +static void +movewordedge(int dir) +{ + if (dir < 0) { /* move cursor to the start of the word*/ + while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) + cursor = nextrune(-1); + while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) + cursor = nextrune(-1); + } else { /* move cursor to the end of the word */ + while (text[cursor] && strchr(worddelimiters, text[cursor])) + cursor = nextrune(+1); + while (text[cursor] && !strchr(worddelimiters, text[cursor])) + cursor = nextrune(+1); + } +} + +static void +keypress(XKeyEvent *ev) +{ + char buf[64]; + int len; + KeySym ksym = NoSymbol; + Status status; + + len = XmbLookupString(xic, ev, buf, sizeof buf, &ksym, &status); + switch (status) { + default: /* XLookupNone, XBufferOverflow */ + return; + case XLookupChars: /* composed string from input method */ + goto insert; + case XLookupKeySym: + case XLookupBoth: /* a KeySym and a string are returned: use keysym */ + break; + } + + if (ev->state & ControlMask) { + switch(ksym) { + case XK_a: ksym = XK_Home; break; + case XK_b: ksym = XK_Left; break; + case XK_c: ksym = XK_Escape; break; + case XK_d: ksym = XK_Delete; break; + case XK_e: ksym = XK_End; break; + case XK_f: ksym = XK_Right; break; + case XK_g: ksym = XK_Escape; break; + case XK_h: ksym = XK_BackSpace; break; + case XK_i: ksym = XK_Tab; break; + case XK_j: /* fallthrough */ + case XK_J: /* fallthrough */ + case XK_m: /* fallthrough */ + case XK_M: ksym = XK_Return; ev->state &= ~ControlMask; break; + case XK_n: ksym = XK_Down; break; + case XK_p: ksym = XK_Up; break; + + case XK_k: /* delete right */ + text[cursor] = '\0'; + match(); + break; + case XK_u: /* delete left */ + insert(NULL, 0 - cursor); + break; + case XK_w: /* delete word */ + while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) + insert(NULL, nextrune(-1) - cursor); + while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) + insert(NULL, nextrune(-1) - cursor); + break; + case XK_y: /* paste selection */ + case XK_Y: + XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY, + utf8, utf8, win, CurrentTime); + return; + case XK_Left: + case XK_KP_Left: + movewordedge(-1); + goto draw; + case XK_Right: + case XK_KP_Right: + movewordedge(+1); + goto draw; + case XK_Return: + case XK_KP_Enter: + break; + case XK_bracketleft: + cleanup(); + exit(1); + default: + return; + } + } else if (ev->state & Mod1Mask) { + switch(ksym) { + case XK_b: + movewordedge(-1); + goto draw; + case XK_f: + movewordedge(+1); + goto draw; + case XK_g: ksym = XK_Home; break; + case XK_G: ksym = XK_End; break; + case XK_h: ksym = XK_Up; break; + case XK_j: ksym = XK_Next; break; + case XK_k: ksym = XK_Prior; break; + case XK_l: ksym = XK_Down; break; + default: + return; + } + } + + switch(ksym) { + default: +insert: + if (!iscntrl((unsigned char)*buf)) + insert(buf, len); + break; + case XK_Delete: + case XK_KP_Delete: + if (text[cursor] == '\0') + return; + cursor = nextrune(+1); + /* fallthrough */ + case XK_BackSpace: + if (cursor == 0) + return; + insert(NULL, nextrune(-1) - cursor); + break; + case XK_End: + case XK_KP_End: + if (text[cursor] != '\0') { + cursor = strlen(text); + break; + } + if (next) { + /* jump to end of list and position items in reverse */ + curr = matchend; + calcoffsets(); + curr = prev; + calcoffsets(); + while (next && (curr = curr->right)) + calcoffsets(); + } + sel = matchend; + break; + case XK_Escape: + cleanup(); + exit(1); + case XK_Home: + case XK_KP_Home: + if (sel == matches) { + cursor = 0; + break; + } + sel = curr = matches; + calcoffsets(); + break; + case XK_Left: + case XK_KP_Left: + if (cursor > 0 && (!sel || !sel->left || lines > 0)) { + cursor = nextrune(-1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XK_Up: + case XK_KP_Up: + if (sel && sel->left && (sel = sel->left)->right == curr) { + curr = prev; + calcoffsets(); + } + break; + case XK_Next: + case XK_KP_Next: + if (!next) + return; + sel = curr = next; + calcoffsets(); + break; + case XK_Prior: + case XK_KP_Prior: + if (!prev) + return; + sel = curr = prev; + calcoffsets(); + break; + case XK_Return: + case XK_KP_Enter: + puts((sel && !(ev->state & ShiftMask)) ? sel->text : text); + if (!(ev->state & ControlMask)) { + cleanup(); + exit(0); + } + if (sel) + sel->out = 1; + break; + case XK_Right: + case XK_KP_Right: + if (text[cursor] != '\0') { + cursor = nextrune(+1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XK_Down: + case XK_KP_Down: + if (sel && sel->right && (sel = sel->right) == next) { + curr = next; + calcoffsets(); + } + break; + case XK_Tab: + if (!sel) + return; + cursor = strnlen(sel->text, sizeof text - 1); + memcpy(text, sel->text, cursor); + text[cursor] = '\0'; + match(); + break; + } + +draw: + drawmenu(); +} + +static void +paste(void) +{ + char *p, *q; + int di; + unsigned long dl; + Atom da; + + /* we have been given the current selection, now insert it into input */ + if (XGetWindowProperty(dpy, win, utf8, 0, (sizeof text / 4) + 1, False, + utf8, &da, &di, &dl, &dl, (unsigned char **)&p) + == Success && p) { + insert(p, (q = strchr(p, '\n')) ? q - p : (ssize_t)strlen(p)); + XFree(p); + } + drawmenu(); +} + +static void +readstdin(void) +{ + char *line = NULL; + size_t i, itemsiz = 0, linesiz = 0; + ssize_t len; + + /* read each line from stdin and add it to the item list */ + for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) { + if (i + 1 >= itemsiz) { + itemsiz += 256; + if (!(items = realloc(items, itemsiz * sizeof(*items)))) + die("cannot realloc %zu bytes:", itemsiz * sizeof(*items)); + } + if (line[len - 1] == '\n') + line[len - 1] = '\0'; + if (!(items[i].text = strdup(line))) + die("strdup:"); + + items[i].out = 0; + } + free(line); + if (items) + items[i].text = NULL; + lines = MIN(lines, i); +} + +static void +run(void) +{ + XEvent ev; + + while (!XNextEvent(dpy, &ev)) { + if (XFilterEvent(&ev, win)) + continue; + switch(ev.type) { + case DestroyNotify: + if (ev.xdestroywindow.window != win) + break; + cleanup(); + exit(1); + case Expose: + if (ev.xexpose.count == 0) + drw_map(drw, win, 0, 0, mw, mh); + break; + case FocusIn: + /* regrab focus from parent window */ + if (ev.xfocus.window != win) + grabfocus(); + break; + case KeyPress: + keypress(&ev.xkey); + break; + case SelectionNotify: + if (ev.xselection.property == utf8) + paste(); + break; + case VisibilityNotify: + if (ev.xvisibility.state != VisibilityUnobscured) + XRaiseWindow(dpy, win); + break; + } + } +} + +static void +setup(void) +{ + int x, y, i, j; + unsigned int du; + XSetWindowAttributes swa; + XIM xim; + Window w, dw, *dws; + XWindowAttributes wa; + XClassHint ch = {"dmenu", "dmenu"}; +#ifdef XINERAMA + XineramaScreenInfo *info; + Window pw; + int a, di, n, area = 0; +#endif + /* init appearance */ + for (j = 0; j < SchemeLast; j++) + scheme[j] = drw_scm_create(drw, colors[j], 2); + + clip = XInternAtom(dpy, "CLIPBOARD", False); + utf8 = XInternAtom(dpy, "UTF8_STRING", False); + + /* calculate menu geometry */ + bh = drw->fonts->h + 2; + lines = MAX(lines, 0); + mh = (lines + 1) * bh; +#ifdef XINERAMA + i = 0; + if (parentwin == root && (info = XineramaQueryScreens(dpy, &n))) { + XGetInputFocus(dpy, &w, &di); + if (mon >= 0 && mon < n) + i = mon; + else if (w != root && w != PointerRoot && w != None) { + /* find top-level window containing current input focus */ + do { + if (XQueryTree(dpy, (pw = w), &dw, &w, &dws, &du) && dws) + XFree(dws); + } while (w != root && w != pw); + /* find xinerama screen with which the window intersects most */ + if (XGetWindowAttributes(dpy, pw, &wa)) + for (j = 0; j < n; j++) + if ((a = INTERSECT(wa.x, wa.y, wa.width, wa.height, info[j])) > area) { + area = a; + i = j; + } + } + /* no focused window is on screen, so use pointer location instead */ + if (mon < 0 && !area && XQueryPointer(dpy, root, &dw, &dw, &x, &y, &di, &di, &du)) + for (i = 0; i < n; i++) + if (INTERSECT(x, y, 1, 1, info[i]) != 0) + break; + + x = info[i].x_org; + y = info[i].y_org + (topbar ? 0 : info[i].height - mh); + mw = info[i].width; + XFree(info); + } else +#endif + { + if (!XGetWindowAttributes(dpy, parentwin, &wa)) + die("could not get embedding window attributes: 0x%lx", + parentwin); + x = 0; + y = topbar ? 0 : wa.height - mh; + mw = wa.width; + } + promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0; + inputw = mw / 3; /* input width: ~33% of monitor width */ + match(); + + /* create menu window */ + swa.override_redirect = True; + swa.background_pixel = scheme[SchemeNorm][ColBg].pixel; + swa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask; + win = XCreateWindow(dpy, root, x, y, mw, mh, 0, + CopyFromParent, CopyFromParent, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + XSetClassHint(dpy, win, &ch); + + /* input methods */ + if ((xim = XOpenIM(dpy, NULL, NULL, NULL)) == NULL) + die("XOpenIM failed: could not open input device"); + + xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, win, XNFocusWindow, win, NULL); + + XMapRaised(dpy, win); + if (embed) { + XReparentWindow(dpy, win, parentwin, x, y); + XSelectInput(dpy, parentwin, FocusChangeMask | SubstructureNotifyMask); + if (XQueryTree(dpy, parentwin, &dw, &w, &dws, &du) && dws) { + for (i = 0; i < du && dws[i] != win; ++i) + XSelectInput(dpy, dws[i], FocusChangeMask); + XFree(dws); + } + grabfocus(); + } + drw_resize(drw, mw, mh); + drawmenu(); +} + +static void +usage(void) +{ + die("usage: dmenu [-bfiv] [-l lines] [-p prompt] [-fn font] [-m monitor]\n" + " [-nb color] [-nf color] [-sb color] [-sf color] [-w windowid]"); +} + +int +main(int argc, char *argv[]) +{ + XWindowAttributes wa; + int i, fast = 0; + + for (i = 1; i < argc; i++) + /* these options take no arguments */ + if (!strcmp(argv[i], "-v")) { /* prints version information */ + puts("dmenu-"VERSION); + exit(0); + } else if (!strcmp(argv[i], "-b")) /* appears at the bottom of the screen */ + topbar = 0; + else if (!strcmp(argv[i], "-f")) /* grabs keyboard before reading stdin */ + fast = 1; + else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */ + fstrncmp = strncasecmp; + fstrstr = cistrstr; + } else if (i + 1 == argc) + usage(); + /* these options take one argument */ + else if (!strcmp(argv[i], "-l")) /* number of lines in vertical list */ + lines = atoi(argv[++i]); + else if (!strcmp(argv[i], "-m")) + mon = atoi(argv[++i]); + else if (!strcmp(argv[i], "-p")) /* adds prompt to left of input field */ + prompt = argv[++i]; + else if (!strcmp(argv[i], "-fn")) /* font or font set */ + fonts[0] = argv[++i]; + else if (!strcmp(argv[i], "-nb")) /* normal background color */ + colors[SchemeNorm][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-nf")) /* normal foreground color */ + colors[SchemeNorm][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-sb")) /* selected background color */ + colors[SchemeSel][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-sf")) /* selected foreground color */ + colors[SchemeSel][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-ob")) /* outline background color */ + colors[SchemeOut][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-of")) /* outline foreground color */ + colors[SchemeOut][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-w")) /* embedding window id */ + embed = argv[++i]; + else + usage(); + + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("warning: no locale support\n", stderr); + if (!(dpy = XOpenDisplay(NULL))) + die("cannot open display"); + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + if (!embed || !(parentwin = strtol(embed, NULL, 0))) + parentwin = root; + if (!XGetWindowAttributes(dpy, parentwin, &wa)) + die("could not get embedding window attributes: 0x%lx", + parentwin); + drw = drw_create(dpy, screen, root, wa.width, wa.height); + if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) + die("no fonts could be loaded."); + lrpad = drw->fonts->h; + +#ifdef __OpenBSD__ + if (pledge("stdio rpath", NULL) == -1) + die("pledge"); +#endif + + if (fast && !isatty(0)) { + grabkeyboard(); + readstdin(); + } else { + readstdin(); + grabkeyboard(); + } + setup(); + run(); + + return 1; /* unreachable */ +} diff --git a/dmenu_path b/dmenu_path new file mode 100755 index 0000000..3a7cda7 --- /dev/null +++ b/dmenu_path @@ -0,0 +1,13 @@ +#!/bin/sh + +cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}" +cache="$cachedir/dmenu_run" + +[ ! -e "$cachedir" ] && mkdir -p "$cachedir" + +IFS=: +if stest -dqr -n "$cache" $PATH; then + stest -flx $PATH | sort -u | tee "$cache" +else + cat "$cache" +fi diff --git a/dmenu_run b/dmenu_run new file mode 100755 index 0000000..834ede5 --- /dev/null +++ b/dmenu_run @@ -0,0 +1,2 @@ +#!/bin/sh +dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} & diff --git a/drw.c b/drw.c new file mode 100644 index 0000000..9fdd1a4 --- /dev/null +++ b/drw.c @@ -0,0 +1,471 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "drw.h" +#include "util.h" + +#define UTF_INVALID 0xFFFD + +static int +utf8decode(const char *s_in, long *u, int *err) +{ + static const unsigned char lens[] = { + /* 0XXXX */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 10XXX */ 0, 0, 0, 0, 0, 0, 0, 0, /* invalid */ + /* 110XX */ 2, 2, 2, 2, + /* 1110X */ 3, 3, + /* 11110 */ 4, + /* 11111 */ 0, /* invalid */ + }; + static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 }; + static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 }; + + const unsigned char *s = (const unsigned char *)s_in; + int len = lens[*s >> 3]; + *u = UTF_INVALID; + *err = 1; + if (len == 0) + return 1; + + long cp = s[0] & leading_mask[len - 1]; + for (int i = 1; i < len; ++i) { + if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) + return i; + cp = (cp << 6) | (s[i] & 0x3F); + } + /* out of range, surrogate, overlong encoding */ + if (cp > 0x10FFFF || (cp >> 11) == 0x1B || cp < overlong[len - 1]) + return len; + + *err = 0; + *u = cp; + return len; +} + +Drw * +drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h) +{ + Drw *drw = ecalloc(1, sizeof(Drw)); + + drw->dpy = dpy; + drw->screen = screen; + drw->root = root; + drw->w = w; + drw->h = h; + drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen)); + drw->gc = XCreateGC(dpy, root, 0, NULL); + XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); + + return drw; +} + +void +drw_resize(Drw *drw, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + drw->w = w; + drw->h = h; + if (drw->drawable) + XFreePixmap(drw->dpy, drw->drawable); + drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen)); +} + +void +drw_free(Drw *drw) +{ + XFreePixmap(drw->dpy, drw->drawable); + XFreeGC(drw->dpy, drw->gc); + drw_fontset_free(drw->fonts); + free(drw); +} + +/* This function is an implementation detail. Library users should use + * drw_fontset_create instead. + */ +static Fnt * +xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern) +{ + Fnt *font; + XftFont *xfont = NULL; + FcPattern *pattern = NULL; + + if (fontname) { + /* Using the pattern found at font->xfont->pattern does not yield the + * same substitution results as using the pattern returned by + * FcNameParse; using the latter results in the desired fallback + * behaviour whereas the former just results in missing-character + * rectangles being drawn, at least with some fonts. */ + if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) { + fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname); + return NULL; + } + if (!(pattern = FcNameParse((FcChar8 *) fontname))) { + fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname); + XftFontClose(drw->dpy, xfont); + return NULL; + } + } else if (fontpattern) { + if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) { + fprintf(stderr, "error, cannot load font from pattern.\n"); + return NULL; + } + } else { + die("no font specified."); + } + + font = ecalloc(1, sizeof(Fnt)); + font->xfont = xfont; + font->pattern = pattern; + font->h = xfont->ascent + xfont->descent; + font->dpy = drw->dpy; + + return font; +} + +static void +xfont_free(Fnt *font) +{ + if (!font) + return; + if (font->pattern) + FcPatternDestroy(font->pattern); + XftFontClose(font->dpy, font->xfont); + free(font); +} + +Fnt* +drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount) +{ + Fnt *cur, *ret = NULL; + size_t i; + + if (!drw || !fonts) + return NULL; + + for (i = 1; i <= fontcount; i++) { + if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) { + cur->next = ret; + ret = cur; + } + } + return (drw->fonts = ret); +} + +void +drw_fontset_free(Fnt *font) +{ + if (font) { + drw_fontset_free(font->next); + xfont_free(font); + } +} + +void +drw_clr_create(Drw *drw, Clr *dest, const char *clrname) +{ + if (!drw || !dest || !clrname) + return; + + if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), + clrname, dest)) + die("error, cannot allocate color '%s'", clrname); +} + +/* Create color schemes. */ +Clr * +drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount) +{ + size_t i; + Clr *ret; + + /* need at least two colors for a scheme */ + if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(Clr)))) + return NULL; + + for (i = 0; i < clrcount; i++) + drw_clr_create(drw, &ret[i], clrnames[i]); + return ret; +} + +void +drw_clr_free(Drw *drw, Clr *c) +{ + if (!drw || !c) + return; + + /* c is typedef XftColor Clr */ + XftColorFree(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), c); +} + +void +drw_scm_free(Drw *drw, Clr *scm, size_t clrcount) +{ + size_t i; + + if (!drw || !scm) + return; + + for (i = 0; i < clrcount; i++) + drw_clr_free(drw, &scm[i]); + free(scm); +} + +void +drw_setfontset(Drw *drw, Fnt *set) +{ + if (drw) + drw->fonts = set; +} + +void +drw_setscheme(Drw *drw, Clr *scm) +{ + if (drw) + drw->scheme = scm; +} + +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + if (!drw || !drw->scheme) + return; + XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel); + if (filled) + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + else + XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1); +} + +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +{ + int ty, ellipsis_x = 0; + unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1; + XftDraw *d = NULL; + Fnt *usedfont, *curfont, *nextfont; + int utf8strlen, utf8charlen, utf8err, render = x || y || w || h; + long utf8codepoint = 0; + const char *utf8str; + FcCharSet *fccharset; + FcPattern *fcpattern; + FcPattern *match; + XftResult result; + int charexists = 0, overflow = 0; + /* keep track of a couple codepoints for which we have no match. */ + static unsigned int nomatches[128], ellipsis_width, invalid_width; + static const char invalid[] = "�"; + + if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) + return 0; + + if (!render) { + w = invert ? invert : ~invert; + } else { + XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + if (w < lpad) + return x + w; + d = XftDrawCreate(drw->dpy, drw->drawable, + DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen)); + x += lpad; + w -= lpad; + } + + usedfont = drw->fonts; + if (!ellipsis_width && render) + ellipsis_width = drw_fontset_getwidth(drw, "..."); + if (!invalid_width && render) + invalid_width = drw_fontset_getwidth(drw, invalid); + while (1) { + ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; + utf8str = text; + nextfont = NULL; + while (*text) { + utf8charlen = utf8decode(text, &utf8codepoint, &utf8err); + for (curfont = drw->fonts; curfont; curfont = curfont->next) { + charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); + if (charexists) { + drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL); + if (ew + ellipsis_width <= w) { + /* keep track where the ellipsis still fits */ + ellipsis_x = x + ew; + ellipsis_w = w - ew; + ellipsis_len = utf8strlen; + } + + if (ew + tmpw > w) { + overflow = 1; + /* called from drw_fontset_getwidth_clamp(): + * it wants the width AFTER the overflow + */ + if (!render) + x += tmpw; + else + utf8strlen = ellipsis_len; + } else if (curfont == usedfont) { + text += utf8charlen; + utf8strlen += utf8err ? 0 : utf8charlen; + ew += utf8err ? 0 : tmpw; + } else { + nextfont = curfont; + } + break; + } + } + + if (overflow || !charexists || nextfont || utf8err) + break; + else + charexists = 0; + } + + if (utf8strlen) { + if (render) { + ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent; + XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg], + usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen); + } + x += ew; + w -= ew; + } + if (utf8err && (!render || invalid_width < w)) { + if (render) + drw_text(drw, x, y, w, h, 0, invalid, invert); + x += invalid_width; + w -= invalid_width; + } + if (render && overflow) + drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert); + + if (!*text || overflow) { + break; + } else if (nextfont) { + charexists = 0; + usedfont = nextfont; + } else { + /* Regardless of whether or not a fallback font is found, the + * character must be drawn. */ + charexists = 1; + + hash = (unsigned int)utf8codepoint; + hash = ((hash >> 16) ^ hash) * 0x21F0AAAD; + hash = ((hash >> 15) ^ hash) * 0xD35A2D97; + h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches); + h1 = (hash >> 17) % LENGTH(nomatches); + /* avoid expensive XftFontMatch call when we know we won't find a match */ + if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint) + goto no_match; + + fccharset = FcCharSetCreate(); + FcCharSetAddChar(fccharset, utf8codepoint); + + if (!drw->fonts->pattern) { + /* Refer to the comment in xfont_create for more information. */ + die("the first font in the cache must be loaded from a font string."); + } + + fcpattern = FcPatternDuplicate(drw->fonts->pattern); + FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue); + + FcConfigSubstitute(NULL, fcpattern, FcMatchPattern); + FcDefaultSubstitute(fcpattern); + match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result); + + FcCharSetDestroy(fccharset); + FcPatternDestroy(fcpattern); + + if (match) { + usedfont = xfont_create(drw, NULL, match); + if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) { + for (curfont = drw->fonts; curfont->next; curfont = curfont->next) + ; /* NOP */ + curfont->next = usedfont; + } else { + xfont_free(usedfont); + nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint; +no_match: + usedfont = drw->fonts; + } + } + } + } + if (d) + XftDrawDestroy(d); + + return x + (render ? w : 0); +} + +void +drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y); + XSync(drw->dpy, False); +} + +unsigned int +drw_fontset_getwidth(Drw *drw, const char *text) +{ + if (!drw || !drw->fonts || !text) + return 0; + return drw_text(drw, 0, 0, 0, 0, 0, text, 0); +} + +unsigned int +drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n) +{ + unsigned int tmp = 0; + if (drw && drw->fonts && text && n) + tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n); + return MIN(n, tmp); +} + +void +drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h) +{ + XGlyphInfo ext; + + if (!font || !text) + return; + + XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext); + if (w) + *w = ext.xOff; + if (h) + *h = font->h; +} + +Cur * +drw_cur_create(Drw *drw, int shape) +{ + Cur *cur; + + if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) + return NULL; + + cur->cursor = XCreateFontCursor(drw->dpy, shape); + + return cur; +} + +void +drw_cur_free(Drw *drw, Cur *cursor) +{ + if (!cursor) + return; + + XFreeCursor(drw->dpy, cursor->cursor); + free(cursor); +} diff --git a/drw.h b/drw.h new file mode 100644 index 0000000..2ed77be --- /dev/null +++ b/drw.h @@ -0,0 +1,60 @@ +/* See LICENSE file for copyright and license details. */ + +typedef struct { + Cursor cursor; +} Cur; + +typedef struct Fnt { + Display *dpy; + unsigned int h; + XftFont *xfont; + FcPattern *pattern; + struct Fnt *next; +} Fnt; + +enum { ColFg, ColBg }; /* Clr scheme index */ +typedef XftColor Clr; + +typedef struct { + unsigned int w, h; + Display *dpy; + int screen; + Window root; + Drawable drawable; + GC gc; + Clr *scheme; + Fnt *fonts; +} Drw; + +/* Drawable abstraction */ +Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h); +void drw_resize(Drw *drw, unsigned int w, unsigned int h); +void drw_free(Drw *drw); + +/* Fnt abstraction */ +Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount); +void drw_fontset_free(Fnt* set); +unsigned int drw_fontset_getwidth(Drw *drw, const char *text); +unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n); +void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h); + +/* Colorscheme abstraction */ +void drw_clr_create(Drw *drw, Clr *dest, const char *clrname); +void drw_clr_free(Drw *drw, Clr *c); +Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount); +void drw_scm_free(Drw *drw, Clr *scm, size_t clrcount); + +/* Cursor abstraction */ +Cur *drw_cur_create(Drw *drw, int shape); +void drw_cur_free(Drw *drw, Cur *cursor); + +/* Drawing context manipulation */ +void drw_setfontset(Drw *drw, Fnt *set); +void drw_setscheme(Drw *drw, Clr *scm); + +/* Drawing functions */ +void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert); +int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert); + +/* Map functions */ +void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); diff --git a/stest.1 b/stest.1 new file mode 100644 index 0000000..2667d8a --- /dev/null +++ b/stest.1 @@ -0,0 +1,90 @@ +.TH STEST 1 dmenu\-VERSION +.SH NAME +stest \- filter a list of files by properties +.SH SYNOPSIS +.B stest +.RB [ -abcdefghlpqrsuwx ] +.RB [ -n +.IR file ] +.RB [ -o +.IR file ] +.RI [ file ...] +.SH DESCRIPTION +.B stest +takes a list of files and filters by the files' properties, analogous to +.IR test (1). +Files which pass all tests are printed to stdout. If no files are given, stest +reads files from stdin. +.SH OPTIONS +.TP +.B \-a +Test hidden files. +.TP +.B \-b +Test that files are block specials. +.TP +.B \-c +Test that files are character specials. +.TP +.B \-d +Test that files are directories. +.TP +.B \-e +Test that files exist. +.TP +.B \-f +Test that files are regular files. +.TP +.B \-g +Test that files have their set-group-ID flag set. +.TP +.B \-h +Test that files are symbolic links. +.TP +.B \-l +Test the contents of a directory given as an argument. +.TP +.BI \-n " file" +Test that files are newer than +.IR file . +.TP +.BI \-o " file" +Test that files are older than +.IR file . +.TP +.B \-p +Test that files are named pipes. +.TP +.B \-q +No files are printed, only the exit status is returned. +.TP +.B \-r +Test that files are readable. +.TP +.B \-s +Test that files are not empty. +.TP +.B \-u +Test that files have their set-user-ID flag set. +.TP +.B \-v +Invert the sense of tests, only failing files pass. +.TP +.B \-w +Test that files are writable. +.TP +.B \-x +Test that files are executable. +.SH EXIT STATUS +.TP +.B 0 +At least one file passed all tests. +.TP +.B 1 +No files passed all tests. +.TP +.B 2 +An error occurred. +.SH SEE ALSO +.IR dmenu (1), +.IR test (1) diff --git a/stest.c b/stest.c new file mode 100644 index 0000000..e27d3a5 --- /dev/null +++ b/stest.c @@ -0,0 +1,109 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include +#include +#include +#include +#include +#include + +#include "arg.h" +char *argv0; + +#define FLAG(x) (flag[(x)-'a']) + +static void test(const char *, const char *); +static void usage(void); + +static int match = 0; +static int flag[26]; +static struct stat old, new; + +static void +test(const char *path, const char *name) +{ + struct stat st, ln; + + if ((!stat(path, &st) && (FLAG('a') || name[0] != '.') /* hidden files */ + && (!FLAG('b') || S_ISBLK(st.st_mode)) /* block special */ + && (!FLAG('c') || S_ISCHR(st.st_mode)) /* character special */ + && (!FLAG('d') || S_ISDIR(st.st_mode)) /* directory */ + && (!FLAG('e') || access(path, F_OK) == 0) /* exists */ + && (!FLAG('f') || S_ISREG(st.st_mode)) /* regular file */ + && (!FLAG('g') || st.st_mode & S_ISGID) /* set-group-id flag */ + && (!FLAG('h') || (!lstat(path, &ln) && S_ISLNK(ln.st_mode))) /* symbolic link */ + && (!FLAG('n') || st.st_mtime > new.st_mtime) /* newer than file */ + && (!FLAG('o') || st.st_mtime < old.st_mtime) /* older than file */ + && (!FLAG('p') || S_ISFIFO(st.st_mode)) /* named pipe */ + && (!FLAG('r') || access(path, R_OK) == 0) /* readable */ + && (!FLAG('s') || st.st_size > 0) /* not empty */ + && (!FLAG('u') || st.st_mode & S_ISUID) /* set-user-id flag */ + && (!FLAG('w') || access(path, W_OK) == 0) /* writable */ + && (!FLAG('x') || access(path, X_OK) == 0)) != FLAG('v')) { /* executable */ + if (FLAG('q')) + exit(0); + match = 1; + puts(name); + } +} + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-abcdefghlpqrsuvwx] " + "[-n file] [-o file] [file...]\n", argv0); + exit(2); /* like test(1) return > 1 on error */ +} + +int +main(int argc, char *argv[]) +{ + struct dirent *d; + char path[PATH_MAX], *line = NULL, *file; + size_t linesiz = 0; + ssize_t n; + DIR *dir; + int r; + + ARGBEGIN { + case 'n': /* newer than file */ + case 'o': /* older than file */ + file = EARGF(usage()); + if (!(FLAG(ARGC()) = !stat(file, (ARGC() == 'n' ? &new : &old)))) + perror(file); + break; + default: + /* miscellaneous operators */ + if (strchr("abcdefghlpqrsuvwx", ARGC())) + FLAG(ARGC()) = 1; + else + usage(); /* unknown flag */ + } ARGEND; + + if (!argc) { + /* read list from stdin */ + while ((n = getline(&line, &linesiz, stdin)) > 0) { + if (line[n - 1] == '\n') + line[n - 1] = '\0'; + test(line, line); + } + free(line); + } else { + for (; argc; argc--, argv++) { + if (FLAG('l') && (dir = opendir(*argv))) { + /* test directory contents */ + while ((d = readdir(dir))) { + r = snprintf(path, sizeof path, "%s/%s", + *argv, d->d_name); + if (r >= 0 && (size_t)r < sizeof path) + test(path, d->d_name); + } + closedir(dir); + } else { + test(*argv, *argv); + } + } + } + return match ? 0 : 1; +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..8e26a51 --- /dev/null +++ b/util.c @@ -0,0 +1,37 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "util.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + int saved_errno; + + saved_errno = errno; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') + fprintf(stderr, " %s", strerror(saved_errno)); + fputc('\n', stderr); + + exit(1); +} + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..c0a50d4 --- /dev/null +++ b/util.h @@ -0,0 +1,9 @@ +/* See LICENSE file for copyright and license details. */ + +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) +#define LENGTH(X) (sizeof (X) / sizeof (X)[0]) + +void die(const char *fmt, ...); +void *ecalloc(size_t nmemb, size_t size); From 7dc635497efafbd87673671be79735f124ad5680 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 07/11] Squashed 'suckless/st/' content from commit 688f70a git-subtree-dir: suckless/st git-subtree-split: 688f70add0d1da8a416bf7df763328d694a24a3a --- FAQ | 253 +++++ LEGACY | 17 + LICENSE | 34 + Makefile | 51 + README | 34 + TODO | 28 + arg.h | 50 + config.def.h | 474 +++++++++ config.mk | 36 + st.1 | 177 ++++ st.c | 2705 ++++++++++++++++++++++++++++++++++++++++++++++++++ st.h | 126 +++ st.info | 243 +++++ win.h | 41 + x.c | 2108 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 6377 insertions(+) create mode 100644 FAQ create mode 100644 LEGACY create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 arg.h create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 st.1 create mode 100644 st.c create mode 100644 st.h create mode 100644 st.info create mode 100644 win.h create mode 100644 x.c diff --git a/FAQ b/FAQ new file mode 100644 index 0000000..6287a27 --- /dev/null +++ b/FAQ @@ -0,0 +1,253 @@ +## Why does st not handle utmp entries? + +Use the excellent tool of [utmp](https://git.suckless.org/utmp/) for this task. + + +## Some _random program_ complains that st is unknown/not recognised/unsupported/whatever! + +It means that st doesn’t have any terminfo entry on your system. Chances are +you did not `make install`. If you just want to test it without installing it, +you can manually run `tic -sx st.info`. + + +## Nothing works, and nothing is said about an unknown terminal! + +* Some programs just assume they’re running in xterm i.e. they don’t rely on + terminfo. What you see is the current state of the “xterm compliance”. +* Some programs don’t complain about the lacking st description and default to + another terminal. In that case see the question about terminfo. + + +## How do I scroll back up? + +* Using a terminal multiplexer. + * `st -e tmux` using C-b [ + * `st -e screen` using C-a ESC +* Using the excellent tool of [scroll](https://git.suckless.org/scroll/). +* Using the scrollback [patch](https://st.suckless.org/patches/scrollback/). + + +## I would like to have utmp and/or scroll functionality by default + +You can add the absolute path of both programs in your config.h file. You only +have to modify the value of utmp and scroll variables. + + +## Why doesn't the Del key work in some programs? + +Taken from the terminfo manpage: + + If the terminal has a keypad that transmits codes when the keys + are pressed, this information can be given. Note that it is not + possible to handle terminals where the keypad only works in + local (this applies, for example, to the unshifted HP 2621 keys). + If the keypad can be set to transmit or not transmit, give these + codes as smkx and rmkx. Otherwise the keypad is assumed to + always transmit. + +In the st case smkx=E[?1hE= and rmkx=E[?1lE>, so it is mandatory that +applications which want to test against keypad keys send these +sequences. + +But buggy applications (like bash and irssi, for example) don't do this. A fast +solution for them is to use the following command: + + $ printf '\033[?1h\033=' >/dev/tty + +or + $ tput smkx + +In the case of bash, readline is used. Readline has a different note in its +manpage about this issue: + + enable-keypad (Off) + When set to On, readline will try to enable the + application keypad when it is called. Some systems + need this to enable arrow keys. + +Adding this option to your .inputrc will fix the keypad problem for all +applications using readline. + +If you are using zsh, then read the zsh FAQ +: + + It should be noted that the O / [ confusion can occur with other keys + such as Home and End. Some systems let you query the key sequences + sent by these keys from the system's terminal database, terminfo. + Unfortunately, the key sequences given there typically apply to the + mode that is not the one zsh uses by default (it's the "application" + mode rather than the "raw" mode). Explaining the use of terminfo is + outside of the scope of this FAQ, but if you wish to use the key + sequences given there you can tell the line editor to turn on + "application" mode when it starts and turn it off when it stops: + + function zle-line-init () { echoti smkx } + function zle-line-finish () { echoti rmkx } + zle -N zle-line-init + zle -N zle-line-finish + +Putting these lines into your .zshrc will fix the problems. + + +## How can I use meta in 8bit mode? + +St supports meta in 8bit mode, but the default terminfo entry doesn't +use this capability. If you want it, you have to use the 'st-meta' value +in TERM. + + +## I cannot compile st in OpenBSD + +OpenBSD lacks librt, despite it being mandatory in POSIX +. +If you want to compile st for OpenBSD you have to remove -lrt from config.mk, and +st will compile without any loss of functionality, because all the functions are +included in libc on this platform. + + +## The Backspace Case + +St is emulating the Linux way of handling backspace being delete and delete being +backspace. + +This is an issue that was discussed in suckless mailing list +. Here is why some old grumpy +terminal users wants its backspace to be how he feels it: + + Well, I am going to comment why I want to change the behaviour + of this key. When ASCII was defined in 1968, communication + with computers was done using punched cards, or hardcopy + terminals (basically a typewriter machine connected with the + computer using a serial port). ASCII defines DELETE as 7F, + because, in punched-card terms, it means all the holes of the + card punched; it is thus a kind of 'physical delete'. In the + same way, the BACKSPACE key was a non-destructive backspace, + as on a typewriter. So, if you wanted to delete a character, + you had to BACKSPACE and then DELETE. Another use of BACKSPACE + was to type accented characters, for example 'a BACKSPACE `'. + The VT100 had no BACKSPACE key; it was generated using the + CONTROL key as another control character (CONTROL key sets to + 0 b7 b6 b5, so it converts H (code 0x48) into BACKSPACE (code + 0x08)), but it had a DELETE key in a similar position where + the BACKSPACE key is located today on common PC keyboards. + All the terminal emulators emulated the difference between + these keys correctly: the backspace key generated a BACKSPACE + (^H) and delete key generated a DELETE (^?). + + But a problem arose when Linus Torvalds wrote Linux. Unlike + earlier terminals, the Linux virtual terminal (the terminal + emulator integrated in the kernel) returned a DELETE when + backspace was pressed, due to the VT100 having a DELETE key in + the same position. This created a lot of problems (see [1] + and [2]). Since Linux has become the king, a lot of terminal + emulators today generate a DELETE when the backspace key is + pressed in order to avoid problems with Linux. The result is + that the only way of generating a BACKSPACE on these systems + is by using CONTROL + H. (I also think that emacs had an + important point here because the CONTROL + H prefix is used + in emacs in some commands (help commands).) + + From point of view of the kernel, you can change the key + for deleting a previous character with stty erase. When you + connect a real terminal into a machine you describe the type + of terminal, so getty configures the correct value of stty + erase for this terminal. In the case of terminal emulators, + however, you don't have any getty that can set the correct + value of stty erase, so you always get the default value. + For this reason, it is necessary to add 'stty erase ^H' to your + profile if you have changed the value of the backspace key. + Of course, another solution is for st itself to modify the + value of stty erase. I usually have the inverse problem: + when I connect to non-Unix machines, I have to press CONTROL + + h to get a BACKSPACE. The inverse problem occurs when a user + connects to my Unix machines from a different system with a + correct backspace key. + + [1] http://www.ibb.net/~anne/keyboard.html + [2] http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-5.html + + +## But I really want the old grumpy behaviour of my terminal + +Apply [1]. + +[1] https://st.suckless.org/patches/delkey + + +## Why do images not work in st using the w3m image hack? + +w3mimg uses a hack that draws an image on top of the terminal emulator Drawable +window. The hack relies on the terminal to use a single buffer to draw its +contents directly. + +st uses double-buffered drawing so the image is quickly replaced and may show a +short flicker effect. + +Below is a patch example to change st double-buffering to a single Drawable +buffer. + +diff --git a/x.c b/x.c +--- a/x.c ++++ b/x.c +@@ -732,10 +732,6 @@ xresize(int col, int row) + win.tw = col * win.cw; + win.th = row * win.ch; + +- XFreePixmap(xw.dpy, xw.buf); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); +- XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ +@@ -1148,8 +1144,7 @@ xinit(int cols, int rows) + gcvalues.graphics_exposures = False; + dc.gc = XCreateGC(xw.dpy, parent, GCGraphicsExposures, + &gcvalues); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.buf = xw.win; + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + +@@ -1632,8 +1627,6 @@ xdrawline(Line line, int x1, int y1, int x2) + void + xfinishdraw(void) + { +- XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, +- win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); + + +## BadLength X error in Xft when trying to render emoji + +Xft makes st crash when rendering color emojis with the following error: + +"X Error of failed request: BadLength (poly request too large or internal Xlib length error)" + Major opcode of failed request: 139 (RENDER) + Minor opcode of failed request: 20 (RenderAddGlyphs) + Serial number of failed request: 1595 + Current serial number in output stream: 1818" + +This is a known bug in Xft (not st) which happens on some platforms and +combination of particular fonts and fontconfig settings. + +See also: +https://gitlab.freedesktop.org/xorg/lib/libxft/issues/6 +https://bugs.freedesktop.org/show_bug.cgi?id=107534 +https://bugzilla.redhat.com/show_bug.cgi?id=1498269 + +The solution is to remove color emoji fonts or disable this in the fontconfig +XML configuration. As an ugly workaround (which may work only on newer +fontconfig versions (FC_COLOR)), the following code can be used to mask color +fonts: + + FcPatternAddBool(fcpattern, FC_COLOR, FcFalse); + +Please don't bother reporting this bug to st, but notify the upstream Xft +developers about fixing this bug. + +As of 2022-09-05 this now seems to be finally fixed in libXft 2.3.5: +https://gitlab.freedesktop.org/xorg/lib/libxft/-/blob/libXft-2.3.5/NEWS diff --git a/LEGACY b/LEGACY new file mode 100644 index 0000000..bf28b1e --- /dev/null +++ b/LEGACY @@ -0,0 +1,17 @@ +A STATEMENT ON LEGACY SUPPORT + +In the terminal world there is much cruft that comes from old and unsup‐ +ported terminals that inherit incompatible modes and escape sequences +which noone is able to know, except when he/she comes from that time and +developed a graphical vt100 emulator at that time. + +One goal of st is to only support what is really needed. When you en‐ +counter a sequence which you really need, implement it. But while you +are at it, do not add the other cruft you might encounter while sneek‐ +ing at other terminal emulators. History has bloated them and there is +no real evidence that most of the sequences are used today. + + +Christoph Lohmann <20h@r-36.net> +2012-09-13T07:00:36.081271045+02:00 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cbf420 --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +MIT/X Consortium License + +© 2014-2022 Hiltjo Posthuma +© 2018 Devin J. Pohly +© 2014-2017 Quentin Rameau +© 2009-2012 Aurélien APTEL +© 2008-2017 Anselm R Garbe +© 2012-2017 Roberto E. Vargas Caballero +© 2012-2016 Christoph Lohmann <20h at r-36 dot net> +© 2013 Eon S. Jeon +© 2013 Alexander Sedov +© 2013 Mark Edgar +© 2013-2014 Eric Pruitt +© 2013 Michael Forney +© 2013-2014 Markus Teich +© 2014-2015 Laslo Hunhold + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15db421 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# st - simple terminal +# See LICENSE file for copyright and license details. +.POSIX: + +include config.mk + +SRC = st.c x.c +OBJ = $(SRC:.c=.o) + +all: st + +config.h: + cp config.def.h 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 + +$(OBJ): config.h config.mk + +st: $(OBJ) + $(CC) -o $@ $(OBJ) $(STLDFLAGS) + +clean: + rm -f st $(OBJ) st-$(VERSION).tar.gz + +dist: clean + mkdir -p st-$(VERSION) + cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ + config.def.h st.info st.1 arg.h st.h win.h $(SRC)\ + st-$(VERSION) + tar -cf - st-$(VERSION) | gzip > st-$(VERSION).tar.gz + rm -rf st-$(VERSION) + +install: st + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f st $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/st + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1 + tic -sx st.info + @echo Please see the README file regarding the terminfo entry of st. + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/st + rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1 + +.PHONY: all clean dist install uninstall diff --git a/README b/README new file mode 100644 index 0000000..6a846ed --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +st - simple terminal +-------------------- +st is a simple terminal emulator for X which sucks less. + + +Requirements +------------ +In order to build st you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (st is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install st (if +necessary as root): + + make clean install + + +Running st +---------- +If you did not install st with make clean install, you must compile +the st terminfo entry with the following command: + + tic -sx st.info + +See the man page for additional details. + +Credits +------- +Based on Aurélien APTEL bt source code. + diff --git a/TODO b/TODO new file mode 100644 index 0000000..5f74cd5 --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +vt emulation +------------ + +* double-height support + +code & interface +---------------- + +* add a simple way to do multiplexing + +drawing +------- +* add diacritics support to xdraws() + * switch to a suckless font drawing library +* make the font cache simpler +* add better support for brightening of the upper colors + +bugs +---- + +* fix shift up/down (shift selection in emacs) +* remove DEC test sequence when appropriate + +misc +---- + + $ grep -nE 'XXX|TODO' st.c + diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..a22e019 --- /dev/null +++ b/arg.h @@ -0,0 +1,50 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + int i_;\ + for (i_ = 1, brk_ = 0, argv_ = argv;\ + argv[0][i_] && !brk_;\ + i_++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][i_];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..2cd740a --- /dev/null +++ b/config.def.h @@ -0,0 +1,474 @@ +/* See LICENSE file for copyright and license details. */ + +/* + * appearance + * + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ +static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; +static int borderpx = 2; + +/* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e + * 2: scroll and/or utmp + * 3: SHELL environment variable + * 4: value of shell in /etc/passwd + * 5: value of shell in config.h + */ +static char *shell = "/bin/sh"; +char *utmp = NULL; +/* scroll program: to enable use a string like "scroll" */ +char *scroll = NULL; +char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + +/* identification sequence returned in DA and DECID */ +char *vtiden = "\033[?6c"; + +/* Kerning / character bounding-box multipliers */ +static float cwscale = 1.0; +static float chscale = 1.0; + +/* + * word delimiter string + * + * More advanced example: L" `'\"()[]{}" + */ +wchar_t *worddelimiters = L" "; + +/* selection timeouts (in milliseconds) */ +static unsigned int doubleclicktimeout = 300; +static unsigned int tripleclicktimeout = 600; + +/* alt screens */ +int allowaltscreen = 1; + +/* allow certain non-interactive (insecure) window operations such as: + setting the clipboard text */ +int allowwindowops = 0; + +/* + * draw latency range in ms - from new content/keypress/etc until drawing. + * within this range, st draws when content stops arriving (idle). mostly it's + * near minlatency, but it waits longer for slow updates to avoid partial draw. + * low minlatency will tear/flicker more, as it can "detect" idle too early. + */ +static double minlatency = 2; +static double maxlatency = 33; + +/* + * blinking timeout (set to 0 to disable blinking) for the terminal blinking + * attribute. + */ +static unsigned int blinktimeout = 800; + +/* + * thickness of underline and bar cursors + */ +static unsigned int cursorthickness = 2; + +/* + * bell volume. It must be a value between -100 and 100. Use 0 for disabling + * it + */ +static int bellvolume = 0; + +/* default TERM value */ +char *termname = "st-256color"; + +/* + * spaces per tab + * + * When you are changing this value, don't forget to adapt the »it« value in + * the st.info and appropriately install the st.info in the environment where + * you use this st version. + * + * it#$tabspaces, + * + * Secondly make sure your kernel is not expanding tabs. When running `stty + * -a` »tab0« should appear. You can tell the terminal to not expand tabs by + * running following command: + * + * stty tabs + */ +unsigned int tabspaces = 8; + +/* Terminal colors (16 first used in escape sequence) */ +static const char *colorname[] = { + /* 8 normal colors */ + "black", + "red3", + "green3", + "yellow3", + "blue2", + "magenta3", + "cyan3", + "gray90", + + /* 8 bright colors */ + "gray50", + "red", + "green", + "yellow", + "#5c5cff", + "magenta", + "cyan", + "white", + + [255] = 0, + + /* more colors can be added after 255 to use with DefaultXX */ + "#cccccc", + "#555555", + "gray90", /* default foreground colour */ + "black", /* default background colour */ +}; + + +/* + * Default colors (colorname index) + * foreground, background, cursor, reverse cursor + */ +unsigned int defaultfg = 258; +unsigned int defaultbg = 259; +unsigned int defaultcs = 256; +static unsigned int defaultrcs = 257; + +/* + * Default shape of cursor + * 2: Block ("█") + * 4: Underline ("_") + * 6: Bar ("|") + * 7: Snowman ("☃") + */ +static unsigned int cursorshape = 2; + +/* + * Default columns and rows numbers + */ + +static unsigned int cols = 80; +static unsigned int rows = 24; + +/* + * Default colour and shape of the mouse cursor + */ +static unsigned int mouseshape = XC_xterm; +static unsigned int mousefg = 7; +static unsigned int mousebg = 0; + +/* + * Color used to display font attributes when fontconfig selected a font which + * doesn't match the ones requested. + */ +static unsigned int defaultattr = 11; + +/* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other + * modifier, set to 0 to not use it. + */ +static uint forcemousemod = ShiftMask; + +/* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ +static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, + { ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} }, + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, +}; + +/* Internal keyboard shortcuts. */ +#define MODKEY Mod1Mask +#define TERMMOD (ControlMask|ShiftMask) + +static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, + { ControlMask, XK_Print, toggleprinter, {.i = 0} }, + { ShiftMask, XK_Print, printscreen, {.i = 0} }, + { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, + { TERMMOD, XK_Prior, zoom, {.f = +1} }, + { TERMMOD, XK_Next, zoom, {.f = -1} }, + { TERMMOD, XK_Home, zoomreset, {.f = 0} }, + { TERMMOD, XK_C, clipcopy, {.i = 0} }, + { TERMMOD, XK_V, clippaste, {.i = 0} }, + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, +}; + +/* + * Special keys (change & recompile st.info accordingly) + * + * Mask value: + * * Use XK_ANY_MOD to match the key no matter modifiers state + * * Use XK_NO_MOD to match the key alone (no modifiers) + * appkey value: + * * 0: no value + * * > 0: keypad application mode enabled + * * = 2: term.numlock = 1 + * * < 0: keypad application mode disabled + * appcursor value: + * * 0: no value + * * > 0: cursor application mode enabled + * * < 0: cursor application mode disabled + * + * Be careful with the order of the definitions because st searches in + * this table sequentially, so any XK_ANY_MOD must be in the last + * position for a key. + */ + +/* + * If you want keys other than the X11 function keys (0xFD00 - 0xFFFF) + * to be mapped below, add them to this array. + */ +static KeySym mappedkeys[] = { -1 }; + +/* + * State bits to ignore when matching key or button events. By default, + * numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored. + */ +static uint ignoremod = Mod2Mask|XK_SWITCH_MOD; + +/* + * This is the huge key array which defines all compatibility to the Linux + * world. Please decide about changes wisely. + */ +static Key key[] = { + /* keysym mask string appkey appcursor */ + { XK_KP_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0}, + { XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0}, + { XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0}, + { XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0}, + { XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0}, + { XK_KP_End, ControlMask, "\033[J", -1, 0}, + { XK_KP_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_KP_End, ShiftMask, "\033[K", -1, 0}, + { XK_KP_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_KP_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[L", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_KP_Delete, ControlMask, "\033[M", -1, 0}, + { XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_KP_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0}, + { XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0}, + { XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0}, + { XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0}, + { XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0}, + { XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0}, + { XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0}, + { XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0}, + { XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0}, + { XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0}, + { XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0}, + { XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0}, + { XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0}, + { XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0}, + { XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0}, + { XK_Up, ShiftMask, "\033[1;2A", 0, 0}, + { XK_Up, Mod1Mask, "\033[1;3A", 0, 0}, + { XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0}, + { XK_Up, ControlMask, "\033[1;5A", 0, 0}, + { XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0}, + { XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0}, + { XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0}, + { XK_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_Down, ShiftMask, "\033[1;2B", 0, 0}, + { XK_Down, Mod1Mask, "\033[1;3B", 0, 0}, + { XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0}, + { XK_Down, ControlMask, "\033[1;5B", 0, 0}, + { XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0}, + { XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0}, + { XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0}, + { XK_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_Left, ShiftMask, "\033[1;2D", 0, 0}, + { XK_Left, Mod1Mask, "\033[1;3D", 0, 0}, + { XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0}, + { XK_Left, ControlMask, "\033[1;5D", 0, 0}, + { XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0}, + { XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0}, + { XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0}, + { XK_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_Right, ShiftMask, "\033[1;2C", 0, 0}, + { XK_Right, Mod1Mask, "\033[1;3C", 0, 0}, + { XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0}, + { XK_Right, ControlMask, "\033[1;5C", 0, 0}, + { XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0}, + { XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0}, + { XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0}, + { XK_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0}, + { XK_Return, Mod1Mask, "\033\r", 0, 0}, + { XK_Return, XK_ANY_MOD, "\r", 0, 0}, + { XK_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_Insert, ControlMask, "\033[L", -1, 0}, + { XK_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_Delete, ControlMask, "\033[M", -1, 0}, + { XK_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_BackSpace, XK_NO_MOD, "\177", 0, 0}, + { XK_BackSpace, Mod1Mask, "\033\177", 0, 0}, + { XK_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_End, ControlMask, "\033[J", -1, 0}, + { XK_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_End, ShiftMask, "\033[K", -1, 0}, + { XK_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_Prior, ControlMask, "\033[5;5~", 0, 0}, + { XK_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_Next, ControlMask, "\033[6;5~", 0, 0}, + { XK_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_F1, XK_NO_MOD, "\033OP" , 0, 0}, + { XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0}, + { XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0}, + { XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0}, + { XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0}, + { XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0}, + { XK_F2, XK_NO_MOD, "\033OQ" , 0, 0}, + { XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0}, + { XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0}, + { XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0}, + { XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0}, + { XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0}, + { XK_F3, XK_NO_MOD, "\033OR" , 0, 0}, + { XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0}, + { XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0}, + { XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0}, + { XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0}, + { XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0}, + { XK_F4, XK_NO_MOD, "\033OS" , 0, 0}, + { XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0}, + { XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0}, + { XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0}, + { XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0}, + { XK_F5, XK_NO_MOD, "\033[15~", 0, 0}, + { XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0}, + { XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0}, + { XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0}, + { XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0}, + { XK_F6, XK_NO_MOD, "\033[17~", 0, 0}, + { XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0}, + { XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0}, + { XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0}, + { XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0}, + { XK_F7, XK_NO_MOD, "\033[18~", 0, 0}, + { XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0}, + { XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0}, + { XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0}, + { XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0}, + { XK_F8, XK_NO_MOD, "\033[19~", 0, 0}, + { XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0}, + { XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0}, + { XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0}, + { XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0}, + { XK_F9, XK_NO_MOD, "\033[20~", 0, 0}, + { XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0}, + { XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0}, + { XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0}, + { XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0}, + { XK_F10, XK_NO_MOD, "\033[21~", 0, 0}, + { XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0}, + { XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0}, + { XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0}, + { XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0}, + { XK_F11, XK_NO_MOD, "\033[23~", 0, 0}, + { XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0}, + { XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0}, + { XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0}, + { XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0}, + { XK_F12, XK_NO_MOD, "\033[24~", 0, 0}, + { XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0}, + { XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0}, + { XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0}, + { XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0}, + { XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0}, + { XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0}, + { XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0}, + { XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0}, + { XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0}, + { XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0}, + { XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0}, + { XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0}, + { XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0}, + { XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0}, + { XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0}, + { XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0}, + { XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0}, + { XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0}, + { XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0}, + { XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0}, + { XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0}, + { XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0}, + { XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0}, + { XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0}, + { XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0}, + { XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0}, + { XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0}, +}; + +/* + * Selection types' masks. + * Use the same masks as usual. + * Button1Mask is always unset, to make masks match between ButtonPress. + * ButtonRelease and MotionNotify. + * If no match is found, regular selection is used. + */ +static uint selmasks[] = { + [SEL_RECTANGULAR] = Mod1Mask, +}; + +/* + * Printable characters in ASCII, used to estimate the advance width + * of single wide characters. + */ +static char ascii_printable[] = + " !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~"; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..2fc854e --- /dev/null +++ b/config.mk @@ -0,0 +1,36 @@ +# st version +VERSION = 0.9.3 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +PKG_CONFIG = pkg-config + +# includes and libs +INCS = -I$(X11INC) \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +# flags +STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 +STCFLAGS = $(INCS) $(STCPPFLAGS) $(CPPFLAGS) $(CFLAGS) +STLDFLAGS = $(LIBS) $(LDFLAGS) + +# OpenBSD: +#CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE +#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \ +# `$(PKG_CONFIG) --libs fontconfig` \ +# `$(PKG_CONFIG) --libs freetype2` +#MANPREFIX = ${PREFIX}/man + +# compiler and linker +# CC = c99 diff --git a/st.1 b/st.1 new file mode 100644 index 0000000..39120b4 --- /dev/null +++ b/st.1 @@ -0,0 +1,177 @@ +.TH ST 1 st\-VERSION +.SH NAME +st \- simple terminal +.SH SYNOPSIS +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-l +.IR line ] +.RB [ \-w +.IR windowid ] +.RB [[ \-e ] +.IR command +.RI [ arguments ...]] +.PP +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-w +.IR windowid ] +.RB \-l +.IR line +.RI [ stty_args ...] +.SH DESCRIPTION +.B st +is a simple terminal emulator. +.SH OPTIONS +.TP +.B \-a +disable alternate screens in terminal +.TP +.BI \-c " class" +defines the window class (default $TERM). +.TP +.BI \-f " font" +defines the +.I font +to use when st is run. +.TP +.BI \-g " geometry" +defines the X11 geometry string. +The form is [=][{xX}][{+-}{+-}]. See +.BR XParseGeometry (3) +for further details. +.TP +.B \-i +will fixate the position given with the -g option. +.TP +.BI \-n " name" +defines the window instance name (default $TERM). +.TP +.BI \-o " iofile" +writes all the I/O to +.I iofile. +This feature is useful when recording st sessions. A value of "-" means +standard output. +.TP +.BI \-T " title" +defines the window title (default 'st'). +.TP +.BI \-t " title" +defines the window title (default 'st'). +.TP +.BI \-w " windowid" +embeds st within the window identified by +.I windowid +.TP +.BI \-l " line" +use a tty +.I line +instead of a pseudo terminal. +.I line +should be a (pseudo-)serial device (e.g. /dev/ttyS0 on Linux for serial port +0). +When this flag is given +remaining arguments are used as flags for +.BR stty(1). +By default st initializes the serial line to 8 bits, no parity, 1 stop bit +and a 38400 baud rate. The speed is set by appending it as last argument +(e.g. 'st -l /dev/ttyS0 115200'). Arguments before the last one are +.BR stty(1) +flags. If you want to set odd parity on 115200 baud use for example 'st -l +/dev/ttyS0 parenb parodd 115200'. Set the number of bits by using for +example 'st -l /dev/ttyS0 cs7 115200'. See +.BR stty(1) +for more arguments and cases. +.TP +.B \-v +prints version information to stderr, then exits. +.TP +.BI \-e " command " [ " arguments " "... ]" +st executes +.I command +instead of the shell. If this is used it +.B must be the last option +on the command line, as in xterm / rxvt. +This option is only intended for compatibility, +and all the remaining arguments are used as a command +even without it. +.SH SHORTCUTS +.TP +.B Break +Send a break in the serial line. +Break key is obtained in PC keyboards +pressing at the same time control and pause. +.TP +.B Ctrl-Print Screen +Toggle if st should print to the +.I iofile. +.TP +.B Shift-Print Screen +Print the full screen to the +.I iofile. +.TP +.B Print Screen +Print the selection to the +.I iofile. +.TP +.B Ctrl-Shift-Page Up +Increase font size. +.TP +.B Ctrl-Shift-Page Down +Decrease font size. +.TP +.B Ctrl-Shift-Home +Reset to default font size. +.TP +.B Ctrl-Shift-y +Paste from primary selection (middle mouse button). +.TP +.B Ctrl-Shift-c +Copy the selected text to the clipboard selection. +.TP +.B Ctrl-Shift-v +Paste from the clipboard selection. +.SH CUSTOMIZATION +.B st +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR tabbed (1), +.BR utmp (1), +.BR stty (1), +.BR scroll (1) +.SH BUGS +See the TODO file in the distribution. + diff --git a/st.c b/st.c new file mode 100644 index 0000000..6f40e35 --- /dev/null +++ b/st.c @@ -0,0 +1,2705 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "st.h" +#include "win.h" + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#endif + +/* Arbitrary sizes */ +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 +#define ESC_BUF_SIZ (128*UTF_SIZ) +#define ESC_ARG_SIZ 16 +#define STR_BUF_SIZ ESC_BUF_SIZ +#define STR_ARG_SIZ ESC_ARG_SIZ + +/* macros */ +#define IS_SET(flag) ((term.mode & (flag)) != 0) +#define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +#define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f)) +#define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c)) +#define ISDELIM(u) (u && wcschr(worddelimiters, u)) + +enum term_mode { + MODE_WRAP = 1 << 0, + MODE_INSERT = 1 << 1, + MODE_ALTSCREEN = 1 << 2, + MODE_CRLF = 1 << 3, + MODE_ECHO = 1 << 4, + MODE_PRINT = 1 << 5, + MODE_UTF8 = 1 << 6, +}; + +enum cursor_movement { + CURSOR_SAVE, + CURSOR_LOAD +}; + +enum cursor_state { + CURSOR_DEFAULT = 0, + CURSOR_WRAPNEXT = 1, + CURSOR_ORIGIN = 2 +}; + +enum charset { + CS_GRAPHIC0, + CS_GRAPHIC1, + CS_UK, + CS_USA, + CS_MULTI, + CS_GER, + CS_FIN +}; + +enum escape_state { + ESC_START = 1, + ESC_CSI = 2, + ESC_STR = 4, /* DCS, OSC, PM, APC */ + ESC_ALTCHARSET = 8, + ESC_STR_END = 16, /* a final string was encountered */ + ESC_TEST = 32, /* Enter in test mode */ + ESC_UTF8 = 64, +}; + +typedef struct { + Glyph attr; /* current char attributes */ + int x; + int y; + char state; +} TCursor; + +typedef struct { + int mode; + int type; + int snap; + /* + * Selection variables: + * nb – normalized coordinates of the beginning of the selection + * ne – normalized coordinates of the end of the selection + * ob – original coordinates of the beginning of the selection + * oe – original coordinates of the end of the selection + */ + struct { + int x, y; + } nb, ne, ob, oe; + + int alt; +} Selection; + +/* Internal representation of the screen */ +typedef struct { + int row; /* nb row */ + int col; /* nb col */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ + TCursor c; /* cursor */ + int ocx; /* old cursor col */ + int ocy; /* old cursor row */ + int top; /* top scroll limit */ + int bot; /* bottom scroll limit */ + int mode; /* terminal mode flags */ + int esc; /* escape state flags */ + char trantbl[4]; /* charset table translation */ + int charset; /* current charset */ + int icharset; /* selected charset for sequence */ + int *tabs; + Rune lastc; /* last printed char outside of sequence, 0 if control */ +} Term; + +/* CSI Escape sequence structs */ +/* ESC '[' [[ [] [;]] []] */ +typedef struct { + char buf[ESC_BUF_SIZ]; /* raw string */ + size_t len; /* raw string length */ + char priv; + int arg[ESC_ARG_SIZ]; + int narg; /* nb of args */ + char mode[2]; +} CSIEscape; + +/* STR Escape sequence structs */ +/* ESC type [[ [] [;]] ] ESC '\' */ +typedef struct { + char type; /* ESC type ... */ + char *buf; /* allocated raw string */ + size_t siz; /* allocation size */ + size_t len; /* raw string length */ + char *args[STR_ARG_SIZ]; + int narg; /* nb of args */ +} STREscape; + +static void execsh(char *, char **); +static void stty(char **); +static void sigchld(int); +static void ttywriteraw(const char *, size_t); + +static void csidump(void); +static void csihandle(void); +static void csiparse(void); +static void csireset(void); +static void osc_color_response(int, int, int); +static int eschandle(uchar); +static void strdump(void); +static void strhandle(void); +static void strparse(void); +static void strreset(void); + +static void tprinter(char *, size_t); +static void tdumpsel(void); +static void tdumpline(int); +static void tdump(void); +static void tclearregion(int, int, int, int); +static void tcursor(int); +static void tdeletechar(int); +static void tdeleteline(int); +static void tinsertblank(int); +static void tinsertblankline(int); +static int tlinelen(int); +static void tmoveto(int, int); +static void tmoveato(int, int); +static void tnewline(int); +static void tputtab(int); +static void tputc(Rune); +static void treset(void); +static void tscrollup(int, int); +static void tscrolldown(int, int); +static void tsetattr(const int *, int); +static void tsetchar(Rune, const Glyph *, int, int); +static void tsetdirt(int, int); +static void tsetscroll(int, int); +static void tswapscreen(void); +static void tsetmode(int, int, const int *, int); +static int twrite(const char *, int, int); +static void tfulldirt(void); +static void tcontrolcode(uchar ); +static void tdectest(char ); +static void tdefutf8(char); +static int32_t tdefcolor(const int *, int *, int); +static void tdeftran(char); +static void tstrsequence(uchar); + +static void drawregion(int, int, int, int); + +static void selnormalize(void); +static void selscroll(int, int); +static void selsnap(int *, int *, int); + +static size_t utf8decode(const char *, Rune *, size_t); +static Rune utf8decodebyte(char, size_t *); +static char utf8encodebyte(Rune, size_t); +static size_t utf8validate(Rune *, size_t); + +static char *base64dec(const char *); +static char base64dec_getc(const char **); + +static ssize_t xwrite(int, const char *, size_t); + +/* Globals */ +static Term term; +static Selection sel; +static CSIEscape csiescseq; +static STREscape strescseq; +static int iofd = 1; +static int cmdfd; +static pid_t pid; + +static const uchar utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +ssize_t +xwrite(int fd, const char *s, size_t len) +{ + size_t aux = len; + ssize_t r; + + while (len > 0) { + r = write(fd, s, len); + if (r < 0) + return r; + len -= r; + s += r; + } + + return aux; +} + +void * +xmalloc(size_t len) +{ + void *p; + + if (!(p = malloc(len))) + die("malloc: %s\n", strerror(errno)); + + return p; +} + +void * +xrealloc(void *p, size_t len) +{ + if ((p = realloc(p, len)) == NULL) + die("realloc: %s\n", strerror(errno)); + + return p; +} + +char * +xstrdup(const char *s) +{ + char *p; + + if ((p = strdup(s)) == NULL) + die("strdup: %s\n", strerror(errno)); + + return p; +} + +size_t +utf8decode(const char *c, Rune *u, size_t clen) +{ + size_t i, j, len, type; + Rune udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type != 0) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Rune +utf8decodebyte(char c, size_t *i) +{ + for (*i = 0; *i < LEN(utfmask); ++(*i)) + if (((uchar)c & utfmask[*i]) == utfbyte[*i]) + return (uchar)c & ~utfmask[*i]; + + return 0; +} + +size_t +utf8encode(Rune u, char *c) +{ + size_t len, i; + + len = utf8validate(&u, 0); + if (len > UTF_SIZ) + return 0; + + for (i = len - 1; i != 0; --i) { + c[i] = utf8encodebyte(u, 0); + u >>= 6; + } + c[0] = utf8encodebyte(u, len); + + return len; +} + +char +utf8encodebyte(Rune u, size_t i) +{ + return utfbyte[i] | (u & ~utfmask[i]); +} + +size_t +utf8validate(Rune *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + + return i; +} + +char +base64dec_getc(const char **src) +{ + while (**src && !isprint((unsigned char)**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char * +base64dec(const char *src) +{ + size_t in_len = strlen(src); + char *result, *dst; + static const char base64_digits[256] = { + [43] = 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, + 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + if (in_len % 4) + in_len += 4 - (in_len % 4); + result = dst = xmalloc(in_len / 4 * 3 + 1); + while (*src) { + int a = base64_digits[(unsigned char) base64dec_getc(&src)]; + int b = base64_digits[(unsigned char) base64dec_getc(&src)]; + int c = base64_digits[(unsigned char) base64dec_getc(&src)]; + int d = base64_digits[(unsigned char) base64dec_getc(&src)]; + + /* invalid input. 'a' can be -1, e.g. if src is "\n" (c-str) */ + if (a == -1 || b == -1) + break; + + *dst++ = (a << 2) | ((b & 0x30) >> 4); + if (c == -1) + break; + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + if (d == -1) + break; + *dst++ = ((c & 0x03) << 6) | d; + } + *dst = '\0'; + return result; +} + +void +selinit(void) +{ + sel.mode = SEL_IDLE; + sel.snap = 0; + sel.ob.x = -1; +} + +int +tlinelen(int y) +{ + int i = term.col; + + if (term.line[y][i - 1].mode & ATTR_WRAP) + return i; + + while (i > 0 && term.line[y][i - 1].u == ' ') + --i; + + return i; +} + +void +selstart(int col, int row, int snap) +{ + selclear(); + sel.mode = SEL_EMPTY; + sel.type = SEL_REGULAR; + sel.alt = IS_SET(MODE_ALTSCREEN); + sel.snap = snap; + sel.oe.x = sel.ob.x = col; + sel.oe.y = sel.ob.y = row; + selnormalize(); + + if (sel.snap != 0) + sel.mode = SEL_READY; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +selextend(int col, int row, int type, int done) +{ + int oldey, oldex, oldsby, oldsey, oldtype; + + if (sel.mode == SEL_IDLE) + return; + if (done && sel.mode == SEL_EMPTY) { + selclear(); + return; + } + + oldey = sel.oe.y; + oldex = sel.oe.x; + oldsby = sel.nb.y; + oldsey = sel.ne.y; + oldtype = sel.type; + + sel.oe.x = col; + sel.oe.y = row; + selnormalize(); + sel.type = type; + + if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY) + tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); + + sel.mode = done ? SEL_IDLE : SEL_READY; +} + +void +selnormalize(void) +{ + int i; + + if (sel.type == SEL_REGULAR && sel.ob.y != sel.oe.y) { + sel.nb.x = sel.ob.y < sel.oe.y ? sel.ob.x : sel.oe.x; + sel.ne.x = sel.ob.y < sel.oe.y ? sel.oe.x : sel.ob.x; + } else { + sel.nb.x = MIN(sel.ob.x, sel.oe.x); + sel.ne.x = MAX(sel.ob.x, sel.oe.x); + } + sel.nb.y = MIN(sel.ob.y, sel.oe.y); + sel.ne.y = MAX(sel.ob.y, sel.oe.y); + + selsnap(&sel.nb.x, &sel.nb.y, -1); + selsnap(&sel.ne.x, &sel.ne.y, +1); + + /* expand selection over line breaks */ + if (sel.type == SEL_RECTANGULAR) + return; + i = tlinelen(sel.nb.y); + if (i < sel.nb.x) + sel.nb.x = i; + if (tlinelen(sel.ne.y) <= sel.ne.x) + sel.ne.x = term.col - 1; +} + +int +selected(int x, int y) +{ + if (sel.mode == SEL_EMPTY || sel.ob.x == -1 || + sel.alt != IS_SET(MODE_ALTSCREEN)) + return 0; + + if (sel.type == SEL_RECTANGULAR) + return BETWEEN(y, sel.nb.y, sel.ne.y) + && BETWEEN(x, sel.nb.x, sel.ne.x); + + return BETWEEN(y, sel.nb.y, sel.ne.y) + && (y != sel.nb.y || x >= sel.nb.x) + && (y != sel.ne.y || x <= sel.ne.x); +} + +void +selsnap(int *x, int *y, int direction) +{ + int newx, newy, xt, yt; + int delim, prevdelim; + const Glyph *gp, *prevgp; + + switch (sel.snap) { + case SNAP_WORD: + /* + * Snap around if the word wraps around at the end or + * beginning of a line. + */ + prevgp = &term.line[*y][*x]; + prevdelim = ISDELIM(prevgp->u); + for (;;) { + newx = *x + direction; + newy = *y; + if (!BETWEEN(newx, 0, term.col - 1)) { + newy += direction; + newx = (newx + term.col) % term.col; + if (!BETWEEN(newy, 0, term.row - 1)) + break; + + if (direction > 0) + yt = *y, xt = *x; + else + yt = newy, xt = newx; + if (!(term.line[yt][xt].mode & ATTR_WRAP)) + break; + } + + if (newx >= tlinelen(newy)) + break; + + gp = &term.line[newy][newx]; + delim = ISDELIM(gp->u); + if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim + || (delim && gp->u != prevgp->u))) + break; + + *x = newx; + *y = newy; + prevgp = gp; + prevdelim = delim; + } + break; + case SNAP_LINE: + /* + * Snap around if the the previous line or the current one + * has set ATTR_WRAP at its end. Then the whole next or + * previous line will be selected. + */ + *x = (direction < 0) ? 0 : term.col - 1; + if (direction < 0) { + for (; *y > 0; *y += direction) { + if (!(term.line[*y-1][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } else if (direction > 0) { + for (; *y < term.row-1; *y += direction) { + if (!(term.line[*y][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } + break; + } +} + +char * +getsel(void) +{ + char *str, *ptr; + int y, bufsize, lastx, linelen; + const Glyph *gp, *last; + + if (sel.ob.x == -1) + return NULL; + + bufsize = (term.col+1) * (sel.ne.y-sel.nb.y+1) * UTF_SIZ; + ptr = str = xmalloc(bufsize); + + /* append every set & selected glyph to the selection */ + for (y = sel.nb.y; y <= sel.ne.y; y++) { + if ((linelen = tlinelen(y)) == 0) { + *ptr++ = '\n'; + continue; + } + + if (sel.type == SEL_RECTANGULAR) { + gp = &term.line[y][sel.nb.x]; + lastx = sel.ne.x; + } else { + gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0]; + lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; + } + last = &term.line[y][MIN(lastx, linelen-1)]; + while (last >= gp && last->u == ' ') + --last; + + for ( ; gp <= last; ++gp) { + if (gp->mode & ATTR_WDUMMY) + continue; + + ptr += utf8encode(gp->u, ptr); + } + + /* + * Copy and pasting of line endings is inconsistent + * in the inconsistent terminal and GUI world. + * The best solution seems like to produce '\n' when + * something is copied from st and convert '\n' to + * '\r', when something to be pasted is received by + * st. + * FIXME: Fix the computer world. + */ + if ((y < sel.ne.y || lastx >= linelen) && + (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR)) + *ptr++ = '\n'; + } + *ptr = 0; + return str; +} + +void +selclear(void) +{ + if (sel.ob.x == -1) + return; + sel.mode = SEL_IDLE; + sel.ob.x = -1; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +void +execsh(char *cmd, char **args) +{ + char *sh, *prog, *arg; + const struct passwd *pw; + + errno = 0; + if ((pw = getpwuid(getuid())) == NULL) { + if (errno) + die("getpwuid: %s\n", strerror(errno)); + else + die("who are you?\n"); + } + + if ((sh = getenv("SHELL")) == NULL) + sh = (pw->pw_shell[0]) ? pw->pw_shell : cmd; + + if (args) { + prog = args[0]; + arg = NULL; + } else if (scroll) { + prog = scroll; + arg = utmp ? utmp : sh; + } else if (utmp) { + prog = utmp; + arg = NULL; + } else { + prog = sh; + arg = NULL; + } + DEFAULT(args, ((char *[]) {prog, arg, NULL})); + + unsetenv("COLUMNS"); + unsetenv("LINES"); + unsetenv("TERMCAP"); + setenv("LOGNAME", pw->pw_name, 1); + setenv("USER", pw->pw_name, 1); + setenv("SHELL", sh, 1); + setenv("HOME", pw->pw_dir, 1); + setenv("TERM", termname, 1); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + execvp(prog, args); + _exit(1); +} + +void +sigchld(int a) +{ + int stat; + pid_t p; + + if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); + + if (pid != p) + return; + + if (WIFEXITED(stat) && WEXITSTATUS(stat)) + die("child exited with status %d\n", WEXITSTATUS(stat)); + else if (WIFSIGNALED(stat)) + die("child terminated due to signal %d\n", WTERMSIG(stat)); + _exit(0); +} + +void +stty(char **args) +{ + char cmd[_POSIX_ARG_MAX], **p, *q, *s; + size_t n, siz; + + if ((n = strlen(stty_args)) > sizeof(cmd)-1) + die("incorrect stty parameters\n"); + memcpy(cmd, stty_args, n); + q = cmd + n; + siz = sizeof(cmd) - n; + for (p = args; p && (s = *p); ++p) { + if ((n = strlen(s)) > siz-1) + die("stty parameter length too long\n"); + *q++ = ' '; + memcpy(q, s, n); + q += n; + siz -= n + 1; + } + *q = '\0'; + if (system(cmd) != 0) + perror("Couldn't call stty"); +} + +int +ttynew(const char *line, char *cmd, const char *out, char **args) +{ + int m, s; + + if (out) { + term.mode |= MODE_PRINT; + iofd = (!strcmp(out, "-")) ? + 1 : open(out, O_WRONLY | O_CREAT, 0666); + if (iofd < 0) { + fprintf(stderr, "Error opening %s:%s\n", + out, strerror(errno)); + } + } + + if (line) { + if ((cmdfd = open(line, O_RDWR)) < 0) + die("open line '%s' failed: %s\n", + line, strerror(errno)); + dup2(cmdfd, 0); + stty(args); + return cmdfd; + } + + /* seems to work fine on linux, openbsd and freebsd */ + if (openpty(&m, &s, NULL, NULL, NULL) < 0) + die("openpty failed: %s\n", strerror(errno)); + + switch (pid = fork()) { + case -1: + die("fork failed: %s\n", strerror(errno)); + break; + case 0: + close(iofd); + close(m); + setsid(); /* create a new process group */ + dup2(s, 0); + dup2(s, 1); + dup2(s, 2); + if (ioctl(s, TIOCSCTTY, NULL) < 0) + die("ioctl TIOCSCTTY failed: %s\n", strerror(errno)); + if (s > 2) + close(s); +#ifdef __OpenBSD__ + if (pledge("stdio getpw proc exec", NULL) == -1) + die("pledge\n"); +#endif + execsh(cmd, args); + break; + default: +#ifdef __OpenBSD__ + if (pledge("stdio rpath tty proc", NULL) == -1) + die("pledge\n"); +#endif + close(s); + cmdfd = m; + signal(SIGCHLD, sigchld); + break; + } + return cmdfd; +} + +size_t +ttyread(void) +{ + static char buf[BUFSIZ]; + static int buflen = 0; + int ret, written; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); + + switch (ret) { + case 0: + exit(0); + case -1: + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; + written = twrite(buf, buflen, 0); + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) + memmove(buf, buf + written, buflen); + return ret; + } +} + +void +ttywrite(const char *s, size_t n, int may_echo) +{ + const char *next; + + if (may_echo && IS_SET(MODE_ECHO)) + twrite(s, n, 1); + + if (!IS_SET(MODE_CRLF)) { + ttywriteraw(s, n); + return; + } + + /* This is similar to how the kernel handles ONLCR for ttys */ + while (n > 0) { + if (*s == '\r') { + next = s + 1; + ttywriteraw("\r\n", 2); + } else { + next = memchr(s, '\r', n); + DEFAULT(next, s + n); + ttywriteraw(s, next - s); + } + n -= next - s; + s = next; + } +} + +void +ttywriteraw(const char *s, size_t n) +{ + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; + + /* + * Remember that we are using a pty, which might be a modem line. + * Writing too much will clog the line. That's why we are doing this + * dance. + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); + FD_SET(cmdfd, &rfd); + + /* Check if we can write. */ + if (pselect(cmdfd+1, &rfd, &wfd, NULL, NULL, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + if (FD_ISSET(cmdfd, &wfd)) { + /* + * Only write the bytes written by ttywrite() or the + * default of 256. This seems to be a reasonable value + * for a serial line. Bigger values might clog the I/O. + */ + if ((r = write(cmdfd, s, (n < lim)? n : lim)) < 0) + goto write_error; + if (r < n) { + /* + * We weren't able to write out everything. + * This means the buffer is getting full + * again. Empty it. + */ + if (n < lim) + lim = ttyread(); + n -= r; + s += r; + } else { + /* All bytes have been written. */ + break; + } + } + if (FD_ISSET(cmdfd, &rfd)) + lim = ttyread(); + } + return; + +write_error: + die("write error on tty: %s\n", strerror(errno)); +} + +void +ttyresize(int tw, int th) +{ + struct winsize w; + + w.ws_row = term.row; + w.ws_col = term.col; + w.ws_xpixel = tw; + w.ws_ypixel = th; + if (ioctl(cmdfd, TIOCSWINSZ, &w) < 0) + fprintf(stderr, "Couldn't set window size: %s\n", strerror(errno)); +} + +void +ttyhangup(void) +{ + /* Send SIGHUP to shell */ + kill(pid, SIGHUP); +} + +int +tattrset(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) + return 1; + } + } + + return 0; +} + +void +tsetdirt(int top, int bot) +{ + int i; + + if (term.row <= 0) + return; + + LIMIT(top, 0, term.row-1); + LIMIT(bot, 0, term.row-1); + + for (i = top; i <= bot; i++) + term.dirty[i] = 1; +} + +void +tsetdirtattr(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) { + tsetdirt(i, i); + break; + } + } + } +} + +void +tfulldirt(void) +{ + tsetdirt(0, term.row-1); +} + +void +tcursor(int mode) +{ + static TCursor c[2]; + int alt = IS_SET(MODE_ALTSCREEN); + + if (mode == CURSOR_SAVE) { + c[alt] = term.c; + } else if (mode == CURSOR_LOAD) { + term.c = c[alt]; + tmoveto(c[alt].x, c[alt].y); + } +} + +void +treset(void) +{ + uint i; + + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, + .bg = defaultbg + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + for (i = tabspaces; i < term.col; i += tabspaces) + term.tabs[i] = 1; + term.top = 0; + term.bot = term.row - 1; + term.mode = MODE_WRAP|MODE_UTF8; + memset(term.trantbl, CS_USA, sizeof(term.trantbl)); + term.charset = 0; + + for (i = 0; i < 2; i++) { + tmoveto(0, 0); + tcursor(CURSOR_SAVE); + tclearregion(0, 0, term.col-1, term.row-1); + tswapscreen(); + } +} + +void +tnew(int col, int row) +{ + term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + tresize(col, row); + treset(); +} + +void +tswapscreen(void) +{ + Line *tmp = term.line; + + term.line = term.alt; + term.alt = tmp; + term.mode ^= MODE_ALTSCREEN; + tfulldirt(); +} + +void +tscrolldown(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + tsetdirt(orig, term.bot-n); + tclearregion(0, term.bot-n+1, term.col-1, term.bot); + + for (i = term.bot; i >= orig+n; i--) { + temp = term.line[i]; + term.line[i] = term.line[i-n]; + term.line[i-n] = temp; + } + + selscroll(orig, n); +} + +void +tscrollup(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + tclearregion(0, orig, term.col-1, orig+n-1); + tsetdirt(orig+n, term.bot); + + for (i = orig; i <= term.bot-n; i++) { + temp = term.line[i]; + term.line[i] = term.line[i+n]; + term.line[i+n] = temp; + } + + selscroll(orig, -n); +} + +void +selscroll(int orig, int n) +{ + if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) + return; + + if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) { + selclear(); + } else if (BETWEEN(sel.nb.y, orig, term.bot)) { + sel.ob.y += n; + sel.oe.y += n; + if (sel.ob.y < term.top || sel.ob.y > term.bot || + sel.oe.y < term.top || sel.oe.y > term.bot) { + selclear(); + } else { + selnormalize(); + } + } +} + +void +tnewline(int first_col) +{ + int y = term.c.y; + + if (y == term.bot) { + tscrollup(term.top, 1); + } else { + y++; + } + tmoveto(first_col ? 0 : term.c.x, y); +} + +void +csiparse(void) +{ + char *p = csiescseq.buf, *np; + long int v; + int sep = ';'; /* colon or semi-colon, but not both */ + + csiescseq.narg = 0; + if (*p == '?') { + csiescseq.priv = 1; + p++; + } + + csiescseq.buf[csiescseq.len] = '\0'; + while (p < csiescseq.buf+csiescseq.len) { + np = NULL; + v = strtol(p, &np, 10); + if (np == p) + v = 0; + if (v == LONG_MAX || v == LONG_MIN) + v = -1; + csiescseq.arg[csiescseq.narg++] = v; + p = np; + if (sep == ';' && *p == ':') + sep = ':'; /* allow override to colon once */ + if (*p != sep || csiescseq.narg == ESC_ARG_SIZ) + break; + p++; + } + csiescseq.mode[0] = *p++; + csiescseq.mode[1] = (p < csiescseq.buf+csiescseq.len) ? *p : '\0'; +} + +/* for absolute user moves, when decom is set */ +void +tmoveato(int x, int y) +{ + tmoveto(x, y + ((term.c.state & CURSOR_ORIGIN) ? term.top: 0)); +} + +void +tmoveto(int x, int y) +{ + int miny, maxy; + + if (term.c.state & CURSOR_ORIGIN) { + miny = term.top; + maxy = term.bot; + } else { + miny = 0; + maxy = term.row - 1; + } + term.c.state &= ~CURSOR_WRAPNEXT; + term.c.x = LIMIT(x, 0, term.col-1); + term.c.y = LIMIT(y, miny, maxy); +} + +void +tsetchar(Rune u, const Glyph *attr, int x, int y) +{ + static const char *vt100_0[62] = { /* 0x41 - 0x7e */ + "↑", "↓", "→", "←", "█", "▚", "☃", /* A - G */ + 0, 0, 0, 0, 0, 0, 0, 0, /* H - O */ + 0, 0, 0, 0, 0, 0, 0, 0, /* P - W */ + 0, 0, 0, 0, 0, 0, 0, " ", /* X - _ */ + "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", /* ` - g */ + "␤", "␋", "┘", "┐", "┌", "└", "┼", "⎺", /* h - o */ + "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */ + "│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */ + }; + + /* + * The table is proudly stolen from rxvt. + */ + if (term.trantbl[term.charset] == CS_GRAPHIC0 && + BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41]) + utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ); + + if (term.line[y][x].mode & ATTR_WIDE) { + if (x+1 < term.col) { + term.line[y][x+1].u = ' '; + term.line[y][x+1].mode &= ~ATTR_WDUMMY; + } + } else if (term.line[y][x].mode & ATTR_WDUMMY) { + term.line[y][x-1].u = ' '; + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; +} + +void +tclearregion(int x1, int y1, int x2, int y2) +{ + int x, y, temp; + Glyph *gp; + + if (x1 > x2) + temp = x1, x1 = x2, x2 = temp; + if (y1 > y2) + temp = y1, y1 = y2, y2 = temp; + + LIMIT(x1, 0, term.col-1); + LIMIT(x2, 0, term.col-1); + LIMIT(y1, 0, term.row-1); + LIMIT(y2, 0, term.row-1); + + for (y = y1; y <= y2; y++) { + term.dirty[y] = 1; + for (x = x1; x <= x2; x++) { + gp = &term.line[y][x]; + if (selected(x, y)) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; + gp->mode = 0; + gp->u = ' '; + } + } +} + +void +tdeletechar(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x; + src = term.c.x + n; + size = term.col - src; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(term.col-n, term.c.y, term.col-1, term.c.y); +} + +void +tinsertblank(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x + n; + src = term.c.x; + size = term.col - dst; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(src, term.c.y, dst - 1, term.c.y); +} + +void +tinsertblankline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrolldown(term.c.y, n); +} + +void +tdeleteline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrollup(term.c.y, n); +} + +int32_t +tdefcolor(const int *attr, int *npar, int l) +{ + int32_t idx = -1; + uint r, g, b; + + switch (attr[*npar + 1]) { + case 2: /* direct color in RGB space */ + if (*npar + 4 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + if (!BETWEEN(r, 0, 255) || !BETWEEN(g, 0, 255) || !BETWEEN(b, 0, 255)) + fprintf(stderr, "erresc: bad rgb color (%u,%u,%u)\n", + r, g, b); + else + idx = TRUECOLOR(r, g, b); + break; + case 5: /* indexed color */ + if (*npar + 2 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + *npar += 2; + if (!BETWEEN(attr[*npar], 0, 255)) + fprintf(stderr, "erresc: bad fgcolor %d\n", attr[*npar]); + else + idx = attr[*npar]; + break; + case 0: /* implemented defined (only foreground) */ + case 1: /* transparent */ + case 3: /* direct color in CMY space */ + case 4: /* direct color in CMYK space */ + default: + fprintf(stderr, + "erresc(38): gfx attr %d unknown\n", attr[*npar]); + break; + } + + return idx; +} + +void +tsetattr(const int *attr, int l) +{ + int i; + int32_t idx; + + for (i = 0; i < l; i++) { + switch (attr[i]) { + case 0: + term.c.attr.mode &= ~( + ATTR_BOLD | + ATTR_FAINT | + ATTR_ITALIC | + ATTR_UNDERLINE | + ATTR_BLINK | + ATTR_REVERSE | + ATTR_INVISIBLE | + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; + break; + case 2: + term.c.attr.mode |= ATTR_FAINT; + break; + case 3: + term.c.attr.mode |= ATTR_ITALIC; + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ + case 6: /* rapid blink */ + term.c.attr.mode |= ATTR_BLINK; + break; + case 7: + term.c.attr.mode |= ATTR_REVERSE; + break; + case 8: + term.c.attr.mode |= ATTR_INVISIBLE; + break; + case 9: + term.c.attr.mode |= ATTR_STRUCK; + break; + case 22: + term.c.attr.mode &= ~(ATTR_BOLD | ATTR_FAINT); + break; + case 23: + term.c.attr.mode &= ~ATTR_ITALIC; + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; + break; + case 27: + term.c.attr.mode &= ~ATTR_REVERSE; + break; + case 28: + term.c.attr.mode &= ~ATTR_INVISIBLE; + break; + case 29: + term.c.attr.mode &= ~ATTR_STRUCK; + break; + case 38: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.fg = idx; + break; + case 39: /* set foreground color to default */ + term.c.attr.fg = defaultfg; + break; + case 48: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.bg = idx; + break; + case 49: /* set background color to default */ + term.c.attr.bg = defaultbg; + break; + case 58: + /* This starts a sequence to change the color of + * "underline" pixels. We don't support that and + * instead eat up a following "5;n" or "2;r;g;b". */ + tdefcolor(attr, &i, l); + break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; + } else if (BETWEEN(attr[i], 40, 47)) { + term.c.attr.bg = attr[i] - 40; + } else if (BETWEEN(attr[i], 90, 97)) { + term.c.attr.fg = attr[i] - 90 + 8; + } else if (BETWEEN(attr[i], 100, 107)) { + term.c.attr.bg = attr[i] - 100 + 8; + } else { + fprintf(stderr, + "erresc(default): gfx attr %d unknown\n", + attr[i]); + csidump(); + } + break; + } + } +} + +void +tsetscroll(int t, int b) +{ + int temp; + + LIMIT(t, 0, term.row-1); + LIMIT(b, 0, term.row-1); + if (t > b) { + temp = t; + t = b; + b = temp; + } + term.top = t; + term.bot = b; +} + +void +tsetmode(int priv, int set, const int *args, int narg) +{ + int alt; const int *lim; + + for (lim = args + narg; args < lim; ++args) { + if (priv) { + switch (*args) { + case 1: /* DECCKM -- Cursor key */ + xsetmode(set, MODE_APPCURSOR); + break; + case 5: /* DECSCNM -- Reverse video */ + xsetmode(set, MODE_REVERSE); + break; + case 6: /* DECOM -- Origin */ + MODBIT(term.c.state, set, CURSOR_ORIGIN); + tmoveato(0, 0); + break; + case 7: /* DECAWM -- Auto wrap */ + MODBIT(term.mode, set, MODE_WRAP); + break; + case 0: /* Error (IGNORED) */ + case 2: /* DECANM -- ANSI/VT52 (IGNORED) */ + case 3: /* DECCOLM -- Column (IGNORED) */ + case 4: /* DECSCLM -- Scroll (IGNORED) */ + case 8: /* DECARM -- Auto repeat (IGNORED) */ + case 18: /* DECPFF -- Printer feed (IGNORED) */ + case 19: /* DECPEX -- Printer extent (IGNORED) */ + case 42: /* DECNRCM -- National characters (IGNORED) */ + case 12: /* att610 -- Start blinking cursor (IGNORED) */ + break; + case 25: /* DECTCEM -- Text Cursor Enable Mode */ + xsetmode(!set, MODE_HIDE); + break; + case 9: /* X10 mouse compatibility mode */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEX10); + break; + case 1000: /* 1000: report button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEBTN); + break; + case 1002: /* 1002: report motion on button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMOTION); + break; + case 1003: /* 1003: enable all mouse motions */ + xsetpointermotion(set); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMANY); + break; + case 1004: /* 1004: send focus events to tty */ + xsetmode(set, MODE_FOCUS); + break; + case 1006: /* 1006: extended reporting mode */ + xsetmode(set, MODE_MOUSESGR); + break; + case 1034: /* 1034: enable 8-bit mode for keyboard input */ + xsetmode(set, MODE_8BIT); + break; + case 1049: /* swap screen & set/restore cursor as xterm */ + if (!allowaltscreen) + break; + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + /* FALLTHROUGH */ + case 47: /* swap screen buffer */ + case 1047: /* swap screen buffer */ + if (!allowaltscreen) + break; + alt = IS_SET(MODE_ALTSCREEN); + if (alt) { + tclearregion(0, 0, term.col-1, + term.row-1); + } + if (set ^ alt) /* set is always 1 or 0 */ + tswapscreen(); + if (*args != 1049) + break; + /* FALLTHROUGH */ + case 1048: /* save/restore cursor (like DECSC/DECRC) */ + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + break; + case 2004: /* 2004: bracketed paste mode */ + xsetmode(set, MODE_BRCKTPASTE); + break; + /* Not implemented mouse modes. See comments there. */ + case 1001: /* mouse highlight mode; can hang the + terminal by design when implemented. */ + case 1005: /* UTF-8 mouse mode; will confuse + applications not supporting UTF-8 + and luit. */ + case 1015: /* urxvt mangled mouse mode; incompatible + and can be mistaken for other control + codes. */ + break; + default: + fprintf(stderr, + "erresc: unknown private set/reset mode %d\n", + *args); + break; + } + } else { + switch (*args) { + case 0: /* Error (IGNORED) */ + break; + case 2: + xsetmode(set, MODE_KBDLOCK); + break; + case 4: /* IRM -- Insertion-replacement */ + MODBIT(term.mode, set, MODE_INSERT); + break; + case 12: /* SRM -- Send/Receive */ + MODBIT(term.mode, !set, MODE_ECHO); + break; + case 20: /* LNM -- Linefeed/new line */ + MODBIT(term.mode, set, MODE_CRLF); + break; + default: + fprintf(stderr, + "erresc: unknown set/reset mode %d\n", + *args); + break; + } + } + } +} + +void +csihandle(void) +{ + char buf[40]; + int len; + + switch (csiescseq.mode[0]) { + default: + unknown: + fprintf(stderr, "erresc: unknown csi "); + csidump(); + /* die(""); */ + break; + case '@': /* ICH -- Insert blank char */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblank(csiescseq.arg[0]); + break; + case 'A': /* CUU -- Cursor Up */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y-csiescseq.arg[0]); + break; + case 'B': /* CUD -- Cursor Down */ + case 'e': /* VPR --Cursor Down */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y+csiescseq.arg[0]); + break; + case 'i': /* MC -- Media Copy */ + switch (csiescseq.arg[0]) { + case 0: + tdump(); + break; + case 1: + tdumpline(term.c.y); + break; + case 2: + tdumpsel(); + break; + case 4: + term.mode &= ~MODE_PRINT; + break; + case 5: + term.mode |= MODE_PRINT; + break; + } + break; + case 'c': /* DA -- Device Attributes */ + if (csiescseq.arg[0] == 0) + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'b': /* REP -- if last char is printable print it more times */ + LIMIT(csiescseq.arg[0], 1, 65535); + if (term.lastc) + while (csiescseq.arg[0]-- > 0) + tputc(term.lastc); + break; + case 'C': /* CUF -- Cursor Forward */ + case 'a': /* HPR -- Cursor Forward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x+csiescseq.arg[0], term.c.y); + break; + case 'D': /* CUB -- Cursor Backward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x-csiescseq.arg[0], term.c.y); + break; + case 'E': /* CNL -- Cursor Down and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y+csiescseq.arg[0]); + break; + case 'F': /* CPL -- Cursor Up and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y-csiescseq.arg[0]); + break; + case 'g': /* TBC -- Tabulation clear */ + switch (csiescseq.arg[0]) { + case 0: /* clear current tab stop */ + term.tabs[term.c.x] = 0; + break; + case 3: /* clear all the tabs */ + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + break; + default: + goto unknown; + } + break; + case 'G': /* CHA -- Move to */ + case '`': /* HPA */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(csiescseq.arg[0]-1, term.c.y); + break; + case 'H': /* CUP -- Move to */ + case 'f': /* HVP */ + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], 1); + tmoveato(csiescseq.arg[1]-1, csiescseq.arg[0]-1); + break; + case 'I': /* CHT -- Cursor Forward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(csiescseq.arg[0]); + break; + case 'J': /* ED -- Clear screen */ + switch (csiescseq.arg[0]) { + case 0: /* below */ + tclearregion(term.c.x, term.c.y, term.col-1, term.c.y); + if (term.c.y < term.row-1) { + tclearregion(0, term.c.y+1, term.col-1, + term.row-1); + } + break; + case 1: /* above */ + if (term.c.y > 0) + tclearregion(0, 0, term.col-1, term.c.y-1); + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, 0, term.col-1, term.row-1); + break; + default: + goto unknown; + } + break; + case 'K': /* EL -- Clear line */ + switch (csiescseq.arg[0]) { + case 0: /* right */ + tclearregion(term.c.x, term.c.y, term.col-1, + term.c.y); + break; + case 1: /* left */ + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, term.c.y, term.col-1, term.c.y); + break; + } + break; + case 'S': /* SU -- Scroll line up */ + if (csiescseq.priv) break; + DEFAULT(csiescseq.arg[0], 1); + tscrollup(term.top, csiescseq.arg[0]); + break; + case 'T': /* SD -- Scroll line down */ + DEFAULT(csiescseq.arg[0], 1); + tscrolldown(term.top, csiescseq.arg[0]); + break; + case 'L': /* IL -- Insert blank lines */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblankline(csiescseq.arg[0]); + break; + case 'l': /* RM -- Reset Mode */ + tsetmode(csiescseq.priv, 0, csiescseq.arg, csiescseq.narg); + break; + case 'M': /* DL -- Delete lines */ + DEFAULT(csiescseq.arg[0], 1); + tdeleteline(csiescseq.arg[0]); + break; + case 'X': /* ECH -- Erase char */ + DEFAULT(csiescseq.arg[0], 1); + tclearregion(term.c.x, term.c.y, + term.c.x + csiescseq.arg[0] - 1, term.c.y); + break; + case 'P': /* DCH -- Delete char */ + DEFAULT(csiescseq.arg[0], 1); + tdeletechar(csiescseq.arg[0]); + break; + case 'Z': /* CBT -- Cursor Backward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(-csiescseq.arg[0]); + break; + case 'd': /* VPA -- Move to */ + DEFAULT(csiescseq.arg[0], 1); + tmoveato(term.c.x, csiescseq.arg[0]-1); + break; + case 'h': /* SM -- Set terminal mode */ + tsetmode(csiescseq.priv, 1, csiescseq.arg, csiescseq.narg); + break; + case 'm': /* SGR -- Terminal attribute (color) */ + tsetattr(csiescseq.arg, csiescseq.narg); + break; + case 'n': /* DSR -- Device Status Report */ + switch (csiescseq.arg[0]) { + case 5: /* Status Report "OK" `0n` */ + ttywrite("\033[0n", sizeof("\033[0n") - 1, 0); + break; + case 6: /* Report Cursor Position (CPR) ";R" */ + len = snprintf(buf, sizeof(buf), "\033[%i;%iR", + term.c.y+1, term.c.x+1); + ttywrite(buf, len, 0); + break; + default: + goto unknown; + } + break; + case 'r': /* DECSTBM -- Set Scrolling Region */ + if (csiescseq.priv) { + goto unknown; + } else { + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], term.row); + tsetscroll(csiescseq.arg[0]-1, csiescseq.arg[1]-1); + tmoveato(0, 0); + } + break; + case 's': /* DECSC -- Save cursor position (ANSI.SYS) */ + tcursor(CURSOR_SAVE); + break; + case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ + if (csiescseq.priv) { + goto unknown; + } else { + tcursor(CURSOR_LOAD); + } + break; + case ' ': + switch (csiescseq.mode[1]) { + case 'q': /* DECSCUSR -- Set Cursor Style */ + if (xsetcursor(csiescseq.arg[0])) + goto unknown; + break; + default: + goto unknown; + } + break; + } +} + +void +csidump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC["); + for (i = 0; i < csiescseq.len; i++) { + c = csiescseq.buf[i] & 0xff; + if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + putc('\n', stderr); +} + +void +csireset(void) +{ + memset(&csiescseq, 0, sizeof(csiescseq)); +} + +void +osc_color_response(int num, int index, int is_osc4) +{ + int n; + char buf[32]; + unsigned char r, g, b; + + if (xgetcolor(is_osc4 ? num : index, &r, &g, &b)) { + fprintf(stderr, "erresc: failed to fetch %s color %d\n", + is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index); + return; + } + + n = snprintf(buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", + is_osc4 ? "4;" : "", num, r, r, g, g, b, b); + if (n < 0 || n >= sizeof(buf)) { + fprintf(stderr, "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc"); + } else { + ttywrite(buf, n, 1); + } +} + +void +strhandle(void) +{ + char *p = NULL, *dec; + int j, narg, par; + const struct { int idx; char *str; } osc_table[] = { + { defaultfg, "foreground" }, + { defaultbg, "background" }, + { defaultcs, "cursor" } + }; + + term.esc &= ~(ESC_STR_END|ESC_STR); + strparse(); + par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0; + + switch (strescseq.type) { + case ']': /* OSC -- Operating System Command */ + switch (par) { + case 0: + if (narg > 1) { + xsettitle(strescseq.args[1]); + xseticontitle(strescseq.args[1]); + } + return; + case 1: + if (narg > 1) + xseticontitle(strescseq.args[1]); + return; + case 2: + if (narg > 1) + xsettitle(strescseq.args[1]); + return; + case 52: /* manipulate selection data */ + if (narg > 2 && allowwindowops) { + dec = base64dec(strescseq.args[2]); + if (dec) { + xsetsel(dec); + xclipcopy(); + } else { + fprintf(stderr, "erresc: invalid base64\n"); + } + } + return; + case 10: /* set dynamic VT100 text foreground color */ + case 11: /* set dynamic VT100 text background color */ + case 12: /* set dynamic text cursor color */ + if (narg < 2) + break; + p = strescseq.args[1]; + if ((j = par - 10) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + + if (!strcmp(p, "?")) { + osc_color_response(par, osc_table[j].idx, 0); + } else if (xsetcolorname(osc_table[j].idx, p)) { + fprintf(stderr, "erresc: invalid %s color: %s\n", + osc_table[j].str, p); + } else { + tfulldirt(); + } + return; + case 4: /* color set */ + if (narg < 3) + break; + p = strescseq.args[2]; + /* FALLTHROUGH */ + case 104: /* color reset */ + j = (narg > 1) ? atoi(strescseq.args[1]) : -1; + + if (p && !strcmp(p, "?")) { + osc_color_response(j, 0, 1); + } else if (xsetcolorname(j, p)) { + if (par == 104 && narg <= 1) { + xloadcols(); + return; /* color reset without parameter */ + } + fprintf(stderr, "erresc: invalid color j=%d, p=%s\n", + j, p ? p : "(null)"); + } else { + /* + * TODO if defaultbg color is changed, borders + * are dirty + */ + tfulldirt(); + } + return; + case 110: /* reset dynamic VT100 text foreground color */ + case 111: /* reset dynamic VT100 text background color */ + case 112: /* reset dynamic text cursor color */ + if (narg != 1) + break; + if ((j = par - 110) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + if (xsetcolorname(osc_table[j].idx, NULL)) { + fprintf(stderr, "erresc: %s color not found\n", osc_table[j].str); + } else { + tfulldirt(); + } + return; + } + break; + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + return; + } + + fprintf(stderr, "erresc: unknown str "); + strdump(); +} + +void +strparse(void) +{ + int c; + char *p = strescseq.buf; + + strescseq.narg = 0; + strescseq.buf[strescseq.len] = '\0'; + + if (*p == '\0') + return; + + while (strescseq.narg < STR_ARG_SIZ) { + strescseq.args[strescseq.narg++] = p; + while ((c = *p) != ';' && c != '\0') + ++p; + if (c == '\0') + return; + *p++ = '\0'; + } +} + +void +strdump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC%c", strescseq.type); + for (i = 0; i < strescseq.len; i++) { + c = strescseq.buf[i] & 0xff; + if (c == '\0') { + putc('\n', stderr); + return; + } else if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + fprintf(stderr, "ESC\\\n"); +} + +void +strreset(void) +{ + strescseq = (STREscape){ + .buf = xrealloc(strescseq.buf, STR_BUF_SIZ), + .siz = STR_BUF_SIZ, + }; +} + +void +sendbreak(const Arg *arg) +{ + if (tcsendbreak(cmdfd, 0)) + perror("Error sending break"); +} + +void +tprinter(char *s, size_t len) +{ + if (iofd != -1 && xwrite(iofd, s, len) < 0) { + perror("Error writing to output file"); + close(iofd); + iofd = -1; + } +} + +void +toggleprinter(const Arg *arg) +{ + term.mode ^= MODE_PRINT; +} + +void +printscreen(const Arg *arg) +{ + tdump(); +} + +void +printsel(const Arg *arg) +{ + tdumpsel(); +} + +void +tdumpsel(void) +{ + char *ptr; + + if ((ptr = getsel())) { + tprinter(ptr, strlen(ptr)); + free(ptr); + } +} + +void +tdumpline(int n) +{ + char buf[UTF_SIZ]; + const Glyph *bp, *end; + + bp = &term.line[n][0]; + end = &bp[MIN(tlinelen(n), term.col) - 1]; + if (bp != end || bp->u != ' ') { + for ( ; bp <= end; ++bp) + tprinter(buf, utf8encode(bp->u, buf)); + } + tprinter("\n", 1); +} + +void +tdump(void) +{ + int i; + + for (i = 0; i < term.row; ++i) + tdumpline(i); +} + +void +tputtab(int n) +{ + uint x = term.c.x; + + if (n > 0) { + while (x < term.col && n--) + for (++x; x < term.col && !term.tabs[x]; ++x) + /* nothing */ ; + } else if (n < 0) { + while (x > 0 && n++) + for (--x; x > 0 && !term.tabs[x]; --x) + /* nothing */ ; + } + term.c.x = LIMIT(x, 0, term.col-1); +} + +void +tdefutf8(char ascii) +{ + if (ascii == 'G') + term.mode |= MODE_UTF8; + else if (ascii == '@') + term.mode &= ~MODE_UTF8; +} + +void +tdeftran(char ascii) +{ + static char cs[] = "0B"; + static int vcs[] = {CS_GRAPHIC0, CS_USA}; + char *p; + + if ((p = strchr(cs, ascii)) == NULL) { + fprintf(stderr, "esc unhandled charset: ESC ( %c\n", ascii); + } else { + term.trantbl[term.icharset] = vcs[p - cs]; + } +} + +void +tdectest(char c) +{ + int x, y; + + if (c == '8') { /* DEC screen alignment test. */ + for (x = 0; x < term.col; ++x) { + for (y = 0; y < term.row; ++y) + tsetchar('E', &term.c.attr, x, y); + } + } +} + +void +tstrsequence(uchar c) +{ + switch (c) { + case 0x90: /* DCS -- Device Control String */ + c = 'P'; + break; + case 0x9f: /* APC -- Application Program Command */ + c = '_'; + break; + case 0x9e: /* PM -- Privacy Message */ + c = '^'; + break; + case 0x9d: /* OSC -- Operating System Command */ + c = ']'; + break; + } + strreset(); + strescseq.type = c; + term.esc |= ESC_STR; +} + +void +tcontrolcode(uchar ascii) +{ + switch (ascii) { + case '\t': /* HT */ + tputtab(1); + return; + case '\b': /* BS */ + tmoveto(term.c.x-1, term.c.y); + return; + case '\r': /* CR */ + tmoveto(0, term.c.y); + return; + case '\f': /* LF */ + case '\v': /* VT */ + case '\n': /* LF */ + /* go to first col if the mode is set */ + tnewline(IS_SET(MODE_CRLF)); + return; + case '\a': /* BEL */ + if (term.esc & ESC_STR_END) { + /* backwards compatibility to xterm */ + strhandle(); + } else { + xbell(); + } + break; + case '\033': /* ESC */ + csireset(); + term.esc &= ~(ESC_CSI|ESC_ALTCHARSET|ESC_TEST); + term.esc |= ESC_START; + return; + case '\016': /* SO (LS1 -- Locking shift 1) */ + case '\017': /* SI (LS0 -- Locking shift 0) */ + term.charset = 1 - (ascii - '\016'); + return; + case '\032': /* SUB */ + tsetchar('?', &term.c.attr, term.c.x, term.c.y); + /* FALLTHROUGH */ + case '\030': /* CAN */ + csireset(); + break; + case '\005': /* ENQ (IGNORED) */ + case '\000': /* NUL (IGNORED) */ + case '\021': /* XON (IGNORED) */ + case '\023': /* XOFF (IGNORED) */ + case 0177: /* DEL (IGNORED) */ + return; + case 0x80: /* TODO: PAD */ + case 0x81: /* TODO: HOP */ + case 0x82: /* TODO: BPH */ + case 0x83: /* TODO: NBH */ + case 0x84: /* TODO: IND */ + break; + case 0x85: /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 0x86: /* TODO: SSA */ + case 0x87: /* TODO: ESA */ + break; + case 0x88: /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 0x89: /* TODO: HTJ */ + case 0x8a: /* TODO: VTS */ + case 0x8b: /* TODO: PLD */ + case 0x8c: /* TODO: PLU */ + case 0x8d: /* TODO: RI */ + case 0x8e: /* TODO: SS2 */ + case 0x8f: /* TODO: SS3 */ + case 0x91: /* TODO: PU1 */ + case 0x92: /* TODO: PU2 */ + case 0x93: /* TODO: STS */ + case 0x94: /* TODO: CCH */ + case 0x95: /* TODO: MW */ + case 0x96: /* TODO: SPA */ + case 0x97: /* TODO: EPA */ + case 0x98: /* TODO: SOS */ + case 0x99: /* TODO: SGCI */ + break; + case 0x9a: /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 0x9b: /* TODO: CSI */ + case 0x9c: /* TODO: ST */ + break; + case 0x90: /* DCS -- Device Control String */ + case 0x9d: /* OSC -- Operating System Command */ + case 0x9e: /* PM -- Privacy Message */ + case 0x9f: /* APC -- Application Program Command */ + tstrsequence(ascii); + return; + } + /* only CAN, SUB, \a and C1 chars interrupt a sequence */ + term.esc &= ~(ESC_STR_END|ESC_STR); +} + +/* + * returns 1 when the sequence is finished and it hasn't to read + * more characters for this sequence, otherwise 0 + */ +int +eschandle(uchar ascii) +{ + switch (ascii) { + case '[': + term.esc |= ESC_CSI; + return 0; + case '#': + term.esc |= ESC_TEST; + return 0; + case '%': + term.esc |= ESC_UTF8; + return 0; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + case ']': /* OSC -- Operating System Command */ + case 'k': /* old title set compatibility */ + tstrsequence(ascii); + return 0; + case 'n': /* LS2 -- Locking shift 2 */ + case 'o': /* LS3 -- Locking shift 3 */ + term.charset = 2 + (ascii - 'n'); + break; + case '(': /* GZD4 -- set primary charset G0 */ + case ')': /* G1D4 -- set secondary charset G1 */ + case '*': /* G2D4 -- set tertiary charset G2 */ + case '+': /* G3D4 -- set quaternary charset G3 */ + term.icharset = ascii - '('; + term.esc |= ESC_ALTCHARSET; + return 0; + case 'D': /* IND -- Linefeed */ + if (term.c.y == term.bot) { + tscrollup(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y+1); + } + break; + case 'E': /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 'H': /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 'M': /* RI -- Reverse index */ + if (term.c.y == term.top) { + tscrolldown(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y-1); + } + break; + case 'Z': /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'c': /* RIS -- Reset to initial state */ + treset(); + resettitle(); + xloadcols(); + xsetmode(0, MODE_HIDE); + xsetmode(0, MODE_BRCKTPASTE); + break; + case '=': /* DECPAM -- Application keypad */ + xsetmode(1, MODE_APPKEYPAD); + break; + case '>': /* DECPNM -- Normal keypad */ + xsetmode(0, MODE_APPKEYPAD); + break; + case '7': /* DECSC -- Save Cursor */ + tcursor(CURSOR_SAVE); + break; + case '8': /* DECRC -- Restore Cursor */ + tcursor(CURSOR_LOAD); + break; + case '\\': /* ST -- String Terminator */ + if (term.esc & ESC_STR_END) + strhandle(); + break; + default: + fprintf(stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", + (uchar) ascii, isprint(ascii)? ascii:'.'); + break; + } + return 1; +} + +void +tputc(Rune u) +{ + char c[UTF_SIZ]; + int control; + int width, len; + Glyph *gp; + + control = ISCONTROL(u); + if (u < 127 || !IS_SET(MODE_UTF8)) { + c[0] = u; + width = len = 1; + } else { + len = utf8encode(u, c); + if (!control && (width = wcwidth(u)) == -1) + width = 1; + } + + if (IS_SET(MODE_PRINT)) + tprinter(c, len); + + /* + * STR sequence must be checked before anything else + * because it uses all following characters until it + * receives a ESC, a SUB, a ST or any other C1 control + * character. + */ + if (term.esc & ESC_STR) { + if (u == '\a' || u == 030 || u == 032 || u == 033 || + ISCONTROLC1(u)) { + term.esc &= ~(ESC_START|ESC_STR); + term.esc |= ESC_STR_END; + goto check_control_code; + } + + if (strescseq.len+len >= strescseq.siz) { + /* + * Here is a bug in terminals. If the user never sends + * some code to stop the str or esc command, then st + * will stop responding. But this is better than + * silently failing with unknown characters. At least + * then users will report back. + * + * In the case users ever get fixed, here is the code: + */ + /* + * term.esc = 0; + * strhandle(); + */ + if (strescseq.siz > (SIZE_MAX - UTF_SIZ) / 2) + return; + strescseq.siz *= 2; + strescseq.buf = xrealloc(strescseq.buf, strescseq.siz); + } + + memmove(&strescseq.buf[strescseq.len], c, len); + strescseq.len += len; + return; + } + +check_control_code: + /* + * Actions of control codes must be performed as soon they arrive + * because they can be embedded inside a control sequence, and + * they must not cause conflicts with sequences. + */ + if (control) { + /* in UTF-8 mode ignore handling C1 control characters */ + if (IS_SET(MODE_UTF8) && ISCONTROLC1(u)) + return; + tcontrolcode(u); + /* + * control codes are not shown ever + */ + if (!term.esc) + term.lastc = 0; + return; + } else if (term.esc & ESC_START) { + if (term.esc & ESC_CSI) { + csiescseq.buf[csiescseq.len++] = u; + if (BETWEEN(u, 0x40, 0x7E) + || csiescseq.len >= \ + sizeof(csiescseq.buf)-1) { + term.esc = 0; + csiparse(); + csihandle(); + } + return; + } else if (term.esc & ESC_UTF8) { + tdefutf8(u); + } else if (term.esc & ESC_ALTCHARSET) { + tdeftran(u); + } else if (term.esc & ESC_TEST) { + tdectest(u); + } else { + if (!eschandle(u)) + return; + /* sequence already finished */ + } + term.esc = 0; + /* + * All characters which form part of a sequence are not + * printed + */ + return; + } + if (selected(term.c.x, term.c.y)) + selclear(); + + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; + tnewline(1); + gp = &term.line[term.c.y][term.c.x]; + } + + if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) { + memmove(gp+width, gp, (term.col - term.c.x - width) * sizeof(Glyph)); + gp->mode &= ~ATTR_WIDE; + } + + if (term.c.x+width > term.col) { + if (IS_SET(MODE_WRAP)) + tnewline(1); + else + tmoveto(term.col - width, term.c.y); + gp = &term.line[term.c.y][term.c.x]; + } + + tsetchar(u, &term.c.attr, term.c.x, term.c.y); + term.lastc = u; + + if (width == 2) { + gp->mode |= ATTR_WIDE; + if (term.c.x+1 < term.col) { + if (gp[1].mode == ATTR_WIDE && term.c.x+2 < term.col) { + gp[2].u = ' '; + gp[2].mode &= ~ATTR_WDUMMY; + } + gp[1].u = '\0'; + gp[1].mode = ATTR_WDUMMY; + } + } + if (term.c.x+width < term.col) { + tmoveto(term.c.x+width, term.c.y); + } else { + term.c.state |= CURSOR_WRAPNEXT; + } +} + +int +twrite(const char *buf, int buflen, int show_ctrl) +{ + int charsize; + Rune u; + int n; + + for (n = 0; n < buflen; n += charsize) { + if (IS_SET(MODE_UTF8)) { + /* process a complete utf8 char */ + charsize = utf8decode(buf + n, &u, buflen - n); + if (charsize == 0) + break; + } else { + u = buf[n] & 0xFF; + charsize = 1; + } + if (show_ctrl && ISCONTROL(u)) { + if (u & 0x80) { + u &= 0x7f; + tputc('^'); + tputc('['); + } else if (u != '\n' && u != '\r' && u != '\t') { + u ^= 0x40; + tputc('^'); + } + } + tputc(u); + } + return n; +} + +void +tresize(int col, int row) +{ + int i; + int minrow = MIN(row, term.row); + int mincol = MIN(col, term.col); + int *bp; + TCursor c; + + if (col < 1 || row < 1) { + fprintf(stderr, + "tresize: error resizing to %dx%d\n", col, row); + return; + } + + /* + * slide screen to keep cursor where we expect it - + * tscrollup would work here, but we can optimize to + * memmove because we're freeing the earlier lines + */ + for (i = 0; i <= term.c.y - row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + /* ensure that both src and dst are not NULL */ + if (i > 0) { + memmove(term.line, term.line + i, row * sizeof(Line)); + memmove(term.alt, term.alt + i, row * sizeof(Line)); + } + for (i += row; i < term.row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + + /* resize to new height */ + term.line = xrealloc(term.line, row * sizeof(Line)); + term.alt = xrealloc(term.alt, row * sizeof(Line)); + term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty)); + term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs)); + + /* resize each row to new width, zero-pad if needed */ + for (i = 0; i < minrow; i++) { + term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph)); + term.alt[i] = xrealloc(term.alt[i], col * sizeof(Glyph)); + } + + /* allocate any new rows */ + for (/* i = minrow */; i < row; i++) { + term.line[i] = xmalloc(col * sizeof(Glyph)); + term.alt[i] = xmalloc(col * sizeof(Glyph)); + } + if (col > term.col) { + bp = term.tabs + term.col; + + memset(bp, 0, sizeof(*term.tabs) * (col - term.col)); + while (--bp > term.tabs && !*bp) + /* nothing */ ; + for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) + *bp = 1; + } + /* update terminal size */ + term.col = col; + term.row = row; + /* reset scrolling region */ + tsetscroll(0, row-1); + /* make use of the LIMIT in tmoveto */ + tmoveto(term.c.x, term.c.y); + /* Clearing both screens (it makes dirty all lines) */ + c = term.c; + for (i = 0; i < 2; i++) { + if (mincol < col && 0 < minrow) { + tclearregion(mincol, 0, col - 1, minrow - 1); + } + if (0 < col && minrow < row) { + tclearregion(0, minrow, col - 1, row - 1); + } + tswapscreen(); + tcursor(CURSOR_LOAD); + } + term.c = c; +} + +void +resettitle(void) +{ + xsettitle(NULL); +} + +void +drawregion(int x1, int y1, int x2, int y2) +{ + int y; + + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; + + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } +} + +void +draw(void) +{ + int cx = term.c.x, ocx = term.ocx, ocy = term.ocy; + + if (!xstartdraw()) + return; + + /* adjust cursor position */ + LIMIT(term.ocx, 0, term.col-1); + LIMIT(term.ocy, 0, term.row-1); + if (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY) + term.ocx--; + if (term.line[term.c.y][cx].mode & ATTR_WDUMMY) + cx--; + + drawregion(0, 0, term.col, term.row); + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; + term.ocy = term.c.y; + xfinishdraw(); + if (ocx != term.ocx || ocy != term.ocy) + xximspot(term.ocx, term.ocy); +} + +void +redraw(void) +{ + tfulldirt(); + draw(); +} diff --git a/st.h b/st.h new file mode 100644 index 0000000..fd3b0d8 --- /dev/null +++ b/st.h @@ -0,0 +1,126 @@ +/* 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 != (b).mode || (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/st.info b/st.info new file mode 100644 index 0000000..efab2cf --- /dev/null +++ b/st.info @@ -0,0 +1,243 @@ +st-mono| simpleterm monocolor, + acsc=+C\,D-A.B0E``aaffgghFiGjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + am, + bce, + bel=^G, + blink=\E[5m, + bold=\E[1m, + cbt=\E[Z, + cvvis=\E[?25h, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?12l\E[?25h, + colors#2, + cols#80, + cr=^M, + csr=\E[%i%p1%d;%p2%dr, + cub=\E[%p1%dD, + cub1=^H, + cud1=^J, + cud=\E[%p1%dB, + cuf1=\E[C, + cuf=\E[%p1%dC, + cup=\E[%i%p1%d;%p2%dH, + cuu1=\E[A, + cuu=\E[%p1%dA, + dch=\E[%p1%dP, + dch1=\E[P, + dim=\E[2m, + dl=\E[%p1%dM, + dl1=\E[M, + ech=\E[%p1%dX, + ed=\E[J, + el=\E[K, + el1=\E[1K, + enacs=\E)0, + flash=\E[?5h$<80/>\E[?5l, + fsl=^G, + home=\E[H, + hpa=\E[%i%p1%dG, + hs, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + il1=\E[L, + il=\E[%p1%dL, + ind=^J, + indn=\E[%p1%dS, + invis=\E[8m, + is2=\E[4l\E>\E[?1034l, + it#8, + kel=\E[1;2F, + ked=\E[1;5F, + ka1=\E[1~, + ka3=\E[5~, + kc1=\E[4~, + kc3=\E[6~, + kbs=\177, + kcbt=\E[Z, + kb2=\EOu, + kcub1=\EOD, + kcud1=\EOB, + kcuf1=\EOC, + kcuu1=\EOA, + kDC=\E[3;2~, + kent=\EOM, + kEND=\E[1;2F, + kIC=\E[2;2~, + kNXT=\E[6;2~, + kPRV=\E[5;2~, + kHOM=\E[1;2H, + kLFT=\E[1;2D, + kRIT=\E[1;2C, + kind=\E[1;2B, + kri=\E[1;2A, + kclr=\E[3;5~, + kdl1=\E[3;2~, + kdch1=\E[3~, + kich1=\E[2~, + kend=\E[4~, + kf1=\EOP, + kf2=\EOQ, + kf3=\EOR, + kf4=\EOS, + kf5=\E[15~, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[1;2P, + kf14=\E[1;2Q, + kf15=\E[1;2R, + kf16=\E[1;2S, + kf17=\E[15;2~, + kf18=\E[17;2~, + kf19=\E[18;2~, + kf20=\E[19;2~, + kf21=\E[20;2~, + kf22=\E[21;2~, + kf23=\E[23;2~, + kf24=\E[24;2~, + kf25=\E[1;5P, + kf26=\E[1;5Q, + kf27=\E[1;5R, + kf28=\E[1;5S, + kf29=\E[15;5~, + kf30=\E[17;5~, + kf31=\E[18;5~, + kf32=\E[19;5~, + kf33=\E[20;5~, + kf34=\E[21;5~, + kf35=\E[23;5~, + kf36=\E[24;5~, + kf37=\E[1;6P, + kf38=\E[1;6Q, + kf39=\E[1;6R, + kf40=\E[1;6S, + kf41=\E[15;6~, + kf42=\E[17;6~, + kf43=\E[18;6~, + kf44=\E[19;6~, + kf45=\E[20;6~, + kf46=\E[21;6~, + kf47=\E[23;6~, + kf48=\E[24;6~, + kf49=\E[1;3P, + kf50=\E[1;3Q, + kf51=\E[1;3R, + kf52=\E[1;3S, + kf53=\E[15;3~, + kf54=\E[17;3~, + kf55=\E[18;3~, + kf56=\E[19;3~, + kf57=\E[20;3~, + kf58=\E[21;3~, + kf59=\E[23;3~, + kf60=\E[24;3~, + kf61=\E[1;4P, + kf62=\E[1;4Q, + kf63=\E[1;4R, + khome=\E[1~, + kil1=\E[2;5~, + krmir=\E[2;2~, + knp=\E[6~, + kmous=\E[M, + kpp=\E[5~, + lines#24, + mir, + msgr, + npc, + op=\E[39;49m, + pairs#64, + mc0=\E[i, + mc4=\E[4i, + mc5=\E[5i, + rc=\E8, + rev=\E[7m, + ri=\EM, + rin=\E[%p1%dT, + ritm=\E[23m, + rmacs=\E(B, + rmcup=\E[?1049l, + rmir=\E[4l, + rmkx=\E[?1l\E>, + rmso=\E[27m, + rmul=\E[24m, + rs1=\Ec, + rs2=\E[4l\E>\E[?1034l, + sc=\E7, + sitm=\E[3m, + sgr0=\E[0m, + smacs=\E(0, + smcup=\E[?1049h, + smir=\E[4h, + smkx=\E[?1h\E=, + smso=\E[7m, + smul=\E[4m, + tbc=\E[3g, + tsl=\E]0;, + xenl, + vpa=\E[%i%p1%dd, +# XTerm extensions + rmxx=\E[29m, + smxx=\E[9m, + BE=\E[?2004h, + BD=\E[?2004l, + PS=\E[200~, + PE=\E[201~, +# disabled rep for now: causes some issues with older ncurses versions. +# rep=%p1%c\E[%p2%{1}%-%db, +# tmux extensions, see TERMINFO EXTENSIONS in tmux(1) + Tc, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, + +st| simpleterm, + use=st-mono, + colors#8, + setab=\E[4%p1%dm, + setaf=\E[3%p1%dm, + setb=\E[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + setf=\E[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, + +st-256color| simpleterm with 256 colors, + use=st, + ccc, + colors#256, + oc=\E]104\007, + pairs#32767, +# Nicked from xterm-256color + 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, + +st-meta| simpleterm with meta key, + use=st, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-meta-256color| simpleterm with meta key and 256 colors, + use=st-256color, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-bs| simpleterm with backspace as backspace, + use=st, + kbs=\010, + kdch1=\177, + +st-bs-256color| simpleterm with backspace as backspace and 256colors, + use=st-256color, + kbs=\010, + kdch1=\177, diff --git a/win.h b/win.h new file mode 100644 index 0000000..6de960d --- /dev/null +++ b/win.h @@ -0,0 +1,41 @@ +/* See LICENSE for license details. */ + +enum win_mode { + MODE_VISIBLE = 1 << 0, + MODE_FOCUSED = 1 << 1, + MODE_APPKEYPAD = 1 << 2, + MODE_MOUSEBTN = 1 << 3, + MODE_MOUSEMOTION = 1 << 4, + MODE_REVERSE = 1 << 5, + MODE_KBDLOCK = 1 << 6, + MODE_HIDE = 1 << 7, + MODE_APPCURSOR = 1 << 8, + MODE_MOUSESGR = 1 << 9, + MODE_8BIT = 1 << 10, + MODE_BLINK = 1 << 11, + MODE_FBLINK = 1 << 12, + MODE_FOCUS = 1 << 13, + MODE_MOUSEX10 = 1 << 14, + MODE_MOUSEMANY = 1 << 15, + MODE_BRCKTPASTE = 1 << 16, + MODE_NUMLOCK = 1 << 17, + MODE_MOUSE = MODE_MOUSEBTN|MODE_MOUSEMOTION|MODE_MOUSEX10\ + |MODE_MOUSEMANY, +}; + +void xbell(void); +void xclipcopy(void); +void xdrawcursor(int, int, Glyph, int, int, Glyph); +void xdrawline(Line, int, int, int); +void xfinishdraw(void); +void xloadcols(void); +int xsetcolorname(int, const char *); +int xgetcolor(int, unsigned char *, unsigned char *, unsigned char *); +void xseticontitle(char *); +void xsettitle(char *); +int xsetcursor(int); +void xsetmode(int, unsigned int); +void xsetpointermotion(int); +void xsetsel(char *); +int xstartdraw(void); +void xximspot(int, int); diff --git a/x.c b/x.c new file mode 100644 index 0000000..d73152b --- /dev/null +++ b/x.c @@ -0,0 +1,2108 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +char *argv0; +#include "arg.h" +#include "st.h" +#include "win.h" + +/* types used in config.h */ +typedef struct { + uint mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Shortcut; + +typedef struct { + uint mod; + uint button; + void (*func)(const Arg *); + const Arg arg; + uint release; +} MouseShortcut; + +typedef struct { + KeySym k; + uint mask; + char *s; + /* three-valued logic variables: 0 indifferent, 1 on, -1 off */ + signed char appkey; /* application keypad */ + signed char appcursor; /* application cursor */ +} Key; + +/* X modifiers */ +#define XK_ANY_MOD UINT_MAX +#define XK_NO_MOD 0 +#define XK_SWITCH_MOD (1<<13|1<<14) + +/* function definitions used in config.h */ +static void clipcopy(const Arg *); +static void clippaste(const Arg *); +static void numlock(const Arg *); +static void selpaste(const Arg *); +static void zoom(const Arg *); +static void zoomabs(const Arg *); +static void zoomreset(const Arg *); +static void ttysend(const Arg *); + +/* config.h for applying patches and the configuration. */ +#include "config.h" + +/* XEMBED messages */ +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 + +/* macros */ +#define IS_SET(flag) ((win.mode & (flag)) != 0) +#define TRUERED(x) (((x) & 0xff0000) >> 8) +#define TRUEGREEN(x) (((x) & 0xff00)) +#define TRUEBLUE(x) (((x) & 0xff) << 8) + +typedef XftDraw *Draw; +typedef XftColor Color; +typedef XftGlyphFontSpec GlyphFontSpec; + +/* Purely graphic info */ +typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ + int cursor; /* cursor style */ +} TermWindow; + +typedef struct { + Display *dpy; + Colormap cmap; + Window win; + Drawable buf; + GlyphFontSpec *specbuf; /* font spec buffer used for rendering */ + Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid; + struct { + XIM xim; + XIC xic; + XPoint spot; + XVaNestedList spotlist; + } ime; + Draw draw; + Visual *vis; + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ +} XWindow; + +typedef struct { + Atom xtarget; + char *primary, *clipboard; + struct timespec tclick1; + struct timespec tclick2; +} XSelection; + +/* Font structure */ +#define Font Font_ +typedef struct { + int height; + int width; + int ascent; + int descent; + int badslant; + int badweight; + short lbearing; + short rbearing; + XftFont *match; + FcFontSet *set; + FcPattern *pattern; +} Font; + +/* Drawing Context */ +typedef struct { + Color *col; + size_t collen; + Font font, bfont, ifont, ibfont; + GC gc; +} DC; + +static inline ushort sixd_to_16bit(int); +static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); +static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); +static void xdrawglyph(Glyph, int, int); +static void xclear(int, int, int, int); +static int xgeommasktogravity(int); +static int ximopen(Display *); +static void ximinstantiate(Display *, XPointer, XPointer); +static void ximdestroy(XIM, XPointer, XPointer); +static int xicdestroy(XIC, XPointer, XPointer); +static void xinit(int, int); +static void cresize(int, int); +static void xresize(int, int); +static void xhints(void); +static int xloadcolor(int, const char *, Color *); +static int xloadfont(Font *, FcPattern *); +static void xloadfonts(const char *, double); +static void xunloadfont(Font *); +static void xunloadfonts(void); +static void xsetenv(void); +static void xseturgency(int); +static int evcol(XEvent *); +static int evrow(XEvent *); + +static void expose(XEvent *); +static void visibility(XEvent *); +static void unmap(XEvent *); +static void kpress(XEvent *); +static void cmessage(XEvent *); +static void resize(XEvent *); +static void focus(XEvent *); +static uint buttonmask(uint); +static int mouseaction(XEvent *, uint); +static void brelease(XEvent *); +static void bpress(XEvent *); +static void bmotion(XEvent *); +static void propnotify(XEvent *); +static void selnotify(XEvent *); +static void selclear_(XEvent *); +static void selrequest(XEvent *); +static void setsel(char *, Time); +static void mousesel(XEvent *, int); +static void mousereport(XEvent *); +static char *kmap(KeySym, uint); +static int match(uint, uint); + +static void run(void); +static void usage(void); + +static void (*handler[LASTEvent])(XEvent *) = { + [KeyPress] = kpress, + [ClientMessage] = cmessage, + [ConfigureNotify] = resize, + [VisibilityNotify] = visibility, + [UnmapNotify] = unmap, + [Expose] = expose, + [FocusIn] = focus, + [FocusOut] = focus, + [MotionNotify] = bmotion, + [ButtonPress] = bpress, + [ButtonRelease] = brelease, +/* + * Uncomment if you want the selection to disappear when you select something + * different in another window. + */ +/* [SelectionClear] = selclear_, */ + [SelectionNotify] = selnotify, +/* + * PropertyNotify is only turned on when there is some INCR transfer happening + * for the selection retrieval. + */ + [PropertyNotify] = propnotify, + [SelectionRequest] = selrequest, +}; + +/* Globals */ +static DC dc; +static XWindow xw; +static XSelection xsel; +static TermWindow win; + +/* Font Ring Cache */ +enum { + FRC_NORMAL, + FRC_ITALIC, + FRC_BOLD, + FRC_ITALICBOLD +}; + +typedef struct { + XftFont *font; + int flags; + Rune unicodep; +} Fontcache; + +/* Fontcache is an array now. A new font will be appended to the array. */ +static Fontcache *frc = NULL; +static int frclen = 0; +static int frccap = 0; +static char *usedfont = NULL; +static double usedfontsize = 0; +static double defaultfontsize = 0; + +static char *opt_class = NULL; +static char **opt_cmd = NULL; +static char *opt_embed = NULL; +static char *opt_font = NULL; +static char *opt_io = NULL; +static char *opt_line = NULL; +static char *opt_name = NULL; +static char *opt_title = NULL; + +static uint buttons; /* bit field of pressed buttons */ + +void +clipcopy(const Arg *dummy) +{ + Atom clipboard; + + free(xsel.clipboard); + xsel.clipboard = NULL; + + if (xsel.primary != NULL) { + xsel.clipboard = xstrdup(xsel.primary); + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XSetSelectionOwner(xw.dpy, clipboard, xw.win, CurrentTime); + } +} + +void +clippaste(const Arg *dummy) +{ + Atom clipboard; + + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard, + xw.win, CurrentTime); +} + +void +selpaste(const Arg *dummy) +{ + XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY, + xw.win, CurrentTime); +} + +void +numlock(const Arg *dummy) +{ + win.mode ^= MODE_NUMLOCK; +} + +void +zoom(const Arg *arg) +{ + Arg larg; + + larg.f = usedfontsize + arg->f; + zoomabs(&larg); +} + +void +zoomabs(const Arg *arg) +{ + xunloadfonts(); + xloadfonts(usedfont, arg->f); + cresize(0, 0); + redraw(); + xhints(); +} + +void +zoomreset(const Arg *arg) +{ + Arg larg; + + if (defaultfontsize > 0) { + larg.f = defaultfontsize; + zoomabs(&larg); + } +} + +void +ttysend(const Arg *arg) +{ + ttywrite(arg->s, strlen(arg->s), 1); +} + +int +evcol(XEvent *e) +{ + int x = e->xbutton.x - borderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; +} + +int +evrow(XEvent *e) +{ + int y = e->xbutton.y - borderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; +} + +void +mousesel(XEvent *e, int done) +{ + int type, seltype = SEL_REGULAR; + uint state = e->xbutton.state & ~(Button1Mask | forcemousemod); + + for (type = 1; type < LEN(selmasks); ++type) { + if (match(selmasks[type], state)) { + seltype = type; + break; + } + } + selextend(evcol(e), evrow(e), seltype, done); + if (done) + setsel(getsel(), e->xbutton.time); +} + +void +mousereport(XEvent *e) +{ + int len, btn, code; + int x = evcol(e), y = evrow(e); + int state = e->xbutton.state; + char buf[40]; + static int ox, oy; + + if (e->type == MotionNotify) { + if (x == ox && y == oy) + return; + if (!IS_SET(MODE_MOUSEMOTION) && !IS_SET(MODE_MOUSEMANY)) + return; + /* MODE_MOUSEMOTION: no reporting if no button is pressed */ + if (IS_SET(MODE_MOUSEMOTION) && buttons == 0) + return; + /* Set btn to lowest-numbered pressed button, or 12 if no + * buttons are pressed. */ + for (btn = 1; btn <= 11 && !(buttons & (1<<(btn-1))); btn++) + ; + code = 32; + } else { + btn = e->xbutton.button; + /* Only buttons 1 through 11 can be encoded */ + if (btn < 1 || btn > 11) + return; + if (e->type == ButtonRelease) { + /* MODE_MOUSEX10: no button release reporting */ + if (IS_SET(MODE_MOUSEX10)) + return; + /* Don't send release events for the scroll wheel */ + if (btn == 4 || btn == 5) + return; + } + code = 0; + } + + ox = x; + oy = y; + + /* Encode btn into code. If no button is pressed for a motion event in + * MODE_MOUSEMANY, then encode it as a release. */ + if ((!IS_SET(MODE_MOUSESGR) && e->type == ButtonRelease) || btn == 12) + code += 3; + else if (btn >= 8) + code += 128 + btn - 8; + else if (btn >= 4) + code += 64 + btn - 4; + else + code += btn - 1; + + if (!IS_SET(MODE_MOUSEX10)) { + code += ((state & ShiftMask ) ? 4 : 0) + + ((state & Mod1Mask ) ? 8 : 0) /* meta key: alt */ + + ((state & ControlMask) ? 16 : 0); + } + + if (IS_SET(MODE_MOUSESGR)) { + len = snprintf(buf, sizeof(buf), "\033[<%d;%d;%d%c", + code, x+1, y+1, + e->type == ButtonRelease ? 'm' : 'M'); + } else if (x < 223 && y < 223) { + len = snprintf(buf, sizeof(buf), "\033[M%c%c%c", + 32+code, 32+x+1, 32+y+1); + } else { + return; + } + + ttywrite(buf, len, 0); +} + +uint +buttonmask(uint button) +{ + return button == Button1 ? Button1Mask + : button == Button2 ? Button2Mask + : button == Button3 ? Button3Mask + : button == Button4 ? Button4Mask + : button == Button5 ? Button5Mask + : 0; +} + +int +mouseaction(XEvent *e, uint release) +{ + MouseShortcut *ms; + + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && + (match(ms->mod, state) || /* exact or forced */ + match(ms->mod, state & ~forcemousemod))) { + ms->func(&(ms->arg)); + return 1; + } + } + + return 0; +} + +void +bpress(XEvent *e) +{ + int btn = e->xbutton.button; + struct timespec now; + int snap; + + if (1 <= btn && btn <= 11) + buttons |= 1 << (btn-1); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 0)) + return; + + if (btn == Button1) { + /* + * If the user clicks below predefined timeouts specific + * snapping behaviour is exposed. + */ + clock_gettime(CLOCK_MONOTONIC, &now); + if (TIMEDIFF(now, xsel.tclick2) <= tripleclicktimeout) { + snap = SNAP_LINE; + } else if (TIMEDIFF(now, xsel.tclick1) <= doubleclicktimeout) { + snap = SNAP_WORD; + } else { + snap = 0; + } + xsel.tclick2 = xsel.tclick1; + xsel.tclick1 = now; + + selstart(evcol(e), evrow(e), snap); + } +} + +void +propnotify(XEvent *e) +{ + XPropertyEvent *xpev; + Atom clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + + xpev = &e->xproperty; + if (xpev->state == PropertyNewValue && + (xpev->atom == XA_PRIMARY || + xpev->atom == clipboard)) { + selnotify(e); + } +} + +void +selnotify(XEvent *e) +{ + ulong nitems, ofs, rem; + int format; + uchar *data, *last, *repl; + Atom type, incratom, property = None; + + incratom = XInternAtom(xw.dpy, "INCR", 0); + + ofs = 0; + if (e->type == SelectionNotify) + property = e->xselection.property; + else if (e->type == PropertyNotify) + property = e->xproperty.atom; + + if (property == None) + return; + + do { + if (XGetWindowProperty(xw.dpy, xw.win, property, ofs, + BUFSIZ/4, False, AnyPropertyType, + &type, &format, &nitems, &rem, + &data)) { + fprintf(stderr, "Clipboard allocation failed\n"); + return; + } + + if (e->type == PropertyNotify && nitems == 0 && rem == 0) { + /* + * If there is some PropertyNotify with no data, then + * this is the signal of the selection owner that all + * data has been transferred. We won't need to receive + * PropertyNotify events anymore. + */ + MODBIT(xw.attrs.event_mask, 0, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + } + + if (type == incratom) { + /* + * Activate the PropertyNotify events so we receive + * when the selection owner does send us the next + * chunk of data. + */ + MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + + /* + * Deleting the property is the transfer start signal. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); + continue; + } + + /* + * As seen in getsel: + * Line endings are inconsistent in the terminal and GUI world + * copy and pasting. When receiving some selection data, + * replace all '\n' with '\r'. + * FIXME: Fix the computer world. + */ + repl = data; + last = data + nitems * format / 8; + while ((repl = memchr(repl, '\n', last - repl))) { + *repl++ = '\r'; + } + + if (IS_SET(MODE_BRCKTPASTE) && ofs == 0) + ttywrite("\033[200~", 6, 0); + ttywrite((char *)data, nitems * format / 8, 1); + if (IS_SET(MODE_BRCKTPASTE) && rem == 0) + ttywrite("\033[201~", 6, 0); + XFree(data); + /* number of 32-bit chunks returned */ + ofs += nitems * format / 32; + } while (rem > 0); + + /* + * Deleting the property again tells the selection owner to send the + * next data chunk in the property. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); +} + +void +xclipcopy(void) +{ + clipcopy(NULL); +} + +void +selclear_(XEvent *e) +{ + selclear(); +} + +void +selrequest(XEvent *e) +{ + XSelectionRequestEvent *xsre; + XSelectionEvent xev; + Atom xa_targets, string, clipboard; + char *seltext; + + xsre = (XSelectionRequestEvent *) e; + xev.type = SelectionNotify; + xev.requestor = xsre->requestor; + xev.selection = xsre->selection; + xev.target = xsre->target; + xev.time = xsre->time; + if (xsre->property == None) + xsre->property = xsre->target; + + /* reject */ + xev.property = None; + + xa_targets = XInternAtom(xw.dpy, "TARGETS", 0); + if (xsre->target == xa_targets) { + /* respond with the supported type */ + string = xsel.xtarget; + XChangeProperty(xsre->display, xsre->requestor, xsre->property, + XA_ATOM, 32, PropModeReplace, + (uchar *) &string, 1); + xev.property = xsre->property; + } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) { + /* + * xith XA_STRING non ascii characters may be incorrect in the + * requestor. It is not our problem, use utf8. + */ + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + if (xsre->selection == XA_PRIMARY) { + seltext = xsel.primary; + } else if (xsre->selection == clipboard) { + seltext = xsel.clipboard; + } else { + fprintf(stderr, + "Unhandled clipboard selection 0x%lx\n", + xsre->selection); + return; + } + if (seltext != NULL) { + XChangeProperty(xsre->display, xsre->requestor, + xsre->property, xsre->target, + 8, PropModeReplace, + (uchar *)seltext, strlen(seltext)); + xev.property = xsre->property; + } + } + + /* all done, send a notification to the listener */ + if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *) &xev)) + fprintf(stderr, "Error sending SelectionNotify event\n"); +} + +void +setsel(char *str, Time t) +{ + if (!str) + return; + + free(xsel.primary); + xsel.primary = str; + + XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t); + if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win) + selclear(); +} + +void +xsetsel(char *str) +{ + setsel(str, CurrentTime); +} + +void +brelease(XEvent *e) +{ + int btn = e->xbutton.button; + + if (1 <= btn && btn <= 11) + buttons &= ~(1 << (btn-1)); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 1)) + return; + if (btn == Button1) + mousesel(e, 1); +} + +void +bmotion(XEvent *e) +{ + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + mousesel(e, 0); +} + +void +cresize(int width, int height) +{ + int col, row; + + if (width != 0) + win.w = width; + if (height != 0) + win.h = height; + + col = (win.w - 2 * borderpx) / win.cw; + row = (win.h - 2 * borderpx) / win.ch; + col = MAX(1, col); + row = MAX(1, row); + + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +} + +void +xresize(int col, int row) +{ + win.tw = col * win.cw; + win.th = row * win.ch; + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ + xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec)); +} + +ushort +sixd_to_16bit(int x) +{ + return x == 0 ? 0 : 0x3737 + 0x2828 * x; +} + +int +xloadcolor(int i, const char *name, Color *ncolor) +{ + XRenderColor color = { .alpha = 0xffff }; + + if (!name) { + if (BETWEEN(i, 16, 255)) { /* 256 color */ + if (i < 6*6*6+16) { /* same colors as xterm */ + color.red = sixd_to_16bit( ((i-16)/36)%6 ); + color.green = sixd_to_16bit( ((i-16)/6) %6 ); + color.blue = sixd_to_16bit( ((i-16)/1) %6 ); + } else { /* greyscale */ + color.red = 0x0808 + 0x0a0a * (i - (6*6*6+16)); + color.green = color.blue = color.red; + } + return XftColorAllocValue(xw.dpy, xw.vis, + xw.cmap, &color, ncolor); + } else + name = colorname[i]; + } + + return XftColorAllocName(xw.dpy, xw.vis, xw.cmap, name, ncolor); +} + +void +xloadcols(void) +{ + int i; + static int loaded; + Color *cp; + + if (loaded) { + for (cp = dc.col; cp < &dc.col[dc.collen]; ++cp) + XftColorFree(xw.dpy, xw.vis, xw.cmap, cp); + } else { + dc.collen = MAX(LEN(colorname), 256); + dc.col = xmalloc(dc.collen * sizeof(Color)); + } + + for (i = 0; i < dc.collen; i++) + if (!xloadcolor(i, NULL, &dc.col[i])) { + if (colorname[i]) + die("could not allocate color '%s'\n", colorname[i]); + else + die("could not allocate color %d\n", i); + } + loaded = 1; +} + +int +xgetcolor(int x, unsigned char *r, unsigned char *g, unsigned char *b) +{ + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + *r = dc.col[x].color.red >> 8; + *g = dc.col[x].color.green >> 8; + *b = dc.col[x].color.blue >> 8; + + return 0; +} + +int +xsetcolorname(int x, const char *name) +{ + Color ncolor; + + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + if (!xloadcolor(x, name, &ncolor)) + return 1; + + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + + return 0; +} + +/* + * Absolute coordinates. + */ +void +xclear(int x1, int y1, int x2, int y2) +{ + XftDrawRect(xw.draw, + &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg], + x1, y1, x2-x1, y2-y1); +} + +void +xhints(void) +{ + XClassHint class = {opt_name ? opt_name : termname, + opt_class ? opt_class : termname}; + XWMHints wm = {.flags = InputHint, .input = 1}; + XSizeHints *sizeh; + + sizeh = XAllocSizeHints(); + + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; + sizeh->height_inc = win.ch; + sizeh->width_inc = win.cw; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; + sizeh->min_width = win.cw + 2 * borderpx; + if (xw.isfixed) { + sizeh->flags |= PMaxSize; + sizeh->min_width = sizeh->max_width = win.w; + sizeh->min_height = sizeh->max_height = win.h; + } + if (xw.gm & (XValue|YValue)) { + sizeh->flags |= USPosition | PWinGravity; + sizeh->x = xw.l; + sizeh->y = xw.t; + sizeh->win_gravity = xgeommasktogravity(xw.gm); + } + + XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, + &class); + XFree(sizeh); +} + +int +xgeommasktogravity(int mask) +{ + switch (mask & (XNegative|YNegative)) { + case 0: + return NorthWestGravity; + case XNegative: + return NorthEastGravity; + case YNegative: + return SouthWestGravity; + } + + return SouthEastGravity; +} + +int +xloadfont(Font *f, FcPattern *pattern) +{ + FcPattern *configured; + FcPattern *match; + FcResult result; + XGlyphInfo extents; + int wantattr, haveattr; + + /* + * Manually configure instead of calling XftMatchFont + * so that we can use the configured pattern for + * "missing glyph" lookups. + */ + configured = FcPatternDuplicate(pattern); + if (!configured) + return 1; + + FcConfigSubstitute(NULL, configured, FcMatchPattern); + XftDefaultSubstitute(xw.dpy, xw.scr, configured); + + match = FcFontMatch(NULL, configured, &result); + if (!match) { + FcPatternDestroy(configured); + return 1; + } + + if (!(f->match = XftFontOpenPattern(xw.dpy, match))) { + FcPatternDestroy(configured); + FcPatternDestroy(match); + return 1; + } + + if ((XftPatternGetInteger(pattern, "slant", 0, &wantattr) == + XftResultMatch)) { + /* + * Check if xft was unable to find a font with the appropriate + * slant but gave us one anyway. Try to mitigate. + */ + if ((XftPatternGetInteger(f->match->pattern, "slant", 0, + &haveattr) != XftResultMatch) || haveattr < wantattr) { + f->badslant = 1; + fputs("font slant does not match\n", stderr); + } + } + + if ((XftPatternGetInteger(pattern, "weight", 0, &wantattr) == + XftResultMatch)) { + if ((XftPatternGetInteger(f->match->pattern, "weight", 0, + &haveattr) != XftResultMatch) || haveattr != wantattr) { + f->badweight = 1; + fputs("font weight does not match\n", stderr); + } + } + + XftTextExtentsUtf8(xw.dpy, f->match, + (const FcChar8 *) ascii_printable, + strlen(ascii_printable), &extents); + + f->set = NULL; + f->pattern = configured; + + f->ascent = f->match->ascent; + f->descent = f->match->descent; + f->lbearing = 0; + f->rbearing = f->match->max_advance_width; + + f->height = f->ascent + f->descent; + f->width = DIVCEIL(extents.xOff, strlen(ascii_printable)); + + return 0; +} + +void +xloadfonts(const char *fontstr, double fontsize) +{ + FcPattern *pattern; + double fontval; + + if (fontstr[0] == '-') + pattern = XftXlfdParse(fontstr, False, False); + else + pattern = FcNameParse((const FcChar8 *)fontstr); + + if (!pattern) + die("can't open font %s\n", fontstr); + + if (fontsize > 1) { + FcPatternDel(pattern, FC_PIXEL_SIZE); + FcPatternDel(pattern, FC_SIZE); + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, (double)fontsize); + usedfontsize = fontsize; + } else { + if (FcPatternGetDouble(pattern, FC_PIXEL_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = fontval; + } else if (FcPatternGetDouble(pattern, FC_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = -1; + } else { + /* + * Default font size is 12, if none given. This is to + * have a known usedfontsize value. + */ + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } + defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) + die("can't open font %s\n", fontstr); + + if (usedfontsize < 0) { + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; + if (fontsize == 0) + defaultfontsize = fontval; + } + + /* Setting character width and height. */ + win.cw = ceilf(dc.font.width * cwscale); + win.ch = ceilf(dc.font.height * chscale); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadfont(&dc.ifont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_WEIGHT); + FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadfont(&dc.ibfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadfont(&dc.bfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDestroy(pattern); +} + +void +xunloadfont(Font *f) +{ + XftFontClose(xw.dpy, f->match); + FcPatternDestroy(f->pattern); + if (f->set) + FcFontSetDestroy(f->set); +} + +void +xunloadfonts(void) +{ + /* Free the loaded fonts in the font cache. */ + while (frclen > 0) + XftFontClose(xw.dpy, frc[--frclen].font); + + xunloadfont(&dc.font); + xunloadfont(&dc.bfont); + xunloadfont(&dc.ifont); + xunloadfont(&dc.ibfont); +} + +int +ximopen(Display *dpy) +{ + XIMCallback imdestroy = { .client_data = NULL, .callback = ximdestroy }; + XICCallback icdestroy = { .client_data = NULL, .callback = xicdestroy }; + + xw.ime.xim = XOpenIM(xw.dpy, NULL, NULL, NULL); + if (xw.ime.xim == NULL) + return 0; + + if (XSetIMValues(xw.ime.xim, XNDestroyCallback, &imdestroy, NULL)) + fprintf(stderr, "XSetIMValues: " + "Could not set XNDestroyCallback.\n"); + + xw.ime.spotlist = XVaCreateNestedList(0, XNSpotLocation, &xw.ime.spot, + NULL); + + if (xw.ime.xic == NULL) { + xw.ime.xic = XCreateIC(xw.ime.xim, XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, xw.win, + XNDestroyCallback, &icdestroy, + NULL); + } + if (xw.ime.xic == NULL) + fprintf(stderr, "XCreateIC: Could not create input context.\n"); + + return 1; +} + +void +ximinstantiate(Display *dpy, XPointer client, XPointer call) +{ + if (ximopen(dpy)) + XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); +} + +void +ximdestroy(XIM xim, XPointer client, XPointer call) +{ + xw.ime.xim = NULL; + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + XFree(xw.ime.spotlist); +} + +int +xicdestroy(XIC xim, XPointer client, XPointer call) +{ + xw.ime.xic = NULL; + return 1; +} + +void +xinit(int cols, int rows) +{ + XGCValues gcvalues; + Cursor cursor; + Window parent, root; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; + + if (!(xw.dpy = XOpenDisplay(NULL))) + die("can't open display\n"); + xw.scr = XDefaultScreen(xw.dpy); + xw.vis = XDefaultVisual(xw.dpy, xw.scr); + + /* font */ + if (!FcInit()) + die("could not init fontconfig.\n"); + + usedfont = (opt_font == NULL)? font : opt_font; + xloadfonts(usedfont, 0); + + /* colors */ + xw.cmap = XDefaultColormap(xw.dpy, xw.scr); + xloadcols(); + + /* adjust fixed window geometry */ + win.w = 2 * borderpx + cols * win.cw; + win.h = 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) + xw.t += DisplayHeight(xw.dpy, xw.scr) - win.h - 2; + + /* Events */ + xw.attrs.background_pixel = dc.col[defaultbg].pixel; + xw.attrs.border_pixel = dc.col[defaultbg].pixel; + xw.attrs.bit_gravity = NorthWestGravity; + xw.attrs.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask + | ExposureMask | VisibilityChangeMask | StructureNotifyMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + + root = XRootWindow(xw.dpy, xw.scr); + if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) + parent = root; + xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, + win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, + xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &xw.attrs); + if (parent != root) + XReparentWindow(xw.dpy, xw.win, parent, xw.l, xw.t); + + memset(&gcvalues, 0, sizeof(gcvalues)); + gcvalues.graphics_exposures = False; + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + + /* font spec buffer */ + xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec)); + + /* Xft rendering context */ + xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap); + + /* input methods */ + if (!ximopen(xw.dpy)) { + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + } + + /* white cursor, black outline */ + cursor = XCreateFontCursor(xw.dpy, mouseshape); + XDefineCursor(xw.dpy, xw.win, cursor); + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousefg], &xmousefg) == 0) { + xmousefg.red = 0xffff; + xmousefg.green = 0xffff; + xmousefg.blue = 0xffff; + } + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousebg], &xmousebg) == 0) { + xmousebg.red = 0x0000; + xmousebg.green = 0x0000; + xmousebg.blue = 0x0000; + } + + XRecolorCursor(xw.dpy, cursor, &xmousefg, &xmousebg); + + xw.xembed = XInternAtom(xw.dpy, "_XEMBED", False); + xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); + xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); + xw.netwmiconname = XInternAtom(xw.dpy, "_NET_WM_ICON_NAME", False); + XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); + + xw.netwmpid = XInternAtom(xw.dpy, "_NET_WM_PID", False); + XChangeProperty(xw.dpy, xw.win, xw.netwmpid, XA_CARDINAL, 32, + PropModeReplace, (uchar *)&thispid, 1); + + win.mode = MODE_NUMLOCK; + resettitle(); + xhints(); + XMapWindow(xw.dpy, xw.win); + XSync(xw.dpy, False); + + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick1); + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick2); + xsel.primary = NULL; + xsel.clipboard = NULL; + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; +} + +int +xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) +{ + float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; + float runewidth = win.cw; + Rune rune; + FT_UInt glyphidx; + FcResult fcres; + FcPattern *fcpattern, *fontpattern; + FcFontSet *fcsets[] = { NULL }; + FcCharSet *fccharset; + int i, f, numspecs = 0; + + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + /* Fetch rune and mode for current glyph. */ + rune = glyphs[i].u; + mode = glyphs[i].mode; + + /* Skip dummy wide-character spacing. */ + if (mode == ATTR_WDUMMY) + continue; + + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; + font = &dc.font; + frcflags = FRC_NORMAL; + runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f); + if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) { + font = &dc.ibfont; + frcflags = FRC_ITALICBOLD; + } else if (mode & ATTR_ITALIC) { + font = &dc.ifont; + frcflags = FRC_ITALIC; + } else if (mode & ATTR_BOLD) { + font = &dc.bfont; + frcflags = FRC_BOLD; + } + yp = winy + font->ascent; + } + + /* Lookup character index with default font. */ + glyphidx = XftCharIndex(xw.dpy, font->match, rune); + if (glyphidx) { + specs[numspecs].font = font->match; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + continue; + } + + /* Fallback on font cache, search the font cache for match. */ + for (f = 0; f < frclen; f++) { + glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); + /* Everything correct. */ + if (glyphidx && frc[f].flags == frcflags) + break; + /* We got a default font for a not found glyph. */ + if (!glyphidx && frc[f].flags == frcflags + && frc[f].unicodep == rune) { + break; + } + } + + /* Nothing was found. Use fontconfig to find matching font. */ + if (f >= frclen) { + if (!font->set) + font->set = FcFontSort(0, font->pattern, + 1, 0, &fcres); + fcsets[0] = font->set; + + /* + * Nothing was found in the cache. Now use + * some dozen of Fontconfig calls to get the + * font for one single character. + * + * Xft and fontconfig are design failures. + */ + fcpattern = FcPatternDuplicate(font->pattern); + fccharset = FcCharSetCreate(); + + FcCharSetAddChar(fccharset, rune); + FcPatternAddCharSet(fcpattern, FC_CHARSET, + fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, 1); + + FcConfigSubstitute(0, fcpattern, + FcMatchPattern); + FcDefaultSubstitute(fcpattern); + + fontpattern = FcFontSetMatch(0, fcsets, 1, + fcpattern, &fcres); + + /* Allocate memory for the new cache entry. */ + if (frclen >= frccap) { + frccap += 16; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + frc[frclen].font = XftFontOpenPattern(xw.dpy, + fontpattern); + if (!frc[frclen].font) + die("XftFontOpenPattern failed seeking fallback font: %s\n", + strerror(errno)); + frc[frclen].flags = frcflags; + frc[frclen].unicodep = rune; + + glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + + f = frclen; + frclen++; + + FcPatternDestroy(fcpattern); + FcCharSetDestroy(fccharset); + } + + specs[numspecs].font = frc[f].font; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + } + + return numspecs; +} + +void +xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) +{ + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); + int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; + XRectangle r; + + /* Fallback on color display for attributes not supported by the font */ + if (base.mode & ATTR_ITALIC && base.mode & ATTR_BOLD) { + if (dc.ibfont.badslant || dc.ibfont.badweight) + base.fg = defaultattr; + } else if ((base.mode & ATTR_ITALIC && dc.ifont.badslant) || + (base.mode & ATTR_BOLD && dc.bfont.badweight)) { + base.fg = defaultattr; + } + + if (IS_TRUECOL(base.fg)) { + colfg.alpha = 0xffff; + colfg.red = TRUERED(base.fg); + colfg.green = TRUEGREEN(base.fg); + colfg.blue = TRUEBLUE(base.fg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &truefg); + fg = &truefg; + } else { + fg = &dc.col[base.fg]; + } + + if (IS_TRUECOL(base.bg)) { + colbg.alpha = 0xffff; + colbg.green = TRUEGREEN(base.bg); + colbg.red = TRUERED(base.bg); + colbg.blue = TRUEBLUE(base.bg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, &truebg); + bg = &truebg; + } else { + bg = &dc.col[base.bg]; + } + + /* Change basic system colors [0-7] to bright system colors [8-15] */ + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_BOLD && BETWEEN(base.fg, 0, 7)) + fg = &dc.col[base.fg + 8]; + + if (IS_SET(MODE_REVERSE)) { + if (fg == &dc.col[defaultfg]) { + fg = &dc.col[defaultbg]; + } else { + colfg.red = ~fg->color.red; + colfg.green = ~fg->color.green; + colfg.blue = ~fg->color.blue; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, + &revfg); + fg = &revfg; + } + + if (bg == &dc.col[defaultbg]) { + bg = &dc.col[defaultfg]; + } else { + colbg.red = ~bg->color.red; + colbg.green = ~bg->color.green; + colbg.blue = ~bg->color.blue; + colbg.alpha = bg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, + &revbg); + bg = &revbg; + } + } + + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_FAINT) { + colfg.red = fg->color.red / 2; + colfg.green = fg->color.green / 2; + colfg.blue = fg->color.blue / 2; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &revfg); + fg = &revfg; + } + + if (base.mode & ATTR_REVERSE) { + temp = fg; + fg = bg; + bg = temp; + } + + if (base.mode & ATTR_BLINK && win.mode & MODE_BLINK) + fg = bg; + + if (base.mode & ATTR_INVISIBLE) + fg = bg; + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { + xclear(0, (y == 0)? 0 : winy, borderpx, + winy + win.ch + + ((winy + win.ch >= borderpx + win.th)? win.h : 0)); + } + if (winx + width >= borderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, + ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) + xclear(winx, 0, winx + width, borderpx); + if (winy + win.ch >= borderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ + XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + + /* Set the clip region because Xft is sometimes dirty. */ + r.x = 0; + r.y = 0; + r.height = win.ch; + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + + /* Render the glyphs. */ + XftDrawGlyphFontSpec(xw.draw, fg, specs, len); + + /* Render underline and strikethrough. */ + if (base.mode & ATTR_UNDERLINE) { + XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, + width, 1); + } + + if (base.mode & ATTR_STRUCK) { + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, + width, 1); + } + + /* Reset clip to none. */ + XftDrawSetClip(xw.draw, 0); +} + +void +xdrawglyph(Glyph g, int x, int y) +{ + int numspecs; + XftGlyphFontSpec spec; + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); +} + +void +xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) +{ + Color drawcol; + + /* remove the old cursor */ + if (selected(ox, oy)) + og.mode ^= ATTR_REVERSE; + xdrawglyph(og, ox, oy); + + if (IS_SET(MODE_HIDE)) + return; + + /* + * Select the right color for the right mode. + */ + g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE; + + if (IS_SET(MODE_REVERSE)) { + g.mode |= ATTR_REVERSE; + g.bg = defaultfg; + if (selected(cx, cy)) { + drawcol = dc.col[defaultcs]; + g.fg = defaultrcs; + } else { + drawcol = dc.col[defaultrcs]; + g.fg = defaultcs; + } + } else { + if (selected(cx, cy)) { + g.fg = defaultfg; + g.bg = defaultrcs; + } else { + g.fg = defaultbg; + g.bg = defaultcs; + } + drawcol = dc.col[g.bg]; + } + + /* draw the new one */ + if (IS_SET(MODE_FOCUSED)) { + switch (win.cursor) { + case 7: /* st extension */ + g.u = 0x2603; /* snowman (U+2603) */ + /* FALLTHROUGH */ + case 0: /* Blinking Block */ + case 1: /* Blinking Block (Default) */ + case 2: /* Steady Block */ + xdrawglyph(g, cx, cy); + break; + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + (cx + 1) * win.cw - 1, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } +} + +void +xsetenv(void) +{ + char buf[sizeof(long) * 8 + 1]; + + snprintf(buf, sizeof(buf), "%lu", xw.win); + setenv("WINDOWID", buf, 1); +} + +void +xseticontitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMIconName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmiconname); + XFree(prop.value); +} + +void +xsettitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); + XFree(prop.value); +} + +int +xstartdraw(void) +{ + return IS_SET(MODE_VISIBLE); +} + +void +xdrawline(Line line, int x1, int y1, int x2) +{ + int i, x, ox, numspecs; + Glyph base, new; + XftGlyphFontSpec *specs = xw.specbuf; + + numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1); + i = ox = 0; + for (x = x1; x < x2 && i < numspecs; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); + specs += i; + numspecs -= i; + i = 0; + } + if (i == 0) { + ox = x; + base = new; + } + i++; + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); +} + +void +xfinishdraw(void) +{ + XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, + win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); +} + +void +xximspot(int x, int y) +{ + if (xw.ime.xic == NULL) + return; + + xw.ime.spot.x = borderpx + x * win.cw; + xw.ime.spot.y = borderpx + (y + 1) * win.ch; + + XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL); +} + +void +expose(XEvent *ev) +{ + redraw(); +} + +void +visibility(XEvent *ev) +{ + XVisibilityEvent *e = &ev->xvisibility; + + MODBIT(win.mode, e->state != VisibilityFullyObscured, MODE_VISIBLE); +} + +void +unmap(XEvent *ev) +{ + win.mode &= ~MODE_VISIBLE; +} + +void +xsetpointermotion(int set) +{ + MODBIT(xw.attrs.event_mask, set, PointerMotionMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs); +} + +void +xsetmode(int set, unsigned int flags) +{ + int mode = win.mode; + MODBIT(win.mode, set, flags); + if ((win.mode & MODE_REVERSE) != (mode & MODE_REVERSE)) + redraw(); +} + +int +xsetcursor(int cursor) +{ + if (!BETWEEN(cursor, 0, 7)) /* 7: st extension */ + return 1; + win.cursor = cursor; + return 0; +} + +void +xseturgency(int add) +{ + XWMHints *h = XGetWMHints(xw.dpy, xw.win); + + MODBIT(h->flags, add, XUrgencyHint); + XSetWMHints(xw.dpy, xw.win, h); + XFree(h); +} + +void +xbell(void) +{ + if (!(IS_SET(MODE_FOCUSED))) + xseturgency(1); + if (bellvolume) + XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL); +} + +void +focus(XEvent *ev) +{ + XFocusChangeEvent *e = &ev->xfocus; + + if (e->mode == NotifyGrab) + return; + + if (ev->type == FocusIn) { + if (xw.ime.xic) + XSetICFocus(xw.ime.xic); + win.mode |= MODE_FOCUSED; + xseturgency(0); + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[I", 3, 0); + } else { + if (xw.ime.xic) + XUnsetICFocus(xw.ime.xic); + win.mode &= ~MODE_FOCUSED; + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[O", 3, 0); + } +} + +int +match(uint mask, uint state) +{ + return mask == XK_ANY_MOD || mask == (state & ~ignoremod); +} + +char* +kmap(KeySym k, uint state) +{ + Key *kp; + int i; + + /* Check for mapped keys out of X11 function keys. */ + for (i = 0; i < LEN(mappedkeys); i++) { + if (mappedkeys[i] == k) + break; + } + if (i == LEN(mappedkeys)) { + if ((k & 0xFFFF) < 0xFD00) + return NULL; + } + + for (kp = key; kp < key + LEN(key); kp++) { + if (kp->k != k) + continue; + + if (!match(kp->mask, state)) + continue; + + if (IS_SET(MODE_APPKEYPAD) ? kp->appkey < 0 : kp->appkey > 0) + continue; + if (IS_SET(MODE_NUMLOCK) && kp->appkey == 2) + continue; + + if (IS_SET(MODE_APPCURSOR) ? kp->appcursor < 0 : kp->appcursor > 0) + continue; + + return kp->s; + } + + return NULL; +} + +void +kpress(XEvent *ev) +{ + XKeyEvent *e = &ev->xkey; + KeySym ksym = NoSymbol; + char buf[64], *customkey; + int len; + Rune c; + Status status; + Shortcut *bp; + + if (IS_SET(MODE_KBDLOCK)) + return; + + if (xw.ime.xic) { + len = XmbLookupString(xw.ime.xic, e, buf, sizeof buf, &ksym, &status); + if (status == XBufferOverflow) + return; + } else { + len = XLookupString(e, buf, sizeof buf, &ksym, NULL); + } + /* 1. shortcuts */ + for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { + if (ksym == bp->keysym && match(bp->mod, e->state)) { + bp->func(&(bp->arg)); + return; + } + } + + /* 2. custom keys from config.h */ + if ((customkey = kmap(ksym, e->state))) { + ttywrite(customkey, strlen(customkey), 1); + return; + } + + /* 3. composed string from input method */ + if (len == 0) + return; + if (len == 1 && e->state & Mod1Mask) { + if (IS_SET(MODE_8BIT)) { + if (*buf < 0177) { + c = *buf | 0x80; + len = utf8encode(c, buf); + } + } else { + buf[1] = buf[0]; + buf[0] = '\033'; + len = 2; + } + } + ttywrite(buf, len, 1); +} + +void +cmessage(XEvent *e) +{ + /* + * See xembed specs + * http://standards.freedesktop.org/xembed-spec/xembed-spec-latest.html + */ + if (e->xclient.message_type == xw.xembed && e->xclient.format == 32) { + if (e->xclient.data.l[1] == XEMBED_FOCUS_IN) { + win.mode |= MODE_FOCUSED; + xseturgency(0); + } else if (e->xclient.data.l[1] == XEMBED_FOCUS_OUT) { + win.mode &= ~MODE_FOCUSED; + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); + exit(0); + } +} + +void +resize(XEvent *e) +{ + if (e->xconfigure.width == win.w && e->xconfigure.height == win.h) + return; + + cresize(e->xconfigure.width, e->xconfigure.height); +} + +void +run(void) +{ + XEvent ev; + int w = win.w, h = win.h; + fd_set rfd; + int xfd = XConnectionNumber(xw.dpy), ttyfd, xev, drawing; + struct timespec seltv, *tv, now, lastblink, trigger; + double timeout; + + /* Waiting for window mapping */ + do { + XNextEvent(xw.dpy, &ev); + /* + * This XFilterEvent call is required because of XOpenIM. It + * does filter out the key event and some client message for + * the input method too. + */ + if (XFilterEvent(&ev, None)) + continue; + if (ev.type == ConfigureNotify) { + w = ev.xconfigure.width; + h = ev.xconfigure.height; + } + } while (ev.type != MapNotify); + + ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd); + cresize(w, h); + + for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) { + FD_ZERO(&rfd); + FD_SET(ttyfd, &rfd); + FD_SET(xfd, &rfd); + + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; + + if (pselect(MAX(xfd, ttyfd)+1, &rfd, NULL, NULL, tv, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + clock_gettime(CLOCK_MONOTONIC, &now); + + if (FD_ISSET(ttyfd, &rfd)) + ttyread(); + + xev = 0; + while (XPending(xw.dpy)) { + xev = 1; + XNextEvent(xw.dpy, &ev); + if (XFilterEvent(&ev, None)) + continue; + if (handler[ev.type]) + (handler[ev.type])(&ev); + } + + /* + * To reduce flicker and tearing, when new content or event + * triggers drawing, we first wait a bit to ensure we got + * everything, and if nothing new arrives - we draw. + * We start with trying to wait minlatency ms. If more content + * arrives sooner, we retry with shorter and shorter periods, + * and eventually draw even without idle after maxlatency ms. + * Typically this results in low latency while interacting, + * maximum latency intervals during `cat huge.txt`, and perfect + * sync with periodic updates from animations/key-repeats/etc. + */ + if (FD_ISSET(ttyfd, &rfd) || xev) { + if (!drawing) { + trigger = now; + drawing = 1; + } + timeout = (maxlatency - TIMEDIFF(now, trigger)) \ + / maxlatency * minlatency; + if (timeout > 0) + continue; /* we have time, try to find idle */ + } + + /* idle detected or maxlatency exhausted -> draw */ + timeout = -1; + if (blinktimeout && tattrset(ATTR_BLINK)) { + timeout = blinktimeout - TIMEDIFF(now, lastblink); + if (timeout <= 0) { + if (-timeout > blinktimeout) /* start visible */ + win.mode |= MODE_BLINK; + win.mode ^= MODE_BLINK; + tsetdirtattr(ATTR_BLINK); + lastblink = now; + timeout = blinktimeout; + } + } + + draw(); + XFlush(xw.dpy); + drawing = 0; + } +} + +void +usage(void) +{ + die("usage: %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid]" + " [[-e] command [args ...]]\n" + " %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid] -l line" + " [stty_args ...]\n", argv0, argv0); +} + +int +main(int argc, char *argv[]) +{ + xw.l = xw.t = 0; + xw.isfixed = False; + xsetcursor(cursorshape); + + ARGBEGIN { + case 'a': + allowaltscreen = 0; + break; + case 'c': + opt_class = EARGF(usage()); + break; + case 'e': + if (argc > 0) + --argc, ++argv; + goto run; + case 'f': + opt_font = EARGF(usage()); + break; + case 'g': + xw.gm = XParseGeometry(EARGF(usage()), + &xw.l, &xw.t, &cols, &rows); + break; + case 'i': + xw.isfixed = 1; + break; + case 'o': + opt_io = EARGF(usage()); + break; + case 'l': + opt_line = EARGF(usage()); + break; + case 'n': + opt_name = EARGF(usage()); + break; + case 't': + case 'T': + opt_title = EARGF(usage()); + break; + case 'w': + opt_embed = EARGF(usage()); + break; + case 'v': + die("%s " VERSION "\n", argv0); + break; + default: + usage(); + } ARGEND; + +run: + if (argc > 0) /* eat all remaining arguments */ + opt_cmd = argv; + + if (!opt_title) + opt_title = (opt_line || !opt_cmd) ? "st" : opt_cmd[0]; + + setlocale(LC_CTYPE, ""); + XSetLocaleModifiers(""); + cols = MAX(cols, 1); + rows = MAX(rows, 1); + tnew(cols, rows); + xinit(cols, rows); + xsetenv(); + selinit(); + run(); + + return 0; +} From 307fba1e17d43f6b9197843a74c02a3908e63049 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 08/11] Squashed 'suckless/dwm/' content from commit 44dbc68 git-subtree-dir: suckless/dwm git-subtree-split: 44dbc6809d05b8f2addc483f882e670db0b6b8e9 --- LICENSE | 38 + Makefile | 45 ++ README | 48 ++ config.def.h | 117 +++ config.mk | 39 + drw.c | 471 +++++++++++ drw.h | 60 ++ dwm.1 | 176 ++++ dwm.c | 2165 ++++++++++++++++++++++++++++++++++++++++++++++++++ dwm.png | Bin 0 -> 373 bytes transient.c | 42 + util.c | 37 + util.h | 9 + 13 files changed, 3247 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 drw.c create mode 100644 drw.h create mode 100644 dwm.1 create mode 100644 dwm.c create mode 100644 dwm.png create mode 100644 transient.c create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..596e6cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,38 @@ +MIT/X Consortium License + +© 2010-2026 Hiltjo Posthuma +© 2006-2019 Anselm R Garbe +© 2006-2009 Jukka Salmi +© 2006-2007 Sander van Dijk +© 2007-2011 Peter Hartlich +© 2007-2009 Szabolcs Nagy +© 2007-2009 Christof Musik +© 2007-2009 Premysl Hruby +© 2007-2008 Enno Gottox Boland +© 2008 Martin Hurton +© 2008 Neale Pickett +© 2009 Mate Nagy +© 2010-2012 Connor Lane Smith +© 2011 Christoph Lohmann <20h@r-36.net> +© 2015-2016 Quentin Rameau +© 2015-2016 Eric Pruitt +© 2016-2017 Markus Teich +© 2020-2022 Chris Down + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ffa69b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +# dwm - dynamic window manager +# See LICENSE file for copyright and license details. + +include config.mk + +SRC = drw.c dwm.c util.c +OBJ = ${SRC:.c=.o} + +all: dwm + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.h config.mk + +config.h: + cp config.def.h $@ + +dwm: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + rm -f dwm ${OBJ} dwm-${VERSION}.tar.gz + +dist: clean + mkdir -p dwm-${VERSION} + cp -R LICENSE Makefile README config.def.h config.mk\ + dwm.1 drw.h util.h ${SRC} dwm.png transient.c dwm-${VERSION} + tar -cf dwm-${VERSION}.tar dwm-${VERSION} + gzip dwm-${VERSION}.tar + rm -rf dwm-${VERSION} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f dwm ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/dwm + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + sed "s/VERSION/${VERSION}/g" < dwm.1 > ${DESTDIR}${MANPREFIX}/man1/dwm.1 + chmod 644 ${DESTDIR}${MANPREFIX}/man1/dwm.1 + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/dwm\ + ${DESTDIR}${MANPREFIX}/man1/dwm.1 + +.PHONY: all clean dist install uninstall diff --git a/README b/README new file mode 100644 index 0000000..95d4fd0 --- /dev/null +++ b/README @@ -0,0 +1,48 @@ +dwm - dynamic window manager +============================ +dwm is an extremely fast, small, and dynamic window manager for X. + + +Requirements +------------ +In order to build dwm you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (dwm is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install dwm (if +necessary as root): + + make clean install + + +Running dwm +----------- +Add the following line to your .xinitrc to start dwm using startx: + + exec dwm + +In order to connect dwm to a specific display, make sure that +the DISPLAY environment variable is set correctly, e.g.: + + DISPLAY=foo.bar:1 exec dwm + +(This will start dwm on display :1 of the host foo.bar.) + +In order to display status info in the bar, you can do something +like this in your .xinitrc: + + while xsetroot -name "`date` `uptime | sed 's/.*,//'`" + do + sleep 1 + done & + exec dwm + + +Configuration +------------- +The configuration of dwm is done by creating a custom config.h +and (re)compiling the source code. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..81c3fc0 --- /dev/null +++ b/config.def.h @@ -0,0 +1,117 @@ +/* See LICENSE file for copyright and license details. */ + +/* appearance */ +static const unsigned int borderpx = 1; /* border pixel of windows */ +static const unsigned int snap = 32; /* snap pixel */ +static const int showbar = 1; /* 0 means no bar */ +static const int topbar = 1; /* 0 means bottom bar */ +static const char *fonts[] = { "monospace:size=10" }; +static const char dmenufont[] = "monospace:size=10"; +static const char col_gray1[] = "#222222"; +static const char col_gray2[] = "#444444"; +static const char col_gray3[] = "#bbbbbb"; +static const char col_gray4[] = "#eeeeee"; +static const char col_cyan[] = "#005577"; +static const char *colors[][3] = { + /* fg bg border */ + [SchemeNorm] = { col_gray3, col_gray1, col_gray2 }, + [SchemeSel] = { col_gray4, col_cyan, col_cyan }, +}; + +/* tagging */ +static const char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + +static const Rule rules[] = { + /* xprop(1): + * WM_CLASS(STRING) = instance, class + * WM_NAME(STRING) = title + */ + /* class instance title tags mask isfloating monitor */ + { "Gimp", NULL, NULL, 0, 1, -1 }, + { "Firefox", NULL, NULL, 1 << 8, 0, -1 }, +}; + +/* layout(s) */ +static const float mfact = 0.55; /* factor of master area size [0.05..0.95] */ +static const int nmaster = 1; /* number of clients in master area */ +static const int resizehints = 1; /* 1 means respect size hints in tiled resizals */ +static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */ +static const int refreshrate = 120; /* refresh rate (per second) for client move/resize */ + +static const Layout layouts[] = { + /* symbol arrange function */ + { "[]=", tile }, /* first entry is default */ + { "><>", NULL }, /* no layout function means floating behavior */ + { "[M]", monocle }, +}; + +/* key definitions */ +#define MODKEY Mod1Mask +#define TAGKEYS(KEY,TAG) \ + { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \ + { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, + +/* helper for spawning shell commands in the pre dwm-5.0 fashion */ +#define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } + +/* commands */ +static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */ +static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_gray1, "-nf", col_gray3, "-sb", col_cyan, "-sf", col_gray4, NULL }; +static const char *termcmd[] = { "st", NULL }; + +static const Key keys[] = { + /* modifier key function argument */ + { MODKEY, XK_p, spawn, {.v = dmenucmd } }, + { MODKEY|ShiftMask, XK_Return, spawn, {.v = termcmd } }, + { MODKEY, XK_b, togglebar, {0} }, + { MODKEY, XK_j, focusstack, {.i = +1 } }, + { MODKEY, XK_k, focusstack, {.i = -1 } }, + { MODKEY, XK_i, incnmaster, {.i = +1 } }, + { MODKEY, XK_d, incnmaster, {.i = -1 } }, + { MODKEY, XK_h, setmfact, {.f = -0.05} }, + { MODKEY, XK_l, setmfact, {.f = +0.05} }, + { MODKEY, XK_Return, zoom, {0} }, + { MODKEY, XK_Tab, view, {0} }, + { MODKEY|ShiftMask, XK_c, killclient, {0} }, + { MODKEY, XK_t, setlayout, {.v = &layouts[0]} }, + { MODKEY, XK_f, setlayout, {.v = &layouts[1]} }, + { MODKEY, XK_m, setlayout, {.v = &layouts[2]} }, + { MODKEY, XK_space, setlayout, {0} }, + { MODKEY|ShiftMask, XK_space, togglefloating, {0} }, + { MODKEY, XK_0, view, {.ui = ~0 } }, + { MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } }, + { MODKEY, XK_comma, focusmon, {.i = -1 } }, + { MODKEY, XK_period, focusmon, {.i = +1 } }, + { MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } }, + { MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } }, + TAGKEYS( XK_1, 0) + TAGKEYS( XK_2, 1) + TAGKEYS( XK_3, 2) + TAGKEYS( XK_4, 3) + TAGKEYS( XK_5, 4) + TAGKEYS( XK_6, 5) + TAGKEYS( XK_7, 6) + TAGKEYS( XK_8, 7) + TAGKEYS( XK_9, 8) + { MODKEY|ShiftMask, XK_q, quit, {0} }, +}; + +/* button definitions */ +/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */ +static const Button buttons[] = { + /* click event mask button function argument */ + { ClkLtSymbol, 0, Button1, setlayout, {0} }, + { ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} }, + { ClkWinTitle, 0, Button2, zoom, {0} }, + { ClkStatusText, 0, Button2, spawn, {.v = termcmd } }, + { ClkClientWin, MODKEY, Button1, movemouse, {0} }, + { ClkClientWin, MODKEY, Button2, togglefloating, {0} }, + { ClkClientWin, MODKEY, Button3, resizemouse, {0} }, + { ClkTagBar, 0, Button1, view, {0} }, + { ClkTagBar, 0, Button3, toggleview, {0} }, + { ClkTagBar, MODKEY, Button1, tag, {0} }, + { ClkTagBar, MODKEY, Button3, toggletag, {0} }, +}; + diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..982dc21 --- /dev/null +++ b/config.mk @@ -0,0 +1,39 @@ +# dwm version +VERSION = 6.8 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# Xinerama, comment if you don't want it +XINERAMALIBS = -lXinerama +XINERAMAFLAGS = -DXINERAMA + +# freetype +FREETYPELIBS = -lfontconfig -lXft +FREETYPEINC = /usr/include/freetype2 +# OpenBSD (uncomment) +#FREETYPEINC = ${X11INC}/freetype2 +#MANPREFIX = ${PREFIX}/man + +# includes and libs +INCS = -I${X11INC} -I${FREETYPEINC} +LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} +#CFLAGS = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS} +CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS} +LDFLAGS = ${LIBS} + +# Solaris +#CFLAGS = -fast ${INCS} -DVERSION=\"${VERSION}\" +#LDFLAGS = ${LIBS} + +# compiler and linker +CC = cc diff --git a/drw.c b/drw.c new file mode 100644 index 0000000..9fdd1a4 --- /dev/null +++ b/drw.c @@ -0,0 +1,471 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "drw.h" +#include "util.h" + +#define UTF_INVALID 0xFFFD + +static int +utf8decode(const char *s_in, long *u, int *err) +{ + static const unsigned char lens[] = { + /* 0XXXX */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 10XXX */ 0, 0, 0, 0, 0, 0, 0, 0, /* invalid */ + /* 110XX */ 2, 2, 2, 2, + /* 1110X */ 3, 3, + /* 11110 */ 4, + /* 11111 */ 0, /* invalid */ + }; + static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 }; + static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 }; + + const unsigned char *s = (const unsigned char *)s_in; + int len = lens[*s >> 3]; + *u = UTF_INVALID; + *err = 1; + if (len == 0) + return 1; + + long cp = s[0] & leading_mask[len - 1]; + for (int i = 1; i < len; ++i) { + if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) + return i; + cp = (cp << 6) | (s[i] & 0x3F); + } + /* out of range, surrogate, overlong encoding */ + if (cp > 0x10FFFF || (cp >> 11) == 0x1B || cp < overlong[len - 1]) + return len; + + *err = 0; + *u = cp; + return len; +} + +Drw * +drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h) +{ + Drw *drw = ecalloc(1, sizeof(Drw)); + + drw->dpy = dpy; + drw->screen = screen; + drw->root = root; + drw->w = w; + drw->h = h; + drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen)); + drw->gc = XCreateGC(dpy, root, 0, NULL); + XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); + + return drw; +} + +void +drw_resize(Drw *drw, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + drw->w = w; + drw->h = h; + if (drw->drawable) + XFreePixmap(drw->dpy, drw->drawable); + drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen)); +} + +void +drw_free(Drw *drw) +{ + XFreePixmap(drw->dpy, drw->drawable); + XFreeGC(drw->dpy, drw->gc); + drw_fontset_free(drw->fonts); + free(drw); +} + +/* This function is an implementation detail. Library users should use + * drw_fontset_create instead. + */ +static Fnt * +xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern) +{ + Fnt *font; + XftFont *xfont = NULL; + FcPattern *pattern = NULL; + + if (fontname) { + /* Using the pattern found at font->xfont->pattern does not yield the + * same substitution results as using the pattern returned by + * FcNameParse; using the latter results in the desired fallback + * behaviour whereas the former just results in missing-character + * rectangles being drawn, at least with some fonts. */ + if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) { + fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname); + return NULL; + } + if (!(pattern = FcNameParse((FcChar8 *) fontname))) { + fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname); + XftFontClose(drw->dpy, xfont); + return NULL; + } + } else if (fontpattern) { + if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) { + fprintf(stderr, "error, cannot load font from pattern.\n"); + return NULL; + } + } else { + die("no font specified."); + } + + font = ecalloc(1, sizeof(Fnt)); + font->xfont = xfont; + font->pattern = pattern; + font->h = xfont->ascent + xfont->descent; + font->dpy = drw->dpy; + + return font; +} + +static void +xfont_free(Fnt *font) +{ + if (!font) + return; + if (font->pattern) + FcPatternDestroy(font->pattern); + XftFontClose(font->dpy, font->xfont); + free(font); +} + +Fnt* +drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount) +{ + Fnt *cur, *ret = NULL; + size_t i; + + if (!drw || !fonts) + return NULL; + + for (i = 1; i <= fontcount; i++) { + if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) { + cur->next = ret; + ret = cur; + } + } + return (drw->fonts = ret); +} + +void +drw_fontset_free(Fnt *font) +{ + if (font) { + drw_fontset_free(font->next); + xfont_free(font); + } +} + +void +drw_clr_create(Drw *drw, Clr *dest, const char *clrname) +{ + if (!drw || !dest || !clrname) + return; + + if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), + clrname, dest)) + die("error, cannot allocate color '%s'", clrname); +} + +/* Create color schemes. */ +Clr * +drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount) +{ + size_t i; + Clr *ret; + + /* need at least two colors for a scheme */ + if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(Clr)))) + return NULL; + + for (i = 0; i < clrcount; i++) + drw_clr_create(drw, &ret[i], clrnames[i]); + return ret; +} + +void +drw_clr_free(Drw *drw, Clr *c) +{ + if (!drw || !c) + return; + + /* c is typedef XftColor Clr */ + XftColorFree(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), c); +} + +void +drw_scm_free(Drw *drw, Clr *scm, size_t clrcount) +{ + size_t i; + + if (!drw || !scm) + return; + + for (i = 0; i < clrcount; i++) + drw_clr_free(drw, &scm[i]); + free(scm); +} + +void +drw_setfontset(Drw *drw, Fnt *set) +{ + if (drw) + drw->fonts = set; +} + +void +drw_setscheme(Drw *drw, Clr *scm) +{ + if (drw) + drw->scheme = scm; +} + +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + if (!drw || !drw->scheme) + return; + XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel); + if (filled) + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + else + XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1); +} + +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +{ + int ty, ellipsis_x = 0; + unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1; + XftDraw *d = NULL; + Fnt *usedfont, *curfont, *nextfont; + int utf8strlen, utf8charlen, utf8err, render = x || y || w || h; + long utf8codepoint = 0; + const char *utf8str; + FcCharSet *fccharset; + FcPattern *fcpattern; + FcPattern *match; + XftResult result; + int charexists = 0, overflow = 0; + /* keep track of a couple codepoints for which we have no match. */ + static unsigned int nomatches[128], ellipsis_width, invalid_width; + static const char invalid[] = "�"; + + if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) + return 0; + + if (!render) { + w = invert ? invert : ~invert; + } else { + XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + if (w < lpad) + return x + w; + d = XftDrawCreate(drw->dpy, drw->drawable, + DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen)); + x += lpad; + w -= lpad; + } + + usedfont = drw->fonts; + if (!ellipsis_width && render) + ellipsis_width = drw_fontset_getwidth(drw, "..."); + if (!invalid_width && render) + invalid_width = drw_fontset_getwidth(drw, invalid); + while (1) { + ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; + utf8str = text; + nextfont = NULL; + while (*text) { + utf8charlen = utf8decode(text, &utf8codepoint, &utf8err); + for (curfont = drw->fonts; curfont; curfont = curfont->next) { + charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); + if (charexists) { + drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL); + if (ew + ellipsis_width <= w) { + /* keep track where the ellipsis still fits */ + ellipsis_x = x + ew; + ellipsis_w = w - ew; + ellipsis_len = utf8strlen; + } + + if (ew + tmpw > w) { + overflow = 1; + /* called from drw_fontset_getwidth_clamp(): + * it wants the width AFTER the overflow + */ + if (!render) + x += tmpw; + else + utf8strlen = ellipsis_len; + } else if (curfont == usedfont) { + text += utf8charlen; + utf8strlen += utf8err ? 0 : utf8charlen; + ew += utf8err ? 0 : tmpw; + } else { + nextfont = curfont; + } + break; + } + } + + if (overflow || !charexists || nextfont || utf8err) + break; + else + charexists = 0; + } + + if (utf8strlen) { + if (render) { + ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent; + XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg], + usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen); + } + x += ew; + w -= ew; + } + if (utf8err && (!render || invalid_width < w)) { + if (render) + drw_text(drw, x, y, w, h, 0, invalid, invert); + x += invalid_width; + w -= invalid_width; + } + if (render && overflow) + drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert); + + if (!*text || overflow) { + break; + } else if (nextfont) { + charexists = 0; + usedfont = nextfont; + } else { + /* Regardless of whether or not a fallback font is found, the + * character must be drawn. */ + charexists = 1; + + hash = (unsigned int)utf8codepoint; + hash = ((hash >> 16) ^ hash) * 0x21F0AAAD; + hash = ((hash >> 15) ^ hash) * 0xD35A2D97; + h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches); + h1 = (hash >> 17) % LENGTH(nomatches); + /* avoid expensive XftFontMatch call when we know we won't find a match */ + if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint) + goto no_match; + + fccharset = FcCharSetCreate(); + FcCharSetAddChar(fccharset, utf8codepoint); + + if (!drw->fonts->pattern) { + /* Refer to the comment in xfont_create for more information. */ + die("the first font in the cache must be loaded from a font string."); + } + + fcpattern = FcPatternDuplicate(drw->fonts->pattern); + FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue); + + FcConfigSubstitute(NULL, fcpattern, FcMatchPattern); + FcDefaultSubstitute(fcpattern); + match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result); + + FcCharSetDestroy(fccharset); + FcPatternDestroy(fcpattern); + + if (match) { + usedfont = xfont_create(drw, NULL, match); + if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) { + for (curfont = drw->fonts; curfont->next; curfont = curfont->next) + ; /* NOP */ + curfont->next = usedfont; + } else { + xfont_free(usedfont); + nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint; +no_match: + usedfont = drw->fonts; + } + } + } + } + if (d) + XftDrawDestroy(d); + + return x + (render ? w : 0); +} + +void +drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y); + XSync(drw->dpy, False); +} + +unsigned int +drw_fontset_getwidth(Drw *drw, const char *text) +{ + if (!drw || !drw->fonts || !text) + return 0; + return drw_text(drw, 0, 0, 0, 0, 0, text, 0); +} + +unsigned int +drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n) +{ + unsigned int tmp = 0; + if (drw && drw->fonts && text && n) + tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n); + return MIN(n, tmp); +} + +void +drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h) +{ + XGlyphInfo ext; + + if (!font || !text) + return; + + XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext); + if (w) + *w = ext.xOff; + if (h) + *h = font->h; +} + +Cur * +drw_cur_create(Drw *drw, int shape) +{ + Cur *cur; + + if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) + return NULL; + + cur->cursor = XCreateFontCursor(drw->dpy, shape); + + return cur; +} + +void +drw_cur_free(Drw *drw, Cur *cursor) +{ + if (!cursor) + return; + + XFreeCursor(drw->dpy, cursor->cursor); + free(cursor); +} diff --git a/drw.h b/drw.h new file mode 100644 index 0000000..bda06f9 --- /dev/null +++ b/drw.h @@ -0,0 +1,60 @@ +/* See LICENSE file for copyright and license details. */ + +typedef struct { + Cursor cursor; +} Cur; + +typedef struct Fnt { + Display *dpy; + unsigned int h; + XftFont *xfont; + FcPattern *pattern; + struct Fnt *next; +} Fnt; + +enum { ColFg, ColBg, ColBorder }; /* Clr scheme index */ +typedef XftColor Clr; + +typedef struct { + unsigned int w, h; + Display *dpy; + int screen; + Window root; + Drawable drawable; + GC gc; + Clr *scheme; + Fnt *fonts; +} Drw; + +/* Drawable abstraction */ +Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h); +void drw_resize(Drw *drw, unsigned int w, unsigned int h); +void drw_free(Drw *drw); + +/* Fnt abstraction */ +Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount); +void drw_fontset_free(Fnt* set); +unsigned int drw_fontset_getwidth(Drw *drw, const char *text); +unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n); +void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h); + +/* Colorscheme abstraction */ +void drw_clr_create(Drw *drw, Clr *dest, const char *clrname); +void drw_clr_free(Drw *drw, Clr *c); +Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount); +void drw_scm_free(Drw *drw, Clr *scm, size_t clrcount); + +/* Cursor abstraction */ +Cur *drw_cur_create(Drw *drw, int shape); +void drw_cur_free(Drw *drw, Cur *cursor); + +/* Drawing context manipulation */ +void drw_setfontset(Drw *drw, Fnt *set); +void drw_setscheme(Drw *drw, Clr *scm); + +/* Drawing functions */ +void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert); +int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert); + +/* Map functions */ +void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); diff --git a/dwm.1 b/dwm.1 new file mode 100644 index 0000000..ddc8321 --- /dev/null +++ b/dwm.1 @@ -0,0 +1,176 @@ +.TH DWM 1 dwm\-VERSION +.SH NAME +dwm \- dynamic window manager +.SH SYNOPSIS +.B dwm +.RB [ \-v ] +.SH DESCRIPTION +dwm is a dynamic window manager for X. It manages windows in tiled, monocle +and floating layouts. Either layout can be applied dynamically, optimising the +environment for the application in use and the task performed. +.P +In tiled layouts windows are managed in a master and stacking area. The master +area on the left contains one window by default, and the stacking area on the +right contains all other windows. The number of master area windows can be +adjusted from zero to an arbitrary number. In monocle layout all windows are +maximised to the screen size. In floating layout windows can be resized and +moved freely. Dialog windows are always managed floating, regardless of the +layout applied. +.P +Windows are grouped by tags. Each window can be tagged with one or multiple +tags. Selecting certain tags displays all windows with these tags. +.P +Each screen contains a small status bar which displays all available tags, the +layout, the title of the focused window, and the text read from the root window +name property, if the screen is focused. A floating window is indicated with an +empty square and a maximised floating window is indicated with a filled square +before the windows title. The selected tags are indicated with a different +color. The tags of the focused window are indicated with a filled square in the +top left corner. The tags which are applied to one or more windows are +indicated with an empty square in the top left corner. +.P +dwm draws a small border around windows to indicate the focus state. +.SH OPTIONS +.TP +.B \-v +prints version information to stderr, then exits. +.SH USAGE +.SS Status bar +.TP +.B X root window name +is read and displayed in the status text area. It can be set with the +.BR xsetroot (1) +command. +.TP +.B Button1 +click on a tag label to display all windows with that tag, click on the layout +label toggles between tiled and floating layout. +.TP +.B Button3 +click on a tag label adds/removes all windows with that tag to/from the view. +.TP +.B Mod1\-Button1 +click on a tag label applies that tag to the focused window. +.TP +.B Mod1\-Button3 +click on a tag label adds/removes that tag to/from the focused window. +.SS Keyboard commands +.TP +.B Mod1\-Shift\-Return +Start +.BR st(1). +.TP +.B Mod1\-p +Spawn +.BR dmenu(1) +for launching other programs. +.TP +.B Mod1\-, +Focus previous screen, if any. +.TP +.B Mod1\-. +Focus next screen, if any. +.TP +.B Mod1\-Shift\-, +Send focused window to previous screen, if any. +.TP +.B Mod1\-Shift\-. +Send focused window to next screen, if any. +.TP +.B Mod1\-b +Toggles bar on and off. +.TP +.B Mod1\-t +Sets tiled layout. +.TP +.B Mod1\-f +Sets floating layout. +.TP +.B Mod1\-m +Sets monocle layout. +.TP +.B Mod1\-space +Toggles between current and previous layout. +.TP +.B Mod1\-j +Focus next window. +.TP +.B Mod1\-k +Focus previous window. +.TP +.B Mod1\-i +Increase number of windows in master area. +.TP +.B Mod1\-d +Decrease number of windows in master area. +.TP +.B Mod1\-l +Increase master area size. +.TP +.B Mod1\-h +Decrease master area size. +.TP +.B Mod1\-Return +Zooms/cycles focused window to/from master area (tiled layouts only). +.TP +.B Mod1\-Shift\-c +Close focused window. +.TP +.B Mod1\-Shift\-space +Toggle focused window between tiled and floating state. +.TP +.B Mod1\-Tab +Toggles to the previously selected tags. +.TP +.B Mod1\-Shift\-[1..n] +Apply nth tag to focused window. +.TP +.B Mod1\-Shift\-0 +Apply all tags to focused window. +.TP +.B Mod1\-Control\-Shift\-[1..n] +Add/remove nth tag to/from focused window. +.TP +.B Mod1\-[1..n] +View all windows with nth tag. +.TP +.B Mod1\-0 +View all windows with any tag. +.TP +.B Mod1\-Control\-[1..n] +Add/remove all windows with nth tag to/from the view. +.TP +.B Mod1\-Shift\-q +Quit dwm. +.SS Mouse commands +.TP +.B Mod1\-Button1 +Move focused window while dragging. Tiled windows will be toggled to the floating state. +.TP +.B Mod1\-Button2 +Toggles focused window between floating and tiled state. +.TP +.B Mod1\-Button3 +Resize focused window while dragging. Tiled windows will be toggled to the floating state. +.SH CUSTOMIZATION +dwm is customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH SEE ALSO +.BR dmenu (1), +.BR st (1) +.SH ISSUES +Java applications which use the XToolkit/XAWT backend may draw grey windows +only. The XToolkit/XAWT backend breaks ICCCM-compliance in recent JDK 1.5 and early +JDK 1.6 versions, because it assumes a reparenting window manager. Possible workarounds +are using JDK 1.4 (which doesn't contain the XToolkit/XAWT backend) or setting the +environment variable +.BR AWT_TOOLKIT=MToolkit +(to use the older Motif backend instead) or running +.B xprop -root -f _NET_WM_NAME 32a -set _NET_WM_NAME LG3D +or +.B wmname LG3D +(to pretend that a non-reparenting window manager is running that the +XToolkit/XAWT backend can recognize) or when using OpenJDK setting the environment variable +.BR _JAVA_AWT_WM_NONREPARENTING=1 . +.SH BUGS +Send all bug reports with a patch to hackers@suckless.org. diff --git a/dwm.c b/dwm.c new file mode 100644 index 0000000..ab3a84c --- /dev/null +++ b/dwm.c @@ -0,0 +1,2165 @@ +/* See LICENSE file for copyright and license details. + * + * dynamic window manager is designed like any other X client as well. It is + * driven through handling X events. In contrast to other X clients, a window + * manager selects for SubstructureRedirectMask on the root window, to receive + * events about window (dis-)appearance. Only one X connection at a time is + * allowed to select for this event mask. + * + * The event handlers of dwm are organized in an array which is accessed + * whenever a new event has been fetched. This allows event dispatching + * in O(1) time. + * + * Each child of the root window is called a client, except windows which have + * set the override_redirect flag. Clients are organized in a linked client + * list on each monitor, the focus history is remembered through a stack list + * on each monitor. Each client contains a bit array to indicate the tags of a + * client. + * + * Keys and tagging rules are organized as arrays and defined in config.h. + * + * To understand everything else, start reading main(). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef XINERAMA +#include +#endif /* XINERAMA */ +#include + +#include "drw.h" +#include "util.h" + +/* macros */ +#define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) +#define CLEANMASK(mask) (mask & ~(numlockmask|LockMask) & (ShiftMask|ControlMask|Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|Mod5Mask)) +#define INTERSECT(x,y,w,h,m) (MAX(0, MIN((x)+(w),(m)->wx+(m)->ww) - MAX((x),(m)->wx)) \ + * MAX(0, MIN((y)+(h),(m)->wy+(m)->wh) - MAX((y),(m)->wy))) +#define ISVISIBLE(C) ((C->tags & C->mon->tagset[C->mon->seltags])) +#define MOUSEMASK (BUTTONMASK|PointerMotionMask) +#define WIDTH(X) ((X)->w + 2 * (X)->bw) +#define HEIGHT(X) ((X)->h + 2 * (X)->bw) +#define TAGMASK ((1 << LENGTH(tags)) - 1) +#define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) + +/* enums */ +enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */ +enum { SchemeNorm, SchemeSel }; /* color schemes */ +enum { NetSupported, NetWMName, NetWMState, NetWMCheck, + NetWMFullscreen, NetActiveWindow, NetWMWindowType, + NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */ +enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */ +enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, + ClkClientWin, ClkRootWin, ClkLast }; /* clicks */ + +typedef union { + int i; + unsigned int ui; + float f; + const void *v; +} Arg; + +typedef struct { + unsigned int click; + unsigned int mask; + unsigned int button; + void (*func)(const Arg *arg); + const Arg arg; +} Button; + +typedef struct Monitor Monitor; +typedef struct Client Client; +struct Client { + char name[256]; + float mina, maxa; + int x, y, w, h; + int oldx, oldy, oldw, oldh; + int basew, baseh, incw, inch, maxw, maxh, minw, minh, hintsvalid; + int bw, oldbw; + unsigned int tags; + int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen; + Client *next; + Client *snext; + Monitor *mon; + Window win; +}; + +typedef struct { + unsigned int mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Key; + +typedef struct { + const char *symbol; + void (*arrange)(Monitor *); +} Layout; + +struct Monitor { + char ltsymbol[16]; + float mfact; + int nmaster; + int num; + int by; /* bar geometry */ + int mx, my, mw, mh; /* screen size */ + int wx, wy, ww, wh; /* window area */ + unsigned int seltags; + unsigned int sellt; + unsigned int tagset[2]; + int showbar; + int topbar; + Client *clients; + Client *sel; + Client *stack; + Monitor *next; + Window barwin; + const Layout *lt[2]; +}; + +typedef struct { + const char *class; + const char *instance; + const char *title; + unsigned int tags; + int isfloating; + int monitor; +} Rule; + +/* function declarations */ +static void applyrules(Client *c); +static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact); +static void arrange(Monitor *m); +static void arrangemon(Monitor *m); +static void attach(Client *c); +static void attachstack(Client *c); +static void buttonpress(XEvent *e); +static void checkotherwm(void); +static void cleanup(void); +static void cleanupmon(Monitor *mon); +static void clientmessage(XEvent *e); +static void configure(Client *c); +static void configurenotify(XEvent *e); +static void configurerequest(XEvent *e); +static Monitor *createmon(void); +static void destroynotify(XEvent *e); +static void detach(Client *c); +static void detachstack(Client *c); +static Monitor *dirtomon(int dir); +static void drawbar(Monitor *m); +static void drawbars(void); +static void enternotify(XEvent *e); +static void expose(XEvent *e); +static void focus(Client *c); +static void focusin(XEvent *e); +static void focusmon(const Arg *arg); +static void focusstack(const Arg *arg); +static Atom getatomprop(Client *c, Atom prop); +static int getrootptr(int *x, int *y); +static long getstate(Window w); +static int gettextprop(Window w, Atom atom, char *text, unsigned int size); +static void grabbuttons(Client *c, int focused); +static void grabkeys(void); +static void incnmaster(const Arg *arg); +static void keypress(XEvent *e); +static void killclient(const Arg *arg); +static void manage(Window w, XWindowAttributes *wa); +static void mappingnotify(XEvent *e); +static void maprequest(XEvent *e); +static void monocle(Monitor *m); +static void motionnotify(XEvent *e); +static void movemouse(const Arg *arg); +static Client *nexttiled(Client *c); +static void pop(Client *c); +static void propertynotify(XEvent *e); +static void quit(const Arg *arg); +static Monitor *recttomon(int x, int y, int w, int h); +static void resize(Client *c, int x, int y, int w, int h, int interact); +static void resizeclient(Client *c, int x, int y, int w, int h); +static void resizemouse(const Arg *arg); +static void restack(Monitor *m); +static void run(void); +static void scan(void); +static int sendevent(Client *c, Atom proto); +static void sendmon(Client *c, Monitor *m); +static void setclientstate(Client *c, long state); +static void setfocus(Client *c); +static void setfullscreen(Client *c, int fullscreen); +static void setlayout(const Arg *arg); +static void setmfact(const Arg *arg); +static void setup(void); +static void seturgent(Client *c, int urg); +static void showhide(Client *c); +static void spawn(const Arg *arg); +static void tag(const Arg *arg); +static void tagmon(const Arg *arg); +static void tile(Monitor *m); +static void togglebar(const Arg *arg); +static void togglefloating(const Arg *arg); +static void toggletag(const Arg *arg); +static void toggleview(const Arg *arg); +static void unfocus(Client *c, int setfocus); +static void unmanage(Client *c, int destroyed); +static void unmapnotify(XEvent *e); +static void updatebarpos(Monitor *m); +static void updatebars(void); +static void updateclientlist(void); +static int updategeom(void); +static void updatenumlockmask(void); +static void updatesizehints(Client *c); +static void updatestatus(void); +static void updatetitle(Client *c); +static void updatewindowtype(Client *c); +static void updatewmhints(Client *c); +static void view(const Arg *arg); +static Client *wintoclient(Window w); +static Monitor *wintomon(Window w); +static int xerror(Display *dpy, XErrorEvent *ee); +static int xerrordummy(Display *dpy, XErrorEvent *ee); +static int xerrorstart(Display *dpy, XErrorEvent *ee); +static void zoom(const Arg *arg); + +/* variables */ +static const char broken[] = "broken"; +static char stext[256]; +static int screen; +static int sw, sh; /* X display screen geometry width, height */ +static int bh; /* bar height */ +static int lrpad; /* sum of left and right padding for text */ +static int (*xerrorxlib)(Display *, XErrorEvent *); +static unsigned int numlockmask = 0; +static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, + [ClientMessage] = clientmessage, + [ConfigureRequest] = configurerequest, + [ConfigureNotify] = configurenotify, + [DestroyNotify] = destroynotify, + [EnterNotify] = enternotify, + [Expose] = expose, + [FocusIn] = focusin, + [KeyPress] = keypress, + [MappingNotify] = mappingnotify, + [MapRequest] = maprequest, + [MotionNotify] = motionnotify, + [PropertyNotify] = propertynotify, + [UnmapNotify] = unmapnotify +}; +static Atom wmatom[WMLast], netatom[NetLast]; +static int running = 1; +static Cur *cursor[CurLast]; +static Clr **scheme; +static Display *dpy; +static Drw *drw; +static Monitor *mons, *selmon; +static Window root, wmcheckwin; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +/* compile-time check if all tags fit into an unsigned int bit array. */ +struct NumTags { char limitexceeded[LENGTH(tags) > 31 ? -1 : 1]; }; + +/* function implementations */ +void +applyrules(Client *c) +{ + const char *class, *instance; + unsigned int i; + const Rule *r; + Monitor *m; + XClassHint ch = { NULL, NULL }; + + /* rule matching */ + c->isfloating = 0; + c->tags = 0; + XGetClassHint(dpy, c->win, &ch); + class = ch.res_class ? ch.res_class : broken; + instance = ch.res_name ? ch.res_name : broken; + + for (i = 0; i < LENGTH(rules); i++) { + r = &rules[i]; + if ((!r->title || strstr(c->name, r->title)) + && (!r->class || strstr(class, r->class)) + && (!r->instance || strstr(instance, r->instance))) + { + c->isfloating = r->isfloating; + c->tags |= r->tags; + for (m = mons; m && m->num != r->monitor; m = m->next); + if (m) + c->mon = m; + } + } + if (ch.res_class) + XFree(ch.res_class); + if (ch.res_name) + XFree(ch.res_name); + c->tags = c->tags & TAGMASK ? c->tags & TAGMASK : c->mon->tagset[c->mon->seltags]; +} + +int +applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) +{ + int baseismin; + Monitor *m = c->mon; + + /* set minimum possible */ + *w = MAX(1, *w); + *h = MAX(1, *h); + if (interact) { + if (*x > sw) + *x = sw - WIDTH(c); + if (*y > sh) + *y = sh - HEIGHT(c); + if (*x + *w + 2 * c->bw < 0) + *x = 0; + if (*y + *h + 2 * c->bw < 0) + *y = 0; + } else { + if (*x >= m->wx + m->ww) + *x = m->wx + m->ww - WIDTH(c); + if (*y >= m->wy + m->wh) + *y = m->wy + m->wh - HEIGHT(c); + if (*x + *w + 2 * c->bw <= m->wx) + *x = m->wx; + if (*y + *h + 2 * c->bw <= m->wy) + *y = m->wy; + } + if (*h < bh) + *h = bh; + if (*w < bh) + *w = bh; + if (resizehints || c->isfloating || !c->mon->lt[c->mon->sellt]->arrange) { + if (!c->hintsvalid) + updatesizehints(c); + /* see last two sentences in ICCCM 4.1.2.3 */ + baseismin = c->basew == c->minw && c->baseh == c->minh; + if (!baseismin) { /* temporarily remove base dimensions */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for aspect limits */ + if (c->mina > 0 && c->maxa > 0) { + if (c->maxa < (float)*w / *h) + *w = *h * c->maxa + 0.5; + else if (c->mina < (float)*h / *w) + *h = *w * c->mina + 0.5; + } + if (baseismin) { /* increment calculation requires this */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for increment value */ + if (c->incw) + *w -= *w % c->incw; + if (c->inch) + *h -= *h % c->inch; + /* restore base dimensions */ + *w = MAX(*w + c->basew, c->minw); + *h = MAX(*h + c->baseh, c->minh); + if (c->maxw) + *w = MIN(*w, c->maxw); + if (c->maxh) + *h = MIN(*h, c->maxh); + } + return *x != c->x || *y != c->y || *w != c->w || *h != c->h; +} + +void +arrange(Monitor *m) +{ + if (m) + showhide(m->stack); + else for (m = mons; m; m = m->next) + showhide(m->stack); + if (m) { + arrangemon(m); + restack(m); + } else for (m = mons; m; m = m->next) + arrangemon(m); +} + +void +arrangemon(Monitor *m) +{ + strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof m->ltsymbol); + if (m->lt[m->sellt]->arrange) + m->lt[m->sellt]->arrange(m); +} + +void +attach(Client *c) +{ + c->next = c->mon->clients; + c->mon->clients = c; +} + +void +attachstack(Client *c) +{ + c->snext = c->mon->stack; + c->mon->stack = c; +} + +void +buttonpress(XEvent *e) +{ + unsigned int i, x, click; + Arg arg = {0}; + Client *c; + Monitor *m; + XButtonPressedEvent *ev = &e->xbutton; + + click = ClkRootWin; + /* focus monitor if necessary */ + if ((m = wintomon(ev->window)) && m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + if (ev->window == selmon->barwin) { + i = x = 0; + do + x += TEXTW(tags[i]); + while (ev->x >= x && ++i < LENGTH(tags)); + if (i < LENGTH(tags)) { + click = ClkTagBar; + arg.ui = 1 << i; + } else if (ev->x < x + TEXTW(selmon->ltsymbol)) + click = ClkLtSymbol; + else if (ev->x > selmon->ww - (int)TEXTW(stext) + lrpad - 2) + click = ClkStatusText; + else + click = ClkWinTitle; + } else if ((c = wintoclient(ev->window))) { + focus(c); + restack(selmon); + XAllowEvents(dpy, ReplayPointer, CurrentTime); + click = ClkClientWin; + } + for (i = 0; i < LENGTH(buttons); i++) + if (click == buttons[i].click && buttons[i].func && buttons[i].button == ev->button + && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state)) + buttons[i].func(click == ClkTagBar && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg); +} + +void +checkotherwm(void) +{ + xerrorxlib = XSetErrorHandler(xerrorstart); + /* this causes an error if some other window manager is running */ + XSelectInput(dpy, DefaultRootWindow(dpy), SubstructureRedirectMask); + XSync(dpy, False); + XSetErrorHandler(xerror); + XSync(dpy, False); +} + +void +cleanup(void) +{ + Arg a = {.ui = ~0}; + Layout foo = { "", NULL }; + Monitor *m; + size_t i; + + view(&a); + selmon->lt[selmon->sellt] = &foo; + for (m = mons; m; m = m->next) + while (m->stack) + unmanage(m->stack, 0); + XUngrabKey(dpy, AnyKey, AnyModifier, root); + while (mons) + cleanupmon(mons); + for (i = 0; i < CurLast; i++) + drw_cur_free(drw, cursor[i]); + for (i = 0; i < LENGTH(colors); i++) + drw_scm_free(drw, scheme[i], 3); + free(scheme); + XDestroyWindow(dpy, wmcheckwin); + drw_free(drw); + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); +} + +void +cleanupmon(Monitor *mon) +{ + Monitor *m; + + if (mon == mons) + mons = mons->next; + else { + for (m = mons; m && m->next != mon; m = m->next); + m->next = mon->next; + } + XUnmapWindow(dpy, mon->barwin); + XDestroyWindow(dpy, mon->barwin); + free(mon); +} + +void +clientmessage(XEvent *e) +{ + XClientMessageEvent *cme = &e->xclient; + Client *c = wintoclient(cme->window); + + if (!c) + return; + if (cme->message_type == netatom[NetWMState]) { + if (cme->data.l[1] == netatom[NetWMFullscreen] + || cme->data.l[2] == netatom[NetWMFullscreen]) + setfullscreen(c, (cme->data.l[0] == 1 /* _NET_WM_STATE_ADD */ + || (cme->data.l[0] == 2 /* _NET_WM_STATE_TOGGLE */ && !c->isfullscreen))); + } else if (cme->message_type == netatom[NetActiveWindow]) { + if (c != selmon->sel && !c->isurgent) + seturgent(c, 1); + } +} + +void +configure(Client *c) +{ + XConfigureEvent ce; + + ce.type = ConfigureNotify; + ce.display = dpy; + ce.event = c->win; + ce.window = c->win; + ce.x = c->x; + ce.y = c->y; + ce.width = c->w; + ce.height = c->h; + ce.border_width = c->bw; + ce.above = None; + ce.override_redirect = False; + XSendEvent(dpy, c->win, False, StructureNotifyMask, (XEvent *)&ce); +} + +void +configurenotify(XEvent *e) +{ + Monitor *m; + Client *c; + XConfigureEvent *ev = &e->xconfigure; + int dirty; + + /* TODO: updategeom handling sucks, needs to be simplified */ + if (ev->window == root) { + dirty = (sw != ev->width || sh != ev->height); + sw = ev->width; + sh = ev->height; + if (updategeom() || dirty) { + drw_resize(drw, sw, bh); + updatebars(); + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) + if (c->isfullscreen) + resizeclient(c, m->mx, m->my, m->mw, m->mh); + XMoveResizeWindow(dpy, m->barwin, m->wx, m->by, m->ww, bh); + } + focus(NULL); + arrange(NULL); + } + } +} + +void +configurerequest(XEvent *e) +{ + Client *c; + Monitor *m; + XConfigureRequestEvent *ev = &e->xconfigurerequest; + XWindowChanges wc; + + if ((c = wintoclient(ev->window))) { + if (ev->value_mask & CWBorderWidth) + c->bw = ev->border_width; + else if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) { + m = c->mon; + if (ev->value_mask & CWX) { + c->oldx = c->x; + c->x = m->mx + ev->x; + } + if (ev->value_mask & CWY) { + c->oldy = c->y; + c->y = m->my + ev->y; + } + if (ev->value_mask & CWWidth) { + c->oldw = c->w; + c->w = ev->width; + } + if (ev->value_mask & CWHeight) { + c->oldh = c->h; + c->h = ev->height; + } + if ((c->x + c->w) > m->mx + m->mw && c->isfloating) + c->x = m->mx + (m->mw / 2 - WIDTH(c) / 2); /* center in x direction */ + if ((c->y + c->h) > m->my + m->mh && c->isfloating) + c->y = m->my + (m->mh / 2 - HEIGHT(c) / 2); /* center in y direction */ + if ((ev->value_mask & (CWX|CWY)) && !(ev->value_mask & (CWWidth|CWHeight))) + configure(c); + if (ISVISIBLE(c)) + XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h); + } else + configure(c); + } else { + wc.x = ev->x; + wc.y = ev->y; + wc.width = ev->width; + wc.height = ev->height; + wc.border_width = ev->border_width; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + XConfigureWindow(dpy, ev->window, ev->value_mask, &wc); + } + XSync(dpy, False); +} + +Monitor * +createmon(void) +{ + Monitor *m; + + m = ecalloc(1, sizeof(Monitor)); + m->tagset[0] = m->tagset[1] = 1; + m->mfact = mfact; + m->nmaster = nmaster; + m->showbar = showbar; + m->topbar = topbar; + m->lt[0] = &layouts[0]; + m->lt[1] = &layouts[1 % LENGTH(layouts)]; + strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol); + return m; +} + +void +destroynotify(XEvent *e) +{ + Client *c; + XDestroyWindowEvent *ev = &e->xdestroywindow; + + if ((c = wintoclient(ev->window))) + unmanage(c, 1); +} + +void +detach(Client *c) +{ + Client **tc; + + for (tc = &c->mon->clients; *tc && *tc != c; tc = &(*tc)->next); + *tc = c->next; +} + +void +detachstack(Client *c) +{ + Client **tc, *t; + + for (tc = &c->mon->stack; *tc && *tc != c; tc = &(*tc)->snext); + *tc = c->snext; + + if (c == c->mon->sel) { + for (t = c->mon->stack; t && !ISVISIBLE(t); t = t->snext); + c->mon->sel = t; + } +} + +Monitor * +dirtomon(int dir) +{ + Monitor *m = NULL; + + if (dir > 0) { + if (!(m = selmon->next)) + m = mons; + } else if (selmon == mons) + for (m = mons; m->next; m = m->next); + else + for (m = mons; m->next != selmon; m = m->next); + return m; +} + +void +drawbar(Monitor *m) +{ + int x, w, tw = 0; + int boxs = drw->fonts->h / 9; + int boxw = drw->fonts->h / 6 + 2; + unsigned int i, occ = 0, urg = 0; + Client *c; + + if (!m->showbar) + return; + + /* draw status first so it can be overdrawn by tags later */ + if (m == selmon) { /* status is only drawn on selected monitor */ + drw_setscheme(drw, scheme[SchemeNorm]); + tw = TEXTW(stext) - lrpad + 2; /* 2px right padding */ + drw_text(drw, m->ww - tw, 0, tw, bh, 0, stext, 0); + } + + for (c = m->clients; c; c = c->next) { + occ |= c->tags; + if (c->isurgent) + urg |= c->tags; + } + x = 0; + for (i = 0; i < LENGTH(tags); i++) { + w = TEXTW(tags[i]); + drw_setscheme(drw, scheme[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, tags[i], urg & 1 << i); + if (occ & 1 << i) + drw_rect(drw, x + boxs, boxs, boxw, boxw, + m == selmon && selmon->sel && selmon->sel->tags & 1 << i, + urg & 1 << i); + x += w; + } + w = TEXTW(m->ltsymbol); + drw_setscheme(drw, scheme[SchemeNorm]); + x = drw_text(drw, x, 0, w, bh, lrpad / 2, m->ltsymbol, 0); + + if ((w = m->ww - tw - x) > bh) { + if (m->sel) { + drw_setscheme(drw, scheme[m == selmon ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, m->sel->name, 0); + if (m->sel->isfloating) + drw_rect(drw, x + boxs, boxs, boxw, boxw, m->sel->isfixed, 0); + } else { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, x, 0, w, bh, 1, 1); + } + } + drw_map(drw, m->barwin, 0, 0, m->ww, bh); +} + +void +drawbars(void) +{ + Monitor *m; + + for (m = mons; m; m = m->next) + drawbar(m); +} + +void +enternotify(XEvent *e) +{ + Client *c; + Monitor *m; + XCrossingEvent *ev = &e->xcrossing; + + if ((ev->mode != NotifyNormal || ev->detail == NotifyInferior) && ev->window != root) + return; + c = wintoclient(ev->window); + m = c ? c->mon : wintomon(ev->window); + if (m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + } else if (!c || c == selmon->sel) + return; + focus(c); +} + +void +expose(XEvent *e) +{ + Monitor *m; + XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && (m = wintomon(ev->window))) + drawbar(m); +} + +void +focus(Client *c) +{ + if (!c || !ISVISIBLE(c)) + for (c = selmon->stack; c && !ISVISIBLE(c); c = c->snext); + if (selmon->sel && selmon->sel != c) + unfocus(selmon->sel, 0); + if (c) { + if (c->mon != selmon) + selmon = c->mon; + if (c->isurgent) + seturgent(c, 0); + detachstack(c); + attachstack(c); + grabbuttons(c, 1); + XSetWindowBorder(dpy, c->win, scheme[SchemeSel][ColBorder].pixel); + setfocus(c); + } else { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } + selmon->sel = c; + drawbars(); +} + +/* there are some broken focus acquiring clients needing extra handling */ +void +focusin(XEvent *e) +{ + XFocusChangeEvent *ev = &e->xfocus; + + if (selmon->sel && ev->window != selmon->sel->win) + setfocus(selmon->sel); +} + +void +focusmon(const Arg *arg) +{ + Monitor *m; + + if (!mons->next) + return; + if ((m = dirtomon(arg->i)) == selmon) + return; + unfocus(selmon->sel, 0); + selmon = m; + focus(NULL); +} + +void +focusstack(const Arg *arg) +{ + Client *c = NULL, *i; + + if (!selmon->sel || (selmon->sel->isfullscreen && lockfullscreen)) + return; + if (arg->i > 0) { + for (c = selmon->sel->next; c && !ISVISIBLE(c); c = c->next); + if (!c) + for (c = selmon->clients; c && !ISVISIBLE(c); c = c->next); + } else { + for (i = selmon->clients; i != selmon->sel; i = i->next) + if (ISVISIBLE(i)) + c = i; + if (!c) + for (; i; i = i->next) + if (ISVISIBLE(i)) + c = i; + } + if (c) { + focus(c); + restack(selmon); + } +} + +Atom +getatomprop(Client *c, Atom prop) +{ + int format; + unsigned long nitems, dl; + unsigned char *p = NULL; + Atom da, atom = None; + + if (XGetWindowProperty(dpy, c->win, prop, 0L, sizeof atom, False, XA_ATOM, + &da, &format, &nitems, &dl, &p) == Success && p) { + if (nitems > 0 && format == 32) + atom = *(long *)p; + XFree(p); + } + return atom; +} + +int +getrootptr(int *x, int *y) +{ + int di; + unsigned int dui; + Window dummy; + + return XQueryPointer(dpy, root, &dummy, &dummy, x, y, &di, &di, &dui); +} + +long +getstate(Window w) +{ + int format; + long result = -1; + unsigned char *p = NULL; + unsigned long n, extra; + Atom real; + + if (XGetWindowProperty(dpy, w, wmatom[WMState], 0L, 2L, False, wmatom[WMState], + &real, &format, &n, &extra, &p) != Success) + return -1; + if (n != 0 && format == 32) + result = *(long *)p; + XFree(p); + return result; +} + +int +gettextprop(Window w, Atom atom, char *text, unsigned int size) +{ + char **list = NULL; + int n; + XTextProperty name; + + if (!text || size == 0) + return 0; + text[0] = '\0'; + if (!XGetTextProperty(dpy, w, &name, atom) || !name.nitems) + return 0; + if (name.encoding == XA_STRING) { + strncpy(text, (char *)name.value, size - 1); + } else if (XmbTextPropertyToTextList(dpy, &name, &list, &n) >= Success && n > 0 && *list) { + strncpy(text, *list, size - 1); + XFreeStringList(list); + } + text[size - 1] = '\0'; + XFree(name.value); + return 1; +} + +void +grabbuttons(Client *c, int focused) +{ + updatenumlockmask(); + { + unsigned int i, j; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + if (!focused) + XGrabButton(dpy, AnyButton, AnyModifier, c->win, False, + BUTTONMASK, GrabModeSync, GrabModeSync, None, None); + for (i = 0; i < LENGTH(buttons); i++) + if (buttons[i].click == ClkClientWin) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabButton(dpy, buttons[i].button, + buttons[i].mask | modifiers[j], + c->win, False, BUTTONMASK, + GrabModeAsync, GrabModeSync, None, None); + } +} + +void +grabkeys(void) +{ + updatenumlockmask(); + { + unsigned int i, j, k; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + int start, end, skip; + KeySym *syms; + + XUngrabKey(dpy, AnyKey, AnyModifier, root); + XDisplayKeycodes(dpy, &start, &end); + syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip); + if (!syms) + return; + for (k = start; k <= end; k++) + for (i = 0; i < LENGTH(keys); i++) + /* skip modifier codes, we do that ourselves */ + if (keys[i].keysym == syms[(k - start) * skip]) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabKey(dpy, k, + keys[i].mod | modifiers[j], + root, True, + GrabModeAsync, GrabModeAsync); + XFree(syms); + } +} + +void +incnmaster(const Arg *arg) +{ + selmon->nmaster = MAX(selmon->nmaster + arg->i, 0); + arrange(selmon); +} + +#ifdef XINERAMA +static int +isuniquegeom(XineramaScreenInfo *unique, size_t n, XineramaScreenInfo *info) +{ + while (n--) + if (unique[n].x_org == info->x_org && unique[n].y_org == info->y_org + && unique[n].width == info->width && unique[n].height == info->height) + return 0; + return 1; +} +#endif /* XINERAMA */ + +void +keypress(XEvent *e) +{ + unsigned int i; + KeySym keysym; + XKeyEvent *ev; + + ev = &e->xkey; + keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0); + for (i = 0; i < LENGTH(keys); i++) + if (keysym == keys[i].keysym + && CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) + && keys[i].func) + keys[i].func(&(keys[i].arg)); +} + +void +killclient(const Arg *arg) +{ + if (!selmon->sel) + return; + if (!sendevent(selmon->sel, wmatom[WMDelete])) { + XGrabServer(dpy); + XSetErrorHandler(xerrordummy); + XSetCloseDownMode(dpy, DestroyAll); + XKillClient(dpy, selmon->sel->win); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } +} + +void +manage(Window w, XWindowAttributes *wa) +{ + Client *c, *t = NULL; + Window trans = None; + XWindowChanges wc; + + c = ecalloc(1, sizeof(Client)); + c->win = w; + /* geometry */ + c->x = c->oldx = wa->x; + c->y = c->oldy = wa->y; + c->w = c->oldw = wa->width; + c->h = c->oldh = wa->height; + c->oldbw = wa->border_width; + + updatetitle(c); + if (XGetTransientForHint(dpy, w, &trans) && (t = wintoclient(trans))) { + c->mon = t->mon; + c->tags = t->tags; + } else { + c->mon = selmon; + applyrules(c); + } + + if (c->x + WIDTH(c) > c->mon->wx + c->mon->ww) + c->x = c->mon->wx + c->mon->ww - WIDTH(c); + if (c->y + HEIGHT(c) > c->mon->wy + c->mon->wh) + c->y = c->mon->wy + c->mon->wh - HEIGHT(c); + c->x = MAX(c->x, c->mon->wx); + c->y = MAX(c->y, c->mon->wy); + c->bw = borderpx; + + wc.border_width = c->bw; + XConfigureWindow(dpy, w, CWBorderWidth, &wc); + XSetWindowBorder(dpy, w, scheme[SchemeNorm][ColBorder].pixel); + configure(c); /* propagates border_width, if size doesn't change */ + updatewindowtype(c); + updatesizehints(c); + updatewmhints(c); + XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); + grabbuttons(c, 0); + if (!c->isfloating) + c->isfloating = c->oldstate = trans != None || c->isfixed; + if (c->isfloating) + XRaiseWindow(dpy, c->win); + attach(c); + attachstack(c); + XChangeProperty(dpy, root, netatom[NetClientList], XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); + XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ + setclientstate(c, NormalState); + if (c->mon == selmon) + unfocus(selmon->sel, 0); + c->mon->sel = c; + arrange(c->mon); + XMapWindow(dpy, c->win); + focus(NULL); +} + +void +mappingnotify(XEvent *e) +{ + XMappingEvent *ev = &e->xmapping; + + XRefreshKeyboardMapping(ev); + if (ev->request == MappingKeyboard) + grabkeys(); +} + +void +maprequest(XEvent *e) +{ + static XWindowAttributes wa; + XMapRequestEvent *ev = &e->xmaprequest; + + if (!XGetWindowAttributes(dpy, ev->window, &wa) || wa.override_redirect) + return; + if (!wintoclient(ev->window)) + manage(ev->window, &wa); +} + +void +monocle(Monitor *m) +{ + unsigned int n = 0; + Client *c; + + for (c = m->clients; c; c = c->next) + if (ISVISIBLE(c)) + n++; + if (n > 0) /* override layout symbol */ + snprintf(m->ltsymbol, sizeof m->ltsymbol, "[%d]", n); + for (c = nexttiled(m->clients); c; c = nexttiled(c->next)) + resize(c, m->wx, m->wy, m->ww - 2 * c->bw, m->wh - 2 * c->bw, 0); +} + +void +motionnotify(XEvent *e) +{ + static Monitor *mon = NULL; + Monitor *m; + XMotionEvent *ev = &e->xmotion; + + if (ev->window != root) + return; + if ((m = recttomon(ev->x_root, ev->y_root, 1, 1)) != mon && mon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + mon = m; +} + +void +movemouse(const Arg *arg) +{ + int x, y, ocx, ocy, nx, ny; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support moving fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurMove]->cursor, CurrentTime) != GrabSuccess) + return; + if (!getrootptr(&x, &y)) + return; + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / refreshrate)) + continue; + lasttime = ev.xmotion.time; + + nx = ocx + (ev.xmotion.x - x); + ny = ocy + (ev.xmotion.y - y); + if (abs(selmon->wx - nx) < snap) + nx = selmon->wx; + else if (abs((selmon->wx + selmon->ww) - (nx + WIDTH(c))) < snap) + nx = selmon->wx + selmon->ww - WIDTH(c); + if (abs(selmon->wy - ny) < snap) + ny = selmon->wy; + else if (abs((selmon->wy + selmon->wh) - (ny + HEIGHT(c))) < snap) + ny = selmon->wy + selmon->wh - HEIGHT(c); + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nx - c->x) > snap || abs(ny - c->y) > snap)) + togglefloating(NULL); + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, nx, ny, c->w, c->h, 1); + break; + } + } while (ev.type != ButtonRelease); + XUngrabPointer(dpy, CurrentTime); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +Client * +nexttiled(Client *c) +{ + for (; c && (c->isfloating || !ISVISIBLE(c)); c = c->next); + return c; +} + +void +pop(Client *c) +{ + detach(c); + attach(c); + focus(c); + arrange(c->mon); +} + +void +propertynotify(XEvent *e) +{ + Client *c; + Window trans; + XPropertyEvent *ev = &e->xproperty; + + if ((ev->window == root) && (ev->atom == XA_WM_NAME)) + updatestatus(); + else if (ev->state == PropertyDelete) + return; /* ignore */ + else if ((c = wintoclient(ev->window))) { + switch(ev->atom) { + default: break; + case XA_WM_TRANSIENT_FOR: + if (!c->isfloating && (XGetTransientForHint(dpy, c->win, &trans)) && + (c->isfloating = (wintoclient(trans)) != NULL)) + arrange(c->mon); + break; + case XA_WM_NORMAL_HINTS: + c->hintsvalid = 0; + break; + case XA_WM_HINTS: + updatewmhints(c); + drawbars(); + break; + } + if (ev->atom == XA_WM_NAME || ev->atom == netatom[NetWMName]) { + updatetitle(c); + if (c == c->mon->sel) + drawbar(c->mon); + } + if (ev->atom == netatom[NetWMWindowType]) + updatewindowtype(c); + } +} + +void +quit(const Arg *arg) +{ + running = 0; +} + +Monitor * +recttomon(int x, int y, int w, int h) +{ + Monitor *m, *r = selmon; + int a, area = 0; + + for (m = mons; m; m = m->next) + if ((a = INTERSECT(x, y, w, h, m)) > area) { + area = a; + r = m; + } + return r; +} + +void +resize(Client *c, int x, int y, int w, int h, int interact) +{ + if (applysizehints(c, &x, &y, &w, &h, interact)) + resizeclient(c, x, y, w, h); +} + +void +resizeclient(Client *c, int x, int y, int w, int h) +{ + XWindowChanges wc; + + c->oldx = c->x; c->x = wc.x = x; + c->oldy = c->y; c->y = wc.y = y; + c->oldw = c->w; c->w = wc.width = w; + c->oldh = c->h; c->h = wc.height = h; + wc.border_width = c->bw; + XConfigureWindow(dpy, c->win, CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &wc); + configure(c); + XSync(dpy, False); +} + +void +resizemouse(const Arg *arg) +{ + int ocx, ocy, nw, nh; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support resizing fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurResize]->cursor, CurrentTime) != GrabSuccess) + return; + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / refreshrate)) + continue; + lasttime = ev.xmotion.time; + + nw = MAX(ev.xmotion.x - ocx - 2 * c->bw + 1, 1); + nh = MAX(ev.xmotion.y - ocy - 2 * c->bw + 1, 1); + if (c->mon->wx + nw >= selmon->wx && c->mon->wx + nw <= selmon->wx + selmon->ww + && c->mon->wy + nh >= selmon->wy && c->mon->wy + nh <= selmon->wy + selmon->wh) + { + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nw - c->w) > snap || abs(nh - c->h) > snap)) + togglefloating(NULL); + } + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, c->x, c->y, nw, nh, 1); + break; + } + } while (ev.type != ButtonRelease); + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + XUngrabPointer(dpy, CurrentTime); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +void +restack(Monitor *m) +{ + Client *c; + XEvent ev; + XWindowChanges wc; + + drawbar(m); + if (!m->sel) + return; + if (m->sel->isfloating || !m->lt[m->sellt]->arrange) + XRaiseWindow(dpy, m->sel->win); + if (m->lt[m->sellt]->arrange) { + wc.stack_mode = Below; + wc.sibling = m->barwin; + for (c = m->stack; c; c = c->snext) + if (!c->isfloating && ISVISIBLE(c)) { + XConfigureWindow(dpy, c->win, CWSibling|CWStackMode, &wc); + wc.sibling = c->win; + } + } + XSync(dpy, False); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); +} + +void +run(void) +{ + XEvent ev; + /* main event loop */ + XSync(dpy, False); + while (running && !XNextEvent(dpy, &ev)) + if (handler[ev.type]) + handler[ev.type](&ev); /* call handler */ +} + +void +scan(void) +{ + unsigned int i, num; + Window d1, d2, *wins = NULL; + XWindowAttributes wa; + + if (XQueryTree(dpy, root, &d1, &d2, &wins, &num)) { + for (i = 0; i < num; i++) { + if (!XGetWindowAttributes(dpy, wins[i], &wa) + || wa.override_redirect || XGetTransientForHint(dpy, wins[i], &d1)) + continue; + if (wa.map_state == IsViewable || getstate(wins[i]) == IconicState) + manage(wins[i], &wa); + } + for (i = 0; i < num; i++) { /* now the transients */ + if (!XGetWindowAttributes(dpy, wins[i], &wa)) + continue; + if (XGetTransientForHint(dpy, wins[i], &d1) + && (wa.map_state == IsViewable || getstate(wins[i]) == IconicState)) + manage(wins[i], &wa); + } + if (wins) + XFree(wins); + } +} + +void +sendmon(Client *c, Monitor *m) +{ + if (c->mon == m) + return; + unfocus(c, 1); + detach(c); + detachstack(c); + c->mon = m; + c->tags = m->tagset[m->seltags]; /* assign tags of target monitor */ + attach(c); + attachstack(c); + if (c->isfullscreen) + resizeclient(c, m->mx, m->my, m->mw, m->mh); + focus(NULL); + arrange(NULL); +} + +void +setclientstate(Client *c, long state) +{ + long data[] = { state, None }; + + XChangeProperty(dpy, c->win, wmatom[WMState], wmatom[WMState], 32, + PropModeReplace, (unsigned char *)data, 2); +} + +int +sendevent(Client *c, Atom proto) +{ + int n; + Atom *protocols; + int exists = 0; + XEvent ev; + + if (XGetWMProtocols(dpy, c->win, &protocols, &n)) { + while (!exists && n--) + exists = protocols[n] == proto; + XFree(protocols); + } + if (exists) { + ev.type = ClientMessage; + ev.xclient.window = c->win; + ev.xclient.message_type = wmatom[WMProtocols]; + ev.xclient.format = 32; + ev.xclient.data.l[0] = proto; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, c->win, False, NoEventMask, &ev); + } + return exists; +} + +void +setfocus(Client *c) +{ + if (!c->neverfocus) + XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); + XChangeProperty(dpy, root, netatom[NetActiveWindow], XA_WINDOW, 32, + PropModeReplace, (unsigned char *)&c->win, 1); + sendevent(c, wmatom[WMTakeFocus]); +} + +void +setfullscreen(Client *c, int fullscreen) +{ + if (fullscreen && !c->isfullscreen) { + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)&netatom[NetWMFullscreen], 1); + c->isfullscreen = 1; + c->oldstate = c->isfloating; + c->oldbw = c->bw; + c->bw = 0; + c->isfloating = 1; + resizeclient(c, c->mon->mx, c->mon->my, c->mon->mw, c->mon->mh); + XRaiseWindow(dpy, c->win); + } else if (!fullscreen && c->isfullscreen){ + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)0, 0); + c->isfullscreen = 0; + c->isfloating = c->oldstate; + c->bw = c->oldbw; + c->x = c->oldx; + c->y = c->oldy; + c->w = c->oldw; + c->h = c->oldh; + resizeclient(c, c->x, c->y, c->w, c->h); + arrange(c->mon); + } +} + +void +setlayout(const Arg *arg) +{ + if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt]) + selmon->sellt ^= 1; + if (arg && arg->v) + selmon->lt[selmon->sellt] = (Layout *)arg->v; + strncpy(selmon->ltsymbol, selmon->lt[selmon->sellt]->symbol, sizeof selmon->ltsymbol); + if (selmon->sel) + arrange(selmon); + else + drawbar(selmon); +} + +/* arg > 1.0 will set mfact absolutely */ +void +setmfact(const Arg *arg) +{ + float f; + + if (!arg || !selmon->lt[selmon->sellt]->arrange) + return; + f = arg->f < 1.0 ? arg->f + selmon->mfact : arg->f - 1.0; + if (f < 0.05 || f > 0.95) + return; + selmon->mfact = f; + arrange(selmon); +} + +void +setup(void) +{ + int i; + XSetWindowAttributes wa; + Atom utf8string; + struct sigaction sa; + + /* do not transform children into zombies when they terminate */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_RESTART; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + + /* clean up any zombies (inherited from .xinitrc etc) immediately */ + while (waitpid(-1, NULL, WNOHANG) > 0); + + /* init screen */ + screen = DefaultScreen(dpy); + sw = DisplayWidth(dpy, screen); + sh = DisplayHeight(dpy, screen); + root = RootWindow(dpy, screen); + drw = drw_create(dpy, screen, root, sw, sh); + if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) + die("no fonts could be loaded."); + lrpad = drw->fonts->h; + bh = drw->fonts->h + 2; + updategeom(); + /* init atoms */ + utf8string = XInternAtom(dpy, "UTF8_STRING", False); + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wmatom[WMState] = XInternAtom(dpy, "WM_STATE", False); + wmatom[WMTakeFocus] = XInternAtom(dpy, "WM_TAKE_FOCUS", False); + netatom[NetActiveWindow] = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + netatom[NetSupported] = XInternAtom(dpy, "_NET_SUPPORTED", False); + netatom[NetWMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + netatom[NetWMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + netatom[NetWMCheck] = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False); + netatom[NetWMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False); + /* init cursors */ + cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr); + cursor[CurResize] = drw_cur_create(drw, XC_sizing); + cursor[CurMove] = drw_cur_create(drw, XC_fleur); + /* init appearance */ + scheme = ecalloc(LENGTH(colors), sizeof(Clr *)); + for (i = 0; i < LENGTH(colors); i++) + scheme[i] = drw_scm_create(drw, colors[i], 3); + /* init bars */ + updatebars(); + updatestatus(); + /* supporting window for NetWMCheck */ + wmcheckwin = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMName], utf8string, 8, + PropModeReplace, (unsigned char *) "dwm", 3); + XChangeProperty(dpy, root, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + /* EWMH support per view */ + XChangeProperty(dpy, root, netatom[NetSupported], XA_ATOM, 32, + PropModeReplace, (unsigned char *) netatom, NetLast); + XDeleteProperty(dpy, root, netatom[NetClientList]); + /* select events */ + wa.cursor = cursor[CurNormal]->cursor; + wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask + |ButtonPressMask|PointerMotionMask|EnterWindowMask + |LeaveWindowMask|StructureNotifyMask|PropertyChangeMask; + XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa); + XSelectInput(dpy, root, wa.event_mask); + grabkeys(); + focus(NULL); +} + +void +seturgent(Client *c, int urg) +{ + XWMHints *wmh; + + c->isurgent = urg; + if (!(wmh = XGetWMHints(dpy, c->win))) + return; + wmh->flags = urg ? (wmh->flags | XUrgencyHint) : (wmh->flags & ~XUrgencyHint); + XSetWMHints(dpy, c->win, wmh); + XFree(wmh); +} + +void +showhide(Client *c) +{ + if (!c) + return; + if (ISVISIBLE(c)) { + /* show clients top down */ + XMoveWindow(dpy, c->win, c->x, c->y); + if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen) + resize(c, c->x, c->y, c->w, c->h, 0); + showhide(c->snext); + } else { + /* hide clients bottom up */ + showhide(c->snext); + XMoveWindow(dpy, c->win, WIDTH(c) * -2, c->y); + } +} + +void +spawn(const Arg *arg) +{ + struct sigaction sa; + + if (arg->v == dmenucmd) + dmenumon[0] = '0' + selmon->num; + if (fork() == 0) { + if (dpy) + close(ConnectionNumber(dpy)); + setsid(); + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa, NULL); + + execvp(((char **)arg->v)[0], (char **)arg->v); + die("dwm: execvp '%s' failed:", ((char **)arg->v)[0]); + } +} + +void +tag(const Arg *arg) +{ + if (selmon->sel && arg->ui & TAGMASK) { + selmon->sel->tags = arg->ui & TAGMASK; + focus(NULL); + arrange(selmon); + } +} + +void +tagmon(const Arg *arg) +{ + if (!selmon->sel || !mons->next) + return; + sendmon(selmon->sel, dirtomon(arg->i)); +} + +void +tile(Monitor *m) +{ + unsigned int i, n, h, mw, my, ty; + Client *c; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++); + if (n == 0) + return; + + if (n > m->nmaster) + mw = m->nmaster ? m->ww * m->mfact : 0; + else + mw = m->ww; + for (i = my = ty = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) + if (i < m->nmaster) { + h = (m->wh - my) / (MIN(n, m->nmaster) - i); + resize(c, m->wx, m->wy + my, mw - (2*c->bw), h - (2*c->bw), 0); + if (my + HEIGHT(c) < m->wh) + my += HEIGHT(c); + } else { + h = (m->wh - ty) / (n - i); + resize(c, m->wx + mw, m->wy + ty, m->ww - mw - (2*c->bw), h - (2*c->bw), 0); + if (ty + HEIGHT(c) < m->wh) + ty += HEIGHT(c); + } +} + +void +togglebar(const Arg *arg) +{ + selmon->showbar = !selmon->showbar; + updatebarpos(selmon); + XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); + arrange(selmon); +} + +void +togglefloating(const Arg *arg) +{ + if (!selmon->sel) + return; + if (selmon->sel->isfullscreen) /* no support for fullscreen windows */ + return; + selmon->sel->isfloating = !selmon->sel->isfloating || selmon->sel->isfixed; + if (selmon->sel->isfloating) + resize(selmon->sel, selmon->sel->x, selmon->sel->y, + selmon->sel->w, selmon->sel->h, 0); + arrange(selmon); +} + +void +toggletag(const Arg *arg) +{ + unsigned int newtags; + + if (!selmon->sel) + return; + newtags = selmon->sel->tags ^ (arg->ui & TAGMASK); + if (newtags) { + selmon->sel->tags = newtags; + focus(NULL); + arrange(selmon); + } +} + +void +toggleview(const Arg *arg) +{ + unsigned int newtagset = selmon->tagset[selmon->seltags] ^ (arg->ui & TAGMASK); + + if (newtagset) { + selmon->tagset[selmon->seltags] = newtagset; + focus(NULL); + arrange(selmon); + } +} + +void +unfocus(Client *c, int setfocus) +{ + if (!c) + return; + grabbuttons(c, 0); + XSetWindowBorder(dpy, c->win, scheme[SchemeNorm][ColBorder].pixel); + if (setfocus) { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } +} + +void +unmanage(Client *c, int destroyed) +{ + Monitor *m = c->mon; + XWindowChanges wc; + + detach(c); + detachstack(c); + if (!destroyed) { + wc.border_width = c->oldbw; + XGrabServer(dpy); /* avoid race conditions */ + XSetErrorHandler(xerrordummy); + XSelectInput(dpy, c->win, NoEventMask); + XConfigureWindow(dpy, c->win, CWBorderWidth, &wc); /* restore border */ + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + setclientstate(c, WithdrawnState); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } + free(c); + focus(NULL); + updateclientlist(); + arrange(m); +} + +void +unmapnotify(XEvent *e) +{ + Client *c; + XUnmapEvent *ev = &e->xunmap; + + if ((c = wintoclient(ev->window))) { + if (ev->send_event) + setclientstate(c, WithdrawnState); + else + unmanage(c, 0); + } +} + +void +updatebars(void) +{ + Monitor *m; + XSetWindowAttributes wa = { + .override_redirect = True, + .background_pixmap = ParentRelative, + .event_mask = ButtonPressMask|ExposureMask + }; + XClassHint ch = {"dwm", "dwm"}; + for (m = mons; m; m = m->next) { + if (m->barwin) + continue; + m->barwin = XCreateWindow(dpy, root, m->wx, m->by, m->ww, bh, 0, DefaultDepth(dpy, screen), + CopyFromParent, DefaultVisual(dpy, screen), + CWOverrideRedirect|CWBackPixmap|CWEventMask, &wa); + XDefineCursor(dpy, m->barwin, cursor[CurNormal]->cursor); + XMapRaised(dpy, m->barwin); + XSetClassHint(dpy, m->barwin, &ch); + } +} + +void +updatebarpos(Monitor *m) +{ + m->wy = m->my; + m->wh = m->mh; + if (m->showbar) { + m->wh -= bh; + m->by = m->topbar ? m->wy : m->wy + m->wh; + m->wy = m->topbar ? m->wy + bh : m->wy; + } else + m->by = -bh; +} + +void +updateclientlist(void) +{ + Client *c; + Monitor *m; + + XDeleteProperty(dpy, root, netatom[NetClientList]); + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + XChangeProperty(dpy, root, netatom[NetClientList], + XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); +} + +int +updategeom(void) +{ + int dirty = 0; + +#ifdef XINERAMA + if (XineramaIsActive(dpy)) { + int i, j, n, nn; + Client *c; + Monitor *m; + XineramaScreenInfo *info = XineramaQueryScreens(dpy, &nn); + XineramaScreenInfo *unique = NULL; + + for (n = 0, m = mons; m; m = m->next, n++); + /* only consider unique geometries as separate screens */ + unique = ecalloc(nn, sizeof(XineramaScreenInfo)); + for (i = 0, j = 0; i < nn; i++) + if (isuniquegeom(unique, j, &info[i])) + memcpy(&unique[j++], &info[i], sizeof(XineramaScreenInfo)); + XFree(info); + nn = j; + + /* new monitors if nn > n */ + for (i = n; i < nn; i++) { + for (m = mons; m && m->next; m = m->next); + if (m) + m->next = createmon(); + else + mons = createmon(); + } + for (i = 0, m = mons; i < nn && m; m = m->next, i++) + if (i >= n + || unique[i].x_org != m->mx || unique[i].y_org != m->my + || unique[i].width != m->mw || unique[i].height != m->mh) + { + dirty = 1; + m->num = i; + m->mx = m->wx = unique[i].x_org; + m->my = m->wy = unique[i].y_org; + m->mw = m->ww = unique[i].width; + m->mh = m->wh = unique[i].height; + updatebarpos(m); + } + /* removed monitors if n > nn */ + for (i = nn; i < n; i++) { + for (m = mons; m && m->next; m = m->next); + while ((c = m->clients)) { + dirty = 1; + m->clients = c->next; + detachstack(c); + c->mon = mons; + attach(c); + attachstack(c); + } + if (m == selmon) + selmon = mons; + cleanupmon(m); + } + free(unique); + } else +#endif /* XINERAMA */ + { /* default monitor setup */ + if (!mons) + mons = createmon(); + if (mons->mw != sw || mons->mh != sh) { + dirty = 1; + mons->mw = mons->ww = sw; + mons->mh = mons->wh = sh; + updatebarpos(mons); + } + } + if (dirty) { + selmon = mons; + selmon = wintomon(root); + } + return dirty; +} + +void +updatenumlockmask(void) +{ + unsigned int i, j; + XModifierKeymap *modmap; + + numlockmask = 0; + modmap = XGetModifierMapping(dpy); + for (i = 0; i < 8; i++) + for (j = 0; j < modmap->max_keypermod; j++) + if (modmap->modifiermap[i * modmap->max_keypermod + j] + == XKeysymToKeycode(dpy, XK_Num_Lock)) + numlockmask = (1 << i); + XFreeModifiermap(modmap); +} + +void +updatesizehints(Client *c) +{ + long msize; + XSizeHints size; + + if (!XGetWMNormalHints(dpy, c->win, &size, &msize)) + /* size is uninitialized, ensure that size.flags aren't used */ + size.flags = PSize; + if (size.flags & PBaseSize) { + c->basew = size.base_width; + c->baseh = size.base_height; + } else if (size.flags & PMinSize) { + c->basew = size.min_width; + c->baseh = size.min_height; + } else + c->basew = c->baseh = 0; + if (size.flags & PResizeInc) { + c->incw = size.width_inc; + c->inch = size.height_inc; + } else + c->incw = c->inch = 0; + if (size.flags & PMaxSize) { + c->maxw = size.max_width; + c->maxh = size.max_height; + } else + c->maxw = c->maxh = 0; + if (size.flags & PMinSize) { + c->minw = size.min_width; + c->minh = size.min_height; + } else if (size.flags & PBaseSize) { + c->minw = size.base_width; + c->minh = size.base_height; + } else + c->minw = c->minh = 0; + if (size.flags & PAspect) { + c->mina = (float)size.min_aspect.y / size.min_aspect.x; + c->maxa = (float)size.max_aspect.x / size.max_aspect.y; + } else + c->maxa = c->mina = 0.0; + c->isfixed = (c->maxw && c->maxh && c->maxw == c->minw && c->maxh == c->minh); + c->hintsvalid = 1; +} + +void +updatestatus(void) +{ + if (!gettextprop(root, XA_WM_NAME, stext, sizeof(stext))) + strcpy(stext, "dwm-"VERSION); + drawbar(selmon); +} + +void +updatetitle(Client *c) +{ + if (!gettextprop(c->win, netatom[NetWMName], c->name, sizeof c->name)) + gettextprop(c->win, XA_WM_NAME, c->name, sizeof c->name); + if (c->name[0] == '\0') /* hack to mark broken clients */ + strcpy(c->name, broken); +} + +void +updatewindowtype(Client *c) +{ + Atom state = getatomprop(c, netatom[NetWMState]); + Atom wtype = getatomprop(c, netatom[NetWMWindowType]); + + if (state == netatom[NetWMFullscreen]) + setfullscreen(c, 1); + if (wtype == netatom[NetWMWindowTypeDialog]) + c->isfloating = 1; +} + +void +updatewmhints(Client *c) +{ + XWMHints *wmh; + + if ((wmh = XGetWMHints(dpy, c->win))) { + if (c == selmon->sel && wmh->flags & XUrgencyHint) { + wmh->flags &= ~XUrgencyHint; + XSetWMHints(dpy, c->win, wmh); + } else + c->isurgent = (wmh->flags & XUrgencyHint) ? 1 : 0; + if (wmh->flags & InputHint) + c->neverfocus = !wmh->input; + else + c->neverfocus = 0; + XFree(wmh); + } +} + +void +view(const Arg *arg) +{ + if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags]) + return; + selmon->seltags ^= 1; /* toggle sel tagset */ + if (arg->ui & TAGMASK) + selmon->tagset[selmon->seltags] = arg->ui & TAGMASK; + focus(NULL); + arrange(selmon); +} + +Client * +wintoclient(Window w) +{ + Client *c; + Monitor *m; + + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + if (c->win == w) + return c; + return NULL; +} + +Monitor * +wintomon(Window w) +{ + int x, y; + Client *c; + Monitor *m; + + if (w == root && getrootptr(&x, &y)) + return recttomon(x, y, 1, 1); + for (m = mons; m; m = m->next) + if (w == m->barwin) + return m; + if ((c = wintoclient(w))) + return c->mon; + return selmon; +} + +/* There's no way to check accesses to destroyed windows, thus those cases are + * ignored (especially on UnmapNotify's). Other types of errors call Xlibs + * default error handler, which may call exit. */ +int +xerror(Display *dpy, XErrorEvent *ee) +{ + if (ee->error_code == BadWindow + || (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) + || (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) + || (ee->request_code == X_PolyFillRectangle && ee->error_code == BadDrawable) + || (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) + || (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) + || (ee->request_code == X_GrabButton && ee->error_code == BadAccess) + || (ee->request_code == X_GrabKey && ee->error_code == BadAccess) + || (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) + return 0; + fprintf(stderr, "dwm: fatal error: request code=%d, error code=%d\n", + ee->request_code, ee->error_code); + return xerrorxlib(dpy, ee); /* may call exit */ +} + +int +xerrordummy(Display *dpy, XErrorEvent *ee) +{ + return 0; +} + +/* Startup Error handler to check if another window manager + * is already running. */ +int +xerrorstart(Display *dpy, XErrorEvent *ee) +{ + die("dwm: another window manager is already running"); + return -1; +} + +void +zoom(const Arg *arg) +{ + Client *c = selmon->sel; + + if (!selmon->lt[selmon->sellt]->arrange || !c || c->isfloating) + return; + if (c == nexttiled(selmon->clients) && !(c = nexttiled(c->next))) + return; + pop(c); +} + +int +main(int argc, char *argv[]) +{ + if (argc == 2 && !strcmp("-v", argv[1])) + die("dwm-"VERSION); + else if (argc != 1) + die("usage: dwm [-v]"); + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("warning: no locale support\n", stderr); + if (!(dpy = XOpenDisplay(NULL))) + die("dwm: cannot open display"); + checkotherwm(); + setup(); +#ifdef __OpenBSD__ + if (pledge("stdio rpath proc exec", NULL) == -1) + die("pledge"); +#endif /* __OpenBSD__ */ + scan(); + run(); + cleanup(); + XCloseDisplay(dpy); + return EXIT_SUCCESS; +} diff --git a/dwm.png b/dwm.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f9ba7e5f4cc7350ee2392ebcea5fcbe00fb49b GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0y~yU^u|Qz!1Q}1Y+HIE6l*az?S6g?!xdN1Q+aGKAC}m zfwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWxvKQ%wx!QPg+ZWfq_A`#5JPCIX^cyHLrxh zxhOTUBsE2$JhLQ2AtWPJ!QIn0;C+f}9s>izO-~ockcwMx?>cfd81OJSD*yX``Xg^6 zTaZz4SYYHW?+Yi+q)+P5khOpNZ+qolCYF{40Rbi^MnwgdmIen04h}{|1pxsjCkGA= zh?J9q0!T_g04&AA!nDZw&Oe5$@0m;M>sM6OY71;LKv54jO6E(b|BHY2*SA&9y8V3S z8iyN)!0OOUb8_ejn#VrNZ2je0``gbZufgI1u!%>m)^#5eUAZ~`sxkuu1B0ilpUXO@ GgeCxv)opSB literal 0 HcmV?d00001 diff --git a/transient.c b/transient.c new file mode 100644 index 0000000..040adb5 --- /dev/null +++ b/transient.c @@ -0,0 +1,42 @@ +/* cc transient.c -o transient -lX11 */ + +#include +#include +#include +#include + +int main(void) { + Display *d; + Window r, f, t = None; + XSizeHints h; + XEvent e; + + d = XOpenDisplay(NULL); + if (!d) + exit(1); + r = DefaultRootWindow(d); + + f = XCreateSimpleWindow(d, r, 100, 100, 400, 400, 0, 0, 0); + h.min_width = h.max_width = h.min_height = h.max_height = 400; + h.flags = PMinSize | PMaxSize; + XSetWMNormalHints(d, f, &h); + XStoreName(d, f, "floating"); + XMapWindow(d, f); + + XSelectInput(d, f, ExposureMask); + while (1) { + XNextEvent(d, &e); + + if (t == None) { + sleep(5); + t = XCreateSimpleWindow(d, r, 50, 50, 100, 100, 0, 0, 0); + XSetTransientForHint(d, t, f); + XStoreName(d, t, "transient"); + XMapWindow(d, t); + XSelectInput(d, t, ExposureMask); + } + } + + XCloseDisplay(d); + exit(0); +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..8e26a51 --- /dev/null +++ b/util.c @@ -0,0 +1,37 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "util.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + int saved_errno; + + saved_errno = errno; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') + fprintf(stderr, " %s", strerror(saved_errno)); + fputc('\n', stderr); + + exit(1); +} + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..c0a50d4 --- /dev/null +++ b/util.h @@ -0,0 +1,9 @@ +/* See LICENSE file for copyright and license details. */ + +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) +#define LENGTH(X) (sizeof (X) / sizeof (X)[0]) + +void die(const char *fmt, ...); +void *ecalloc(size_t nmemb, size_t size); From d6dbf86493f9c7c6a25221c09a6218b1958debba Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:25 +0200 Subject: [PATCH 09/11] =?UTF-8?q?st:=20im=C3=A1genes=20con=20el=20protocol?= =?UTF-8?q?o=20de=20kitty=20(vistas=20previas=20en=20yazi)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se rehace st desde el 0.9.3 original con los parches: - kitty-graphics (oficial, 20251230): muestra imágenes en la terminal; yazi lo detecta solo y no necesita ueberzugpp. Ya incluye anysize, así que ese parche sobra. - alpha, sin cambios. - glyph-wide-support: los iconos anchos (Nerd Font) no se cortan. - ligatures, con los conflictos resueltos como en la rama graphics-with-patches de st-graphics, quitando lo de boxdraw. config.h lleva los mismos cambios de antes sobre el config.def.h nuevo. Necesita imlib2-devel para compilar. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m --- suckless/st/Makefile | 7 +- suckless/st/config.def.h | 46 +- suckless/st/config.h | 46 +- suckless/st/config.mk | 5 +- suckless/st/graphics.c | 4393 +++++++++ suckless/st/graphics.h | 112 + suckless/st/icat-mini.sh | 875 ++ suckless/st/khash.h | 627 ++ suckless/st/kvec.h | 90 + .../patches/st-anysize-20220718-baa9357.diff | 164 - .../st-glyph-wide-support-graphics.diff | 199 + .../st-kitty-graphics-20251230-0.9.3.diff | 8033 +++++++++++++++++ suckless/st/rowcolumn_diacritics_helpers.c | 391 + suckless/st/st.c | 303 +- suckless/st/st.h | 90 +- suckless/st/st.info | 6 + suckless/st/win.h | 3 + suckless/st/x.c | 678 +- 18 files changed, 15728 insertions(+), 340 deletions(-) create mode 100644 suckless/st/graphics.c create mode 100644 suckless/st/graphics.h create mode 100755 suckless/st/icat-mini.sh create mode 100644 suckless/st/khash.h create mode 100644 suckless/st/kvec.h delete mode 100644 suckless/st/patches/st-anysize-20220718-baa9357.diff create mode 100644 suckless/st/patches/st-glyph-wide-support-graphics.diff create mode 100644 suckless/st/patches/st-kitty-graphics-20251230-0.9.3.diff create mode 100644 suckless/st/rowcolumn_diacritics_helpers.c diff --git a/suckless/st/Makefile b/suckless/st/Makefile index dfcea0f..4c5251c 100644 --- a/suckless/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/suckless/st/config.def.h b/suckless/st/config.def.h index 019a4e1..3601e41 100644 --- a/suckless/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/suckless/st/config.h b/suckless/st/config.h index 2e27b4d..78d8154 100644 --- a/suckless/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/suckless/st/config.mk b/suckless/st/config.mk index ec69d1a..922a9c5 100644 --- a/suckless/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/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/suckless/st/patches/st-anysize-20220718-baa9357.diff b/suckless/st/patches/st-anysize-20220718-baa9357.diff deleted file mode 100644 index 675ffdf..0000000 --- a/suckless/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/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/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/suckless/st/st.c b/suckless/st/st.c index e16541a..cd9e718 100644 --- a/suckless/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 index 142fdfe..864ddfb 100644 --- a/suckless/st/st.h +++ b/suckless/st/st.h @@ -13,7 +13,7 @@ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) #define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \ (a).fg != (b).fg || \ - (a).bg != (b).bg) + (a).bg != (b).bg || (a).decor != (b).decor) #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ (t1.tv_nsec-t2.tv_nsec)/1E6) #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) @@ -21,6 +21,10 @@ #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) #define IS_TRUECOL(x) (1 << 24 & (x)) +// This decor color indicates that the fg color should be used. Note that it's +// not a 24-bit color because the 25-th bit is not set. +#define DECOR_DEFAULT_COLOR 0x0ffffff + enum glyph_attribute { ATTR_NULL = 0, ATTR_BOLD = 1 << 0, @@ -35,6 +39,13 @@ enum glyph_attribute { 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 { @@ -53,6 +64,14 @@ enum selection_snap { SNAP_LINE = 2 }; +enum underline_style { + UNDERLINE_STRAIGHT = 1, + UNDERLINE_DOUBLE = 2, + UNDERLINE_CURLY = 3, + UNDERLINE_DOTTED = 4, + UNDERLINE_DASHED = 5, +}; + typedef unsigned char uchar; typedef unsigned int uint; typedef unsigned long ulong; @@ -66,6 +85,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; @@ -106,6 +126,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); @@ -125,3 +147,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/suckless/st/st.info b/suckless/st/st.info index efab2cf..ded76c1 100644 --- a/suckless/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/suckless/st/win.h b/suckless/st/win.h index 94679e4..c292af5 100644 --- a/suckless/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/suckless/st/x.c b/suckless/st/x.c index 9292287..2261ff1 100644 --- a/suckless/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; From 3aa250c8ddf167026e2ab834f1f9f4174cac73a4 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:32 +0200 Subject: [PATCH 10/11] =?UTF-8?q?Dotfiles=20de=20la=20sesi=C3=B3n=20e=20in?= =?UTF-8?q?stall.sh=20para=20Void?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - home/: xinitrc, bashrc, bash_profile, gitconfig, picom, dunst, mimeapps.list y el fondo de pantalla. Se enlazan en ~ con la misma ruta. - xinitrc: feh directamente en vez de .fehbg, y XDG_RUNTIME_DIR a mano solo si elogind no lo ha creado. - mimeapps.list: firefox.desktop en vez del .desktop autogenerado. - install.sh: instala los paquetes de Void, compila los programas, enlaza home/, prepara GnuPG (permisos, pinentry-gtk y agente SSH), copia sistema/ y activa servicios y grupos. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m --- README.md | 68 ++++- home/.bash_profile | 8 + home/.bashrc | 99 ++++++ home/.config/dunst/dunstrc | 514 ++++++++++++++++++++++++++++++++ home/.config/mimeapps.list | 23 ++ home/.config/picom/picom.conf | 44 +++ home/.gitconfig | 3 + home/.local/share/wallpaper.jpg | Bin 0 -> 374083 bytes home/.xinitrc | 31 ++ install.sh | 161 ++++++++++ 10 files changed, 939 insertions(+), 12 deletions(-) create mode 100644 home/.bash_profile create mode 100644 home/.bashrc create mode 100644 home/.config/dunst/dunstrc create mode 100644 home/.config/mimeapps.list create mode 100644 home/.config/picom/picom.conf create mode 100644 home/.gitconfig create mode 100644 home/.local/share/wallpaper.jpg create mode 100644 home/.xinitrc create mode 100755 install.sh 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/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 0000000000000000000000000000000000000000..177270dee4ac8323d9f460a07304caff38bff7fb GIT binary patch literal 374083 zcmex=F0ES%nl09od8f6WNstMT{CJF62;l+IUbj=;8+zR;@QBE$5`HGdRHf`Rrb=&qGJ9iyAeB|h{<0np@x^(%<)oa&p+`RSh(c>pipFMx^ z^3}&rpTB(l_Wj4tUm$-mGB88D#R!TEkk`=sCCI?U$i%|J%)$=x7b8j7{Xt- z715#AO0V%AHOgBb*;YFzUy0USmm@;2tge2W`*5#)(4H-qa*`8O z?=3FmDJy$9ZCVP~5r!hc7r*9QKfTO3xzKK@>Wj-s8=8zwpWJHpO7PauU3A~%dypUN zo&cwQmCbt!+}7%BT+C4VpFwb+a81w;+jZ+(_vzM{f4qO_KZEez)_p7H$^2-4s9Sxo z>T~Aygsn?|z4)iR;c}^PdRbTM)}3Pae2-Yn6R+?nV$}R${V}&rZvD}1KVm-$)`{#} zW)kva);+F|N`LHXQ)euC$^F>uvU|I9s18@JWvOz7hedm{1J95058aQpAAMVu&-F+B zN9N=Ao&VIgUtVuoxySOu=Rtgo@1YOjmp)xd+a~PKV06wPyF@uU8VarrZ4>C`k`(AgWjaq9=j#CT+RJ*Uijs& z*f+OwuQT2HXCZa%Vb>a?-BwMEI`M4#^!-1)^Y8xfN9RYU`k{!e&WpT1y4r|8GW$^~ zw{5LV{f+aF3zz+8IApYI*@-1~>PyfjM;6KCB`7P6L#m)Q}{jmQCe_PbF?Mn}@%eYm3M4tUe^TFxAwsjU9 z)ZMz)?EI0cy4i)bOJa7jY`^k$@w8**)=6`3cQKUCKX|`8s{im^_eXcz56jEntI@pp zRpU!+{G;6MEv7&B$-Ruaaz=<(I_Tq~2=lf#rIk0QI4+CL3XJ!Y^a-#?Rbgsxu#>Fu z__6+xbv=8H?g#c>JJlci-ralm?)stq{D1V<{y2YRy4f^A&h_ioe$nTPconte+P4Bh z(<>KmZ7Fb`nVQ-o#vt=<;ezZcx7Fnfvu^n&|`TYwbHf^y2eO@sn59DDT)bB`@Pys=F4W zZ2Q6g4Bb}S8%*bS{R%(K-w`$c(0#u6gG(#gABD|*a9=RKg4yw^Uf0@nv)5Lfy6!jq z@UF0hMdv#6Q|nho-#PbYrHJ1VE{3HK_}k(I_vutjx_odycMa=f{Z2cB{auxBYSbU- z^Jd!_d^CMywkqybv@iSNZ&hupi>7UC);;Z<@ra{+w%&<LB z3m0Wu#a{U)HE;5&TmH*mMV04%4YI3J4AYkV-{kNc90U1$G9EpPbxSN`a)wQY?nmV`}O-nXulSMGkm zzJC|C2ktosW!U);OXu19|Ab{7-uf6JNkXU=+^9`*KQvwR*I^aAM``M z_QS<}_UoI3IKN6LUA5D{}cxKL4KNNAo3r%zpH~#pJA?F89HIjMug>FVjDK^?GTx zH0Rb@=Oa~@UhY%gec9yw*2s*t50A`xn_KEzsyOfSqhUY4dz<2iNqHY0y)d7pI=|ch+oL}b(}TBfn{~?M zW5!-PqlsIx6ZlW<|6%I=kE`-O1Ix$1?f+!{U7pWvpVcq_Bm6=8(R#TbzuiCHZ~v!X zvDy2I%7s7aAO16Vf4JK>m;dnfk81tP@7;T8zE9%1?$`GMFMruBlqrjva!+?wHgoyP z8?&V+@OPE+|Ijr5BYbuLgJ&H786GTH`%q(DebUt*>$B_a_SKlKV%G0e`XON_wf^|_ z4>ndczjiI1fAqcjqr*|RUu>>eR-rrp$i&P|KQvozy?7jB+N*|ohpKl}i$W}PugUURmt0i-F*Kf6# z{4qW3!~ITulU~dG#7qAf9`aS17ACK_8*%#%L*1M|#{U_bUe_O#_+$Q`fhFMLKf!D} zg%5MD?R~iarl=j~>-!s9?>BEX{pjCar+oOs=YN7{!=1XUbA=us-Y%uzvnOu*tS|co z=LgL?b#rTFjq9p=(b*o?6qrA`|Ks)i&(JjUKf^)$`oy>5-&TJRf1CLDKLghv!5{q} zwU5a0ek^bPr~Biu-&MUY=Y@ahAO7XGb@}}otJz1a!e+0uYh000b@t^))2VYmyQe-X z|Hyj7$<$ZKltEsrCgeXu)9m_#wx;nvEXzIqGc+|He#rmBw?65u@+0v!rR%0`AHt`Y z&KG=npIP|fteUFLA5_2g?ps=&`QUw5QRaT(k{x(~4UtE@546GJGpn~pq?@2mfi#r{WR`8T$| zZC~|otv~ReVe9(d8q*Kq$K^#;e$=n6;C^i0U2^;97G3AVb;8>|nn;~LeEM#&_on@m z9p>E*xcavAt(En%?VCO_?%BINd)dxQI=aspem?)U<3B^w_WV4TM+sE1*P@%NvYVJh`d-wVWFXMk`{b%?i-2CnA-${1tf5iXJtW&FS{?`8E zAp5agkNO9>*$?BzUrPO(S-mcMm)qnL(|U<4se6rSHhR0ayDp5W zXpijn-M#%{>kIb#_8KYD)) znSXHCw$+c;cl~D&$o->ck}u`GIP>t~=ZEdoFV`9WXk1_8UcEm4)nebx^N*BI{ITsn zgYUodJEl4CA36WSRR15B`ERy=hyF7N?9Y17|L@v7?GM`z&KIpoyu44w|HJ*q{GDs^ z-Iccl8$968ro_6WpY;W1R#~xz8dJo0l%sg>Ge&&CM z4~6<~JAZ4{-I4zg_@6=IPv+kVZ{NK8CwusTKl=~kBXxG0F6otM*LVMC5U{b-+j8st zksT(dUuVmDM4w-nB`5L0cH8nbkx{RHy^&+}O?r0g-lcbk&oTVb7_Gtwz6u}h&|>S* zG7Y>SQ-so_%*Ac0>66?zI=Xk>m}jv1nE#c=J9E++>^Hr?<^PXY{^R*KzyC9^Jo@#`VYGgTzFB(y#3?z&YHl-vD}A$iS1vTwQGs^KFtqj+a^D>m96n!c#|V6 z`>ucN6~5)7OJXnAQr|-_yxAw6X~(ho$JFG<+WK7k)+X0iuXw#rDJ5Zznr!ge0|O8*u?djy(?p0nt$_8C^=cO{PI?9XVb4!?_OYkA%9f=hl&3q z^~3o;)UJ2hXM5IX)hhjGc>KL_AImFy`I_Vp{{I9N-F{=Z<(0C-&jn z+(+*@YOKiWMU_V`D|3^^#vFh_bVgdgd4l3l^sa7OiKKgB* z&Q9_Dk`FFE+4JFwJ$J&7c=cni^W_~Ms$EmuIXiIwkF0Is0zd5H1#~7<%zG8f^f=n% z?b2%^%rE4B=uQ74{d2{ChJ%WKWIwn+I^I%eJHM}sNC|)S@*15(h5PQa&IdOKWUsDuJYe^I?|+77 zr#j8d`VW5B|E|c}pIK`re=G0!gM~kCuc$l!{y_wP)7v$RKOCyq^`GJ4#UK6r-S>X{ zICtFrcB+1d!iBs&#%g;uWnP~`{7!>*G?Zy_xJ7zd~{yczCE|bvqE{(6&ue5w_euCUNG4kefaX7bD!?T zy$-ip9r5x~=Go-R8V9@V`TscTKhCH>m@fQ}k;|~e^Xk_?kySt+Pca?Nd=~jkc-T$~({%2tOoE87U zqrUAvr=8^w^KXr_AMZajzvG{UJh$oONBqpUen=nPC;IX2@%Ga5VwvtIF8}d;Bx@aY z!St>6;W#CO7?)Y4*ZM1$-+uW$IlyY_+EWd+^B?H{FmZmA-}|58!Q5NnZ^PN&S|{iJ zcCA?apm+W8eC`^K`1XmXAFS@x5Bo5w;_~5nEPH-*?KAOyu_toPOQEQ!Z9C>k@3>g< zHtTE2%`LxwQT&y$uo?n3n!5bvor>9q%cRUEKD=wWWp{7XzptkDZzW5+W39cnwPk0Y z{_Vie^Kt$kf!mMs+xI`%v@8CPkhJrUxewlEAG7+lKe2pqP13GQrM%jQ(;uen=U)Ef z?CKx4ejoaDKkgOh+KhUBld~0PAN<(b-E#F$=u)-z#7D-7D;?LnF@D|uN1Xc~@42h> z4<^lLt&^y`D98DCVfv5vA5I^&v)J$>{BgwA_so$$xR1;e`M7WE!t8s>7v8VQePa{) zNZU64aMjoKZCg(lG-+Gk=a0P=K}z4EceMBEq`$OF7r`2q3+{yTp#MTe-x;=#OrkW$UL>tkm3@i&T{!{r8eRQ7YkF{$n!d^doestv* z>$BX4zt{8G_%6ZTzuc`iJd1%%Yw*tl7i6 z?nn61I)#g+y}?e`N)~25suw!Ay23wXvh%*nOE>PAr0efC>#?`7|LVilCf6L~ga4hD z|B$i%2>&hrf1LLp%)hmx?#i$DmVcHti9ei|*QEal{ZLikp1Yz#_K*Fe`FzJ8&1ZUH zXYlb;#UWk4!x10TIxCmP`LBFAPki^L4_kJ%-+WiHwyZ+M?121i`43L_e<-GH{LjGJ z_)-5julEso$^Q%w-Tx^35EgIIv2k9Xc~7|i=soj~;@vf+a=Qx`zL+J~TDmr4R@vOd z#N>y2-M+_O+iLy#GWVSq*Q)Az>rA;%-_>Dm|GTdKLEC(({b}_-wALT0|Dh%~`J?dR ze^%H2*}sZoxKhVo6H%+?&-XI?K7UR1n(R0^JN=L0Z4a;7IF>K)eaM@9$j-L;h?Gs# zxrMrCx3?U9Hrwj>nn-63_FMHIqTc^7+Z_6zp((8HOdQ|e6*c_7z4-g(1STK1ll)Qn z(cbG?olNu}+p9L#>wYZuxw`byAJ2!|H-D_}%3WWhUA`vMe~I_vSHgO0mw2ztR5g{2 z)8BeBkMT?To6!FZY#o2R{;vGbkRadwpJA(c$NtQ_<`35&o8LR1yX1b4J-eOY`i8fE zd>?#%XfKh;`6$1w;K%FZVf!q$?%Z(YPvOJrx9qXAf15iLKA7?K#T(PP_K{b%ymh;D zgZZ=hKaQ#Y8CXm|>i-d4{#g9@evTijAHDb?`Qi1qoGl-YZa-36cKEPB#r~tS=6CN& zT=iqK_k}(B4|~;Wvt2Gdj<|i$PGff1N4CY~)%giJZ{ED!GS}(R58D;&!oQ2^5Ay%h zt2_Ch;X|l=^ZgG#=lN>fF6>i!HGfO!`$NCaH~nW2(fgAAVQue!hUR^m6}uxpxUSpz z=sus?=Jn?n-u)4NL{98uY~Rjww)HEdZ^sV@xl6AdHQ{E(idzzALeuZNw{dbzFn}!Zr?>yeM$en6BqsHKKMTS(SHWvqbn+^ z;)Ohu7u|g+HM2kL^5ZMHUmaK6KQeuS1LGU}5B}$Wm|Q+)zis*(&IkUtE+3KKYBu-7 z@kj2*=Cj`U@%X`ihQk(hGW?y9_K_uIsa_m-T#ooN%9p3An};KCQx>4&CO zwQ$yy?w+weCyC+Dt^W*72K5K|YBK*bum=BK@Xz*-?#KM2DnH~O`aIt4yDse0 zulUap_ak#z%#Y7r#@Y8j^xeyl)Bdo$({I-;w}p?SBe!@<*XA8QJ?lTiZ?*%>U()|^ zz5UO?wyUE1ALrKJlCl4;*D?KFF8+Al@88z9DD%XFh&X@YpeOOBNd%LN1_|>FM*W}|SM@_Pp?iOWuRsUdC{D-XYj{5xd zH>?lYZ++geKkKx8OP$t_qsuC~V}6)Eu=Z~|`ZMH%+BfOZAp!og{~3NLKmQ|e_)+*Dk@w$L|6O3m{A2wi z8+)c2_1`+~g~=cHAKf;Kb${>Ix8}!;E4D{}XzPB^&zJFfT}D*Rt-htT5BtkA~ox_s8<;2j&UzInRss;>vwE1~RM>lZ&IR^&`A7MGSWN!MmHDy% zo8jKS?R&~UW;^ctk-p}KmVDbjt&e->w?a2KP0dut<>v!nw&zk*SGHagEwS7vj zrOGC*=odV@=7w75s*3vf51$>y+t=4f);`?yPuz~b-*@{(llQ#0Z)V(5-8Vfyd%5RQ zhN|U%M5_OBef^#EpW$He*Z&OdvyQ(FeKeowF6Thi1vJ?P5UyLr0mK5fr~u71@%x_Rs62OBS~d6WBm_h$#YNgwzBk%|7#z-smJ z{u{sl3>(@uek?!y_QR^r56>p#NT=$)3Bnho*~=LdfUE;e3R(+ zyhr~taBN$@W?5_gV|l?0{zCSSJ&r5yWl61l`h&guKf^(b{SUVOXV~ukN9eZO$Nvm1 z_G$J~Kho;k_Y0Zw|M1v)zujKAM(>~Lht&RFzL}{@w)dIv8rdeh?!IIbX*PH1OR2nt zD{sBZ&Rv)J%dDGk`?CAI%D!R?L+Po2Be|Uw#X_w(Z*F+YHtG ze?;d0A{kPhW-V>9P{t==7{6l`jKIw`if7Tybhoi2(UAuN`z3|uoT}*b}SP5_fQq z4^LYt!$*El8~Be{^0#aM8JZ&HJIwZP`p?kyM_awkPTfCy#gEMg?1gMJ|1)srH?6C2 zUG8W)ZT6$zyjGXPFMO3%J}UN2c(!);UGuQ*Tdw9LY`*+;HLn-9H2YSD52xq<&}4sG zTl%};_EB0HL|4|S7&ma)}C`77k0*lq&b?x#k!Jh4hFGqVn{JBT%9(xl% zvw54I$>T+*AAGfw+r4qu#=FlLs-OQ6nf{O4^LIWd_)6Zl&y)YV{7?MDh(Dqq&i(ov z{o(rI{cZC^>)9(bYl15?e&y`bznb%I>)Ps&Yk#~{geSkmrg8hR>=YMFvZ?8`}^0)QN{hlb>^@k;Fxc)P6m)!3V{NZ)uPtk>aQge-b zx#Zb1?FHKw%TLWrfB4gW!y41Yi8E3lT$K+)ZFQYsz>zm}V-9NOezWq%JH+RGrlMlJuTXHWSG^)s|)}Ct}`k^@{ zQ|gJ;obq4&w;Jqv{xdZ7{bx9syFah~hgN)ho%|2|Zwo%Aww*{{(zBbcxL zlc_&=B>qFNeQW*3koXPLoBwH56dix)HuHgJomfTkvCTi6w9og<ubs_+H~m#^O=2W{}~Q?)@QHpp1*DB`adGVNAHVoUHW%L$$E*5 zI>Qh5`$|@8Yg_wso`1MR>=#?}!~J~Ceg;q7_(k*|_Gk4d;y+QKDwv&4QqdbRo9 zt8l3a>=*4n1ls@5{m=0B_J0Oe#}DO)&$q{K`OnZ@BW5T6$MV{vk8!gftZ%!=E5~!8 zbiL%^Wv{GE_xIHJNJm`Vsv8`$`O?ko=EN z>pw%&yg$r;`_|Sc*|+^?kSMV=e^}T5;Pn0WeS9D1es5Z1%HI>u_eZ!=^z5P^(Ff(M zvtQI5UHVPX>)&?Jm6Wo%aQ=`7-#kZ=kPO({xpYHz=?*GSi@^@nWL7kfH z$N3WfPVLkE@OA0@BYVv~FV`tnFdv^M_n)C<`NQ|!^$%rd2hToSHErplqxDN(iTyg? zK0E4Ku=~|5y~*Btw{E*;TFtp+&&Dh2tqm6WeexfI=HL4Kt@ZClll!;#Khi&Xwse1{ zJYU7+!*=Q)?7P26yxp>!zdc-zbANO2hrI3w_WUQdSGl+U(S7*(M9uG5*4(_e`g+T+ z=zncqb??=$CEFe3J^wQtbgh4|^FPD(`+r2fKiuC~$6t4;&iF%&JkKA_wUIy8>2v-u z{}5uwyYZucugP2MfQw)LGe}e%$}F|b`p*!2O{e5+eCYPgTQ}{!Y-5{Is=YJ!i^>ke zI|}Rz>dx7JDER(ItVzZ1g0>HUcPSnss;NA|LeS=-xc1RuSvK3G<#@?-sn-v90StcBRDBY_htM9RW6T{E?f1IFn5dL?~ ze})Is|1${Hq<*yi7V|NzGqd)ivfH)``;>k-AD<_mFZM(F*tII(qff85FT1|w;_A#@ zb7rk?f45aO?78c;lDpBKSKdfRTz&gw=iW_M^m3OnJlvnD|3j1iZT9crf7f@_zdifW z{88UR@wcK6-&P;|r})9W>yLJ3{=?Vu?cw%(UtO-%ur993d676|G0SnGq67Y&(PFbf6%*5;79NS|F`mw&$Au>aQJZ4 zzL}3~y${dlv*AsD(6OiXQLYg83X}PQ72hICc^$V}@Alv3U&z+Ktmh(RjKAPXNf3tbdmJ_D?WWID_XAq5c;awf6b)KlGRX5efdsW%|MR@chl&-->^fJ~Z)Nw1ArzfVs2$Isu!74PodKKjr6QvMeEjw=7YFWWxGx7aCXPQSG&JI!7)qv-za z^Og14t9540o4@|$uXS@D$=W@8_DwrBNZZ#vv^{{~seS(X9~$v*b^kN4YW%osfBgNe z$w$A-2P zGqx8DlTOlM%qU_6^xF=6fDXrB$11(P=k3UA4Z3iaea zab7@vpR4L3i%6!8ObybkbFZE(%+EJ@W-+hQW=T3HPlBXs(SnvfQ^h4^ipepXrv`X1 zxNn_j%J*?DBU)@w$h;K(zWQBT_#N$B3@4gx9x6dnbhTYy7unE+18;Q3~kn#PoF9$ADTEv z@4-9e*xeoXyoDQfT8FtGJFz5{q2yY**}LR;B)O3lo;8yKCJE zVEC2$$!R;=wP0qi1YrdsH+AXG)_F#!HnYT=pGuNHQL+5?le64A*#0xL*f2ky-(fXp zmU_b;*+>1-uLC1vUIk_R6TDFBFO;=%d1PgM&i-6$x2=2kh=tXau)TYI@{{hgil|J% zw4M#@R^P-o#%}G|w%N5(_HdMEww6io;i63@`YabcL?2F7b$9o4GBVmwV35JQ>SOxh z`VKp$E4e21vOkoM&f~ng{>yFKq{H4H+ZtC@BnRxhWGefg;pVTkOXqFRPA)87cAw+s zTK$K;Z}URf+rqB+GEp+3`(+sAL6v6K0r-u%b^alBxRtLf2w zjSpqnLq4i)FJ#@w^r*sQuGF<}|1uJESS}sUyLhCcEQpW6FmKCTw^Q$O?LJ+K&VF6J zGH0F6b8gu);sqXwv+h+ybSh5MbnJ4S7_y{capH~zeEmn~^Vuo<;L5eSecX=YV|ugR z)?PE-bgwNRHmZ;Gr7NbrRGW7B@IS#z`g=Ur#<}Kk`>RfyzSz0C zx>81PwXgB5gIdR#_8y+v8s)&)SiUSvOg!Ao{MzKmxSMx8Gb=O8+HO6&^olLxo$k7Y zvs0#hzICEk_D-;t?V7IS9T9VK7w}C!8qc?<{(;-Y@MHftKgKu5ADI=qZ|B1&AH&b!rGw;X=ANEwQ4=B8FZ0hu6!$wv)K_CwEDO`q5d-n{7;W z%>F5VESvxEZ`kLcl?8 zcmF9~7x5$gV4e8&Khck31D|wze|X=vW%k?R*elPv)aQAa+?DrjdLFocu43=f3|-x2 zQxteN^Xz0eb#+T^wBwcaUvl$(_g(y2tM@Eg*W3Hb+w=`rN`9X%&se^7@1|WI>vHoF zrbcN#m}N3e)3MJrTsniSp9O5a-iNRM zn!0rBvhB-PvY5V`X?k_lJgGnjo{M%ee;huVH9wLU`H_F%pWa9IHXCJ;;2)chRH?U{ z){Rnrw zX5;;cH>fao>5C}7x#g>0OU*s0!kugFv!UzJw_i_M%f-HJ@HXsKnV>#hdQ*e@x*x?y zAc5^>opPlwFse?P@YjsZDA^$S@IOO)oc+hNnU8q058dPX7~b(Eo->ly`{=A= ztqZbdt(*KX*J6WX{i9f~W7n#6*-q>f{JT1A-X(s+spjufgBVY(U3z$fYtM9}<9ugN z$e*2H=yEGU;HFy`OHJQ0^IQKJbQwJL=GL;D&o^E3(O+ht_dz9*DL!V;1f>&i1;jGD zE!eer(NvyS989N|-FN0)d*s@tqxn_p*`7}r6?clA-B!%A#>sxle+K2&BhIf>&!0Ru$t{)a03U2mWRa& z6&V3H$Abr1nrt~#m z(H)jEyhW>G7epRQ$`mt5O$}FZ{{8s(BDn?*le12*oO#zSxgON&s-m83?=Sj^ag@X(awFE?P*2bVSJuB9>l?APk+_8XN*6eSBxo@V$Rv{DnEW_ow(*>uJbtDryk8HA5s^diy!4TmB>r$AD`bH z&$e|f`;j{NYkwqHzN+KTZ?|LL|D*BY@7Z_dkJ>4G`2F%fgMcYVob>9KHpbbHOt#N@ zxqr*GTX&eh>;L0&1$CnTGc-xp`2J^T(){E8qwvG!N9Aw6KH`-=Qos3eMfSJCzcXxf z{)zr)Xg&Pl_MNUr~I`RLlkukLAnwEequ?wVQdZChs~U)|$i zdSu%*2Ro7f49!9IdGmj$zJGK1x9p$F59M!pKh{5pKj42$J@CiDbrtQ$q~0G}FZ3h) z;Pw52cEY|7<+-ANxO;r4+y6*yLwD4LFS2f@kFG5K@SnkF*N!W0J6B{`t*J6SQm3;0 z7UR#%-#m5K;y-xxGyi8ecx=Czea3symvQ_b*1w5=q~BpL{?0xyP_j#VX(GmSzKcxOM1jbAMXE>xb{g>|C?M*2k=eK^{J=gkh;E&4pB?|nD z_dj?$|2FqO&bz+@emwsc)%`8(N9TvdkK2#kXTMo-b)U|k=pUw!WQC9XldEUU;(j>w zgL|WCzub?CS5oqwrQNz-_oJ?Ed9rQ)emnk6eZQ``ZJHguWz%+szq9}D-2Y(7{SPks zlJy5U>r`r#e`J4j{#JJ7&DZ}7H?u=Nlpm14x!7U$;h;asAHI#qF5?F#KJ%Rli%RZ~yv=>yPX9KkR3@Q{(W%y!yd=?x>3B!}kO}ws+;_ zto)$5-XJA?XVf`POrH{_v z%KPs7A-vr_k$ZnQ?$K>-Xs&epE01 zBkTEZE!7XZDkeL8w6O|p{}xxWfq!-VgSY2zYlHTK1l+4TRptL?>qqH_`p5PA?fCY5 zoVW1te}?XOwypPhf2?=BY$uUb6*n<=VTJOMeKsEs?z;TPd+ptdzEi&OZkHBZ`zpCd zJUw-_^lop}kEXj~8tP8fRR3pan);vNAote$KNQ)!{t5pue`NpWrCRsOx)bL27=Oh7 z)>!>QJYK3s|KqX85BDCM_~69*eX@szDi*taoZj|N!*ls-tK-MGd#!j4;%~N{B7%hoQHo0 z{D}S*b+JB8zVklYKI^)Raxx$7J51xnY65>4KGNr{cw>fAH4&$bGiAe+=`p^B>&j`f>lLaptu!aVz_7-9L`2vge8XySio9^~t$ZY3%9;oz35MH*D+tq z(%Iem;)frtmab0ut-${H`yZ+D{|v0s{~4Of{~ePP`MY8d`|rRS=MVY^`g`Wte$;Qb z@qT!|d!PM(2B(WQu^(x&NWKcKzFX`{XZ~{%5#R?Du2pLeCYiui5j9`N};%7ADV- z_@k)K>hOo3YjyLta?7$mj=ELjYd#?}=*Rq^SpI*)t9g|}KAN6gH@$}O`};q#>His6 z%|Htv>Tb$2{AWnFZ_Z9eb$7Ms9DKl%^u zn%|Z8=IyLn_v8N*uI%AoF`w(e{GS$Lr(sdEGWX)W7lE=ZEG;?Qf3%8Ct5|{J4H>zIg6G+2|kPN5gjIHD@p1Qnhz` zZ=Is*`L4^4g0qApTz0MeShjNA&DUi{wmaCL=>L(*{?EWF_@AMv>fg~zbyxQB{asa4 z`QUxye6fA1_kV~-e271mFSll!6yN#>^}LzCu1NJCUQy$xyY8)xW%1J2=7(d=e)U`v zsMu;+@xX2O{KH`v6xh$iZ<+r`MEW1+)<^SySaKh<-?p#!;a%$^^&9J3_9<0})py)e z-0)$^bu0P)KiLa@SOTLG_QUzDb#@=?56@-$exQCcPd(Q@?Ya}?w-~SNDgG$`$j44l zO1$sUmJcT`sciTdQZHT;cKx49zW7}~lQ28Q$pIhZ7B9IQ6}xo<`y=~5Ql;?X5-uh#{<45tw{^Mbn*0-`() zQFq&>+w~^oGuVCm&(M@re=w{5LDzoyB<01Rj%DVeG%R5UAG#n>h{fkB>d2O$5&a$_quBob~1db|DoCb zM}+$y=k<^2|M)n6=>CqYkaxQHPxar)`8<0fKPc(HD|t ze)e&{~D%XjaiAB!Weef|B`?2tcy zY(=x{p2>&mKeXIg|LyFN>4$@^m{c1sym=>p{qp_~9`AqXJpUuO|3Um8!R-(855;%K z3+|JvvHGZg=zOcy{gyhby7N}&+n(?FSbucB#2wd_Cm*d1cK`6i%J)gVpz@E^w&!nM z`r%#7yJT0##jV#betD~OtAfE^!2ZFt`L`ecJ&asTlv_&y{f(Q&+N7jf3J7GDfMUeOJOfZ-@5-z`|phZ3_|K3r|HkbD_M63z%9GXE>O;|G@x z7aUy_d+Sexf5$GnrArTs2yXqIdHeRkTN^fA+|pec!u*l@A8+@6hNi~<3Ms%{TAXy+6#8|1&f_`FH>R2fz2X%>Qxw|2F)0;Xea= z&X@Qedxko`SM{6U$ZwVJ{$sgl_lx;*xqnzcM!i33XMXw1ZQC$i%{`g72aeR#Z#_|B?D1=FZ>#GyG?0s;oP0{~?(F*6K&$$7jd$)o^@ZKepETm|6U$@BMqC zlevFXKWLk{qQ-guN1+Pwuq!2?t-meNu^)1F@pb;)%Y1NY^#0cRjZ3cTl)Uw0OW>;i z5O06`IjEoJ^`GHjWPO@^Z#`@77X9A;3>^D-_*=Gt7tQ?9Ow%o??-%oMBKW@w4o;4Let}d&w z|2uylM@?XS@Avpktp6E~a@+D^ zI#+al#~$-V^*KLx@t)l`i}Cl&{|v0x|1&i8)E|`GXIy_!rY7>k?g#gezQ1|+(fXb} zz7OBGPpjYbpW#q`>!ymN$H(P4|2Qtm-Y2(r%a!yGX_LCw)CgVvWBD-KHs1Hz+OKQP z53gPE@{<2U*R6YYT)AY#__Y2*&h@vTq{uS!2jhPRmXlZOWdD@>xc-6rLHcoD`(yG# zrTVv|4nH>A{ct|l>-%C`PM@s~y7Gs=df~x!r%LrZf3eMfz**wY?fj6J>vr@!-DJOO z3$2*Xu4=o&U@yM^LHB)7*oZ%P|3^IiTf>jLkAM6T{xJJn^vAmOAOFeUK^4PAFb26xW{_gk59K^>n{Bc>i29uDy6#kmG#?u+zkA8_y5qD|3_r@ zKaRN{&>+93i8hqo`@G2H=S{dE^TGd!tne9IPLu0Hdx`#0Z9{~1!(ci!i)v-*?&A>^Oct{;sby&vrFFyR-= z*cE?RH|oA~zrzRjjyi=OdA{CWAH|lc?PR^Ta?y*6ud0^({O1 z-@^V!Wbr=^_ciqgdH%$H*nH4l;6Fq9&p(PQ>rR#1@%@PYt$MKb;mUoM74eUb{A2yi z-;}#mzCFc{KH{X>6S zP4=V3iOaQK$eC>acx?HqA6C+zYwPYQZ@ZGX>B@o`sx#Q1tGC)`?f;>=#QsC^vW>s} zVj?PxZhvFm_2c~7EGzMjye$^jA5NWqr1Kx2TEl+vyg#O9(`|AeRNud)X6yclZ?5?_ z6R}yL`xY+!u_!w9at`kbhTr}F_-_2&RR3U({Fdi!`_p*EzoqqTCd z-_!ZA@I!pehx5HAbGaW}`6m;du}AQczub?o>a+HtJ9dAp`4zT);g6hGx6XcR6`Q<1 z_Ri5v)nCpvjK6M!cK-V8f6!XK|ASNZ{g#)H-!}?ZINGOl)^H~NRyw@8BI)+yQ-2cU z+g~rcbvvqpGyI=WwY;qRN1k=-=8N~uC^>RX z{SV@A^*jDuZl_Tr_JjAgd&T5qQEqD&)CBxE`$(U!Lf+@2{=w>c-pJ^~57zTk+&(IN z*BlqLryua8x8F1RJ0&fi z*Zt$_=pS$9^F6xo$0g^VTmNtW8n+*|AGE))|Kw9_S$jPzO)#tZ*vNP1CY@}Gg#^!0wOmDl%3Cd`U&smcDxbo@}2?eru0(iKG~ z6}R@h+^6_4M*QatohMVE%ZI25aD0*;(XvC_MPYe?_MMT!Z=7 z`Jxs5N6dbyF59~<>(|-!ZCh4s(dWDUYS-Lvi}{)tIhtG-$hc)vKKab7ys4~5j~@B( z@>-L&#j|;tEp{cBJr&sPvCX|OjtWv71oD^L{|J@;^GV z3fGV3pxwJR?4PB}`F8Cia|e0;f5+~Ba6Nx3_}fm<7J)zB{~4MJmhF8g-zG2ck2Su= zW%A?ceWeQv*H`2pw3Gdj{OGLofnWa_I!b!24tZOz7nKb@a!>Wc-u|{NPj+1r4vrVh z+&+E!EZ50ru04=t|0DYSAIHp(r~h%4*F_ zlh=wXD=xi0TxYlI`noUw8DvgBsOH}qdw)rt)Q2lpt$lM>#AR+irx0<6`MCXqiT@cs z|3j<)&BI6gZ)NfK$MO7N|L{v+=+!=sA33}JGc=j@%VzDfyS~R-bjL^5yGOUp zf0WPX{;;3#Mpk%0rr57F%j0)n(RW+;Emn+wLuVSpkNMyH{xdY`{JT=WRsN62;z#m- zgoA9|56AQUQ>_uXx=-mp!=YCnzik#mT#;o?m%CLn`&|wWUdn_v;TP|7Up6UN8Bd;lYG`c0bIQR!o1? z-uBO`#_GrW%ttCesvqy?t0?+>aMqT2Y9Dg%w?F)7-&%T~H7?suxmN1cEq<{dX}`Vp zb;s}Dd{yzv@{Za`pZ#}iOW?n-{?`0|yheXl?@zsd!~E#|t>@N%y#H{1*FVJ??H|R3 zAN?O)KcFjoXxGuL&)aJZ7rf+s#4mI3;eUpX?^1tmm3MvZXUP!zwQlW7$Nvl*>l!mx z+t*?6D~Ih3@+WH<>Kf_~8vSQ@FfU$g|AXc8xAwQxr}np0ZR>p)eK*?u+MmS7 z@p3=OZ-TD zlX~@!{Q4h$zhgc+Y}s9_9(MU_uX@1t>&v%Zd-L#G^TG7OrKfoswzxix`yXlO)KZXzb`7_MrbU)7BH~*vN z*Um3*i;v6`{IGZKx25$<458h{}6Z+wN%&PrxJ@@OX^UYuFJEG3_+mtVPaVu`?+FKDXtjat7Xqfc# zXKbtb*S1~i@3rNnx@X@$`BqkCdgWqK*AGx)6e(#>rg735DH~z7H;46RV+C01ZjtiA4QJFl7Wsz2gy z+oN3nAbLH2#bc+&QttKK72SuzfBk3ZnkBsI#V5O!5AXW>ePn%O)s$KFb?w=C^X;bf z)-ZnCe#HKV$?`a&7g>V@S)C;MO7r&G?d)puTcSw~rFRqv# zd+AM$_411TL(e@fmB`8;`6oW@bKLH$c`VmwUAlQQbNbrEsu1QQHr)RiSUmnND!ukNB0SQ*necN`H@{sKe`{)gjKb7)JQJ>W4ZB1x%$x;b}BDtMeMw|l{e~s zo%5lOL|ff0*9)Wv*fAiZ8wHPX3Y=@2&@3HeD;F>-O((_||KQ zJ6c{`|2BPA_LQbiPnh4Y7x~X{aACa69{K+aEWfYTXU%o~HsgZY-vxi{KZHGh9Jl+o zx%%Pl{f-+pUfQE)le*@In0`0s<7MJ@hL=qu(l%8$KUntR>$83RiJ7&kYrf6eb3k7B zKf^)6`Um&!i~eUg7{5=xCichi)pZxP&ENd|(E4^eqxiO4c1!a=JpLyZ-cb|#ct(k= z_K{m>1T%#zo*y);QfpoKQcmH+=Q~-K?`M@uwf+_gS_#}{U2Q7`)1Am zBc}HGAIHD_f_oI%kJwrtU7K6|v2@GrZ6CJO^BzB}VP|^yBO5ZXZJzDF zS_8 zWBwn}?}zS-?^Da#FR!yd{eEja*B|%g6+*YeyAL=&3h%T@e4M>k?Q;B;H9974AGbHY z-Lh8u@GZZU3;kDQmrJ!ij*ZDSv&wCo^PX*CF$4SgSBphx``3N(WVn)at>k^1U|eimh1$JeWz&y^ zO{xg@`)GQ1$+dlIQj^aHGcLqkM}^@#+kz@0}&Qx61#h{j=SE{g36k%sRo322%aEO-`0n|CP;Ir;`>N zckdAc|H^%W^#_;4OYPIHKWI^-|2yqg-4!|6%XTI|K0hd(4%%N@cka49ugYx4-5;)P ze;Dzn_+dWht9sG%eG}L2iGHNdbAI`mNE>Fg$xE`QH@ocHv141?`>+n?2mcv);hlpzXo;d5>Ki;PQw*RB_!+Lc_oyhKwK6Z@qLN?Bi@@2ATr5@W;K3f{NYLYt+n?it4(}3m;K0gtLZI| zu5RUgaOF$brL|jU&);%8ZU_6-{U4(KGyE{OJ{teQQ@?RO&wqxDe*S+Kr_J9YfB3vu z#ka!`<9os$Kb%}+biGFQv2XO@SRT1@F^%i?U0nJ~D)dZL z*4=|E*R9wdz)&0c+qVATB6*oQ+xmlcdt`q{M*q7aC;s95vH4x`>}%{y{uuvYt^RQC z(fh;Y`{X~Gw=et{-?YVEwZmb!V-MV|ig3 zs?H8-1y5zT)1{g2RX|Bvfi_oP2M-?M9bQ~Hn4kC|I`zvrz8kNY6s^Z2UT^y5Z@}kzeV${NCNeh3Uo&wT1r~SeO21XwI)m|Ifg(;P3oB z?7zK!)PI|H@jpX`efzKLeOv0=<*YVMf7pBe(B1q)aVj76rmxMa;#<8q^7!L;`Ioou zx8}zm{xxl8$@f|N`d7c)z9~~TKYeY~y;tH^8thl$ofXX$yqS8`Gx>ssdN{D@6_bib?Yp6$kZ{#X0VKU{q` zb4}de?t{1X*a=&$ytFlR-gloXy*t>Swf_+e|Ho1Kxc-Os`6KW9?n&?baXR?}_ksG& zQ|GrPeth1Q{-N;W_YQp<-^bba#co!lAC{B+wv9X=W-@ge`Huio>A*){stYLr*} z*tulGMbp0=>V1C-`8#cFi`OlY(mi4>l<{lceBE#JiRlzh&Q5#Hi+-5hKPE5ucAsL# zrHpbrlZ{=6gL=Q*XV3P2xI5NKu z&k*oqZSn{1gYlbv=QXk)trzou$ov?zxxHfhQLE+MCjHzM)~+A6$SHl8yXE@zq6*<- z!mp&ZKHHm{yX@AB&07q!%*(fZRAATrT>`2y?JWMCkpB?;pP?oGgKs>4jmwAaN8?5Q zUHJS*bI%X?k8;`4`&nP!+TOlTr{Z|@2eqjye_VIZo-gG6FnT`UkJ*RzY4#s6h{%dl z4P5-<<7=yu>ALlN(;3+B-~SLU|HF*=5&Iuuy7Um ziBE)cK?Vj ze{@#uvCF2bU*{@2@A`PZPvPUSZu|E0ol7p&-IuA7Z~5b>*S_R#bnKnOE9188%5jjN zTmQkm|A%7z(fL2L*1zTa`2C^p_ak@hABZ>Cr}1yh{Ldh@C%y7v{*Q>LbJHK47s&pn zalyv;!Cm&l0YBOgmCaiFZ*B2|c&V6}Y@v#_W0!rJ9WocU{qjCtk-UaLHxaa=3V!Pz0(iacXTfDe9&E6{ZFjCPyXTDmN@+nZ*E=h zoP20|m3q&Eom;NWx*vV@%ePC@WV7zRP1^bI%oc{f%>NmhHr5|pFK7Lqp(*p>BlZdbcgo2dZb-m#Bdj6-hYOJtLlaJKUfgIVSihFj&J(6YfR&1D(2ljwoB!n?3Rywn-|}yopjsf!|{%M zoh_AXP1(9$J$qWRi{W4BKlK``kLHK;JMP*32!HHdq4OvC!}ViznjiSPb=Kx3@7ccj zqki+&)9?ASr(OGAFq!x6mZ*92i?&^#Fq!uXljY-|DGUtpKg>VuKWZ<%Pv?*E57P$~ z3cEheKiV%}VgGP{%iDE%Lf7uq3spQm5|!Up;<0>3!6EMQD_gx23oYCYqJM5SvMJm( zf&Z28Kd#;X8Q6AIy#L3!{Wt$>`)zKse}`qrO9ek%`=8J!VIRWw*i=5w{NpJ)y|*TCVMNUB>|OC~X?ri{>up_;()Q|3#&ia;{|pbN z)qlvke`Ng+jrni97uQ|Cr}}qgiGE}AqsEUl_gVgM%O86APf<4C^TV>8YkxSrzGrpa zz0XEdzjbZteUXa#@XeRrn%osXGF|MJ>Yg}-v}w6sym|)wPk%pNfAGA-zw`Ezby_tX zKRS8WZtwrcTjTn1`JwaDw<6Bzy~+6#|A1fM{*UM*)3$|oiRSA(yjHUN%H};Gz4-(R9 zZPveQ_M84Qyy1V;e_J@}$JY{R?$-^sMoz4nK5Y4=(8@XeDC$4On*TX?B-@siiGW^bD`tAo9z{-C`5gFEqk@*hIi zH~zEvyKvEu=MVXBetx*E@v*M|!~2cvZ2TYiCLfPF-@hk(Vdl2-Uw@BC#m#>t&!gA= z@UGh9kjq=PTvge7W&KuOkE{!~U%WC|DfNQ+@%@JS53cKP)j!_T^e z;C=UfoiZt%mcVM9J&F{*OdwY>m$TTXj6-($@R@u|I;nd!p`T z7iCWqyDn}O#kstN`BMFZE%qOrW77FUaW)_R zGqio>XU&>b-uXveuD^KAkK`ls_^z1F7n0Qr-*8=LtFCy!_K#{y^H)VWuWau-^xV(v zJA-`1zYFyrGMB&2{cTfsAWr}9Ce!~6efE4`=Xcuk{1c0-N%^tzw`IhSn|J~q$l;+9_Z z=<9D4FMPQ#R1sL>&z~Vyr*t8C@AdWh`g-a6_f0x<-gU;!^I8lwt3SN|$Fcl(=#S_B zxVEqU&u~ypzf_*1!uxT2>z?3Mw|;Ct9<}uB-Tw@YTjsY*#Wy|tF;)I3xAM}%W=mIQ z-c$QgSi9&)`ynZo4KHu4{kHItFZbbHpDVIH`TEuBFkiQSu=zj32gmz8`#-p^tP`x5 zeI#CdkM_s=56zGM+}g|S`SD!y!&vVFyXLq5$$$7e!c_Y_PsQXzyQb}YG$U8(s>$lM zD>as@ZvB|`BkSa{*B?$Ve|aghQ1=7--TgxK4=&BW^`GJ4^f&D5{$2a^pW#@1d-G%d zrhP)MeH19iOZA`S?G{4=48Ryjl|Y(4u5z z>1#>QSV`ME_pMj*80=jBGc+03AKVKXhFf39_+#nEYL6d}AIdk_bJfXKZ2Tv4v!XlZ zL*4SnZ?lii7kFF3D}7XITL1DN#r_|~Ta!0m{1PjEScD~(tER` zcP29YobU9Xp-I2)O4a^9qE)~DacZh9e<&T_8sjwjUUv3d${gl5vF`tPwg0YvQ~zN8=YKacKdwF?K40|F4~shU z`8Uh%AF8@}?a7BDc5?L+cPgUpKa&4;`$o(kS2?lpoy~_g{;>aTl`rV1-gdm>Q9`LI z!;j1VI6y5@_8(XOGq6^DxPCOhbC&wCcti#j)Z27S_j5-dUh<>d@oGt~`;n?|iywJMzD@ne)|kE|W7h3EuYz7G$Z^d7;KqNe zp#EUE-hYM%lcvr3&(OH;pLt}(FaQ((GB{Z8o0e%PZQBhj4!D zm3npUxA*0^Xh#F~t8xPW8Jd0eXX{IV?glWKf7{>p$L9~%|B0n{?&G@gr}o43H`i?R z*PJe0`9nVB!8zC%vsL+|&#cYpjpGKXAV`}N>M zf4(2V{vX*}Z=P#ja4&P#{?4to?y+0j4%t>8{m)wf%@n<*dUw{VsMIUj zwaa2=s$KjVUHwX0V?n7^O@$GP!81G{M5-S`ha>u-LK`aAQV(2W|E zkN+7CvtKp+&md62cm2`!HXGk{S@GPZ?}cNe|EAS*`9EAQU124?SL}Ou_1v<@hvzAD zhiY-jEOKQ(hL4UA`+OK@PNk=Inol=GOlVxuEm=4%$o9 zov&vz$#4E=|08+*>s{-6YK)isiT<#?vvl?R_IXCva@M}x8XJ5#e4gmX?ak>sE^n>O zS+nk|Er0a%zzyObJ!_ZoTHJj#?bZtRllM2r{}G-1ZTH`4`}6)YfCgh-_OL%%D}N+j z{-4t8d-m7r^gmiFJ3{(SJBx>e7v@UwShaXIVvj zc>AsS;r7d^E*IBcYOt>Vp{4#u%=_D#{|rr=etdtpyxTq_;t%J8_xv^TmrJ|t+wJ-O z3B9O#*88xQ>GzS%nUj9ZJ`!&HWA@QH&5vSRAK4a`@G_ly9&p9<+O{nlF6F%ave)il z>ziE->^JXk3I8MW`#%G#?BAvPlYPU#nS4BRpL2~#zGQ_;jm3|}ANvp2@n8FLzx%$> z9^v9-vAOGB+`9YsdiPg(_RPr@`;Mw^__%K6!~Lu;rpGC5+`WHWlP#C7(>rs9KRf@n z)PROvetpQ>`tel$4-NK1`n~dEUwo4v)OT&EZxox)ZsX40zRpJfi$-G zt>mup@jCIXSHE=cRX!qBb8CBxP3ptkDv|cc59K{3(bj+E9tVE-w=8kXj*^X6O?Lma z58QBhYpvd|>j9T@WgYA`{AXxdQGYP~KSNgk5AE_!JDFGZTV$sn{?EW#;r!5c-bVSx zI;lSz7xy?W`Qh((b&t=l#E;n8RHOWeJHPv% z)sO1(C3>x^UR0gE^Urw42Ose`jf;QYDj(hTZu*0@_QBV-{9b*>cFynyX8N_gUR>#_bJq<{19FGNBu|Vhr`GBDeU~X;Y0bcc>W)Yqc?n5-@oolud&na zkKt{4Hp-9nSznbM558RDD|J8O;<~TgYxTa_>YaSlcI{(c-ns_6tRJ9jmJ5`XGX}x;W*WSpgiiv7&sCxcKH2fcT{O{2EgVCk_tsnkWUjD=S zAfNG%d~8-eYmLQ^$fZB}eLmc&y5EtssX6Rp*>3%=J<*Te=Er{2*;ZzDD|OE0S06He zh0WwWVwKw?F12rEX#wL`^S=ELp1=R0Y2N>zA#wHj8`)9wA2M7ho!`3lgZq*D(z$yS z*U#^*Q~MEZ-_GCsRaZLn>iRwLwGXae-};|{d!~tR^|7#foSUz|*VS8}mpJcw%nqwd z4Hf2pgs%VNz6f5crEVktQT}23fwSfh(~sD<$_wssH1B=4hxudu;eTpZOU|-C;<;ru z_uuI`|QKcKaBqwI8xWQ=`Y{8wf{ib zecrVH46UWP50~vK_h(;HRg|?c-euNChA;X*G^hU&3;uTgKSR^n*guy2-)3FkBcA-w zzEyjr|AYPQ_jzm#%OA?K-O8SIb?@4B8z1$$oe#QhB)&|A<`vc6jsO-R^%xy5zgc znZIqav(A@4`*`BgmnWCS#GhQC-<KF-@VA@)Gq6UrKfKrf+c*0k`{f$p54&Eyj`*9CLLU0Z9TkHp`6R%>La z`?s+C4Zrw~eO&qN_8-=Y?OgePf-CpP=RbB{yKI7TKDWs{neHR} z4~rjjnDn@^cPd}u%9_AX^Y=dk%f6?uTcS-9L)1n_pVIJmy|Rw!zKUQ59P@T`PIL zaoJ0AsSNh+*Z&zl1fRdHeMElyiT?~uQ|{EnT+y}9dYvyB@#DDP$7@rUzN`wHpRy~w zZSe=U`42Vr7-#4I?W^Jb&%hz0`9RkBuz28o*VR9)qVK9l?Y_Ev|Ni~E8Gf<<;|l-J zz`F16_WukIR?U<7QT<^3t@!v4Zj&GAx80gEPvv5re2vD3t?!%v$k#u%G&xyP|HHS& zF?VG}_mNx6wm-5kIepgO|I+gvSCuZ8Xv+qNb$eXWU3zsZ=QD{5P@>_sjhdlW*JQe%SPf@x${SlYb~|KenFhk5<2D|KYNI+#kO8EWEC8Nq4Qu zeeR>;?IpcY?+<<7r+leohHk>9YeK(H9S_~g@bmaT&V~OOSo{BO+W%n6tn2;n{xhWU z)*t-7PvXby{H5|zHC8_sKb*Z^tU^5O>K2Xs#EW}e^Ot8X~Jz&??Tz{|Y zx_yhYUdnCGU3oA!Ju`>#shvIOTCuw8{~3h-GvsOe|Ik?8X{R0U{yX@^KK+X}sSo?b zU(O1fpS}1;UOoTqwQcHhX#{)gAM{kproyJS6UjrNDy^DQQ^>Br4> z*)3e?`nX@nY~lJJ)5ETq-j3A|zm`+7x}r2YR@ig#n)%P>N99NB!_)0FKk)4@e)#+ff7>31Uv;zB{HTt) z{Kt8r-o`bD6IZ;n`rB+eZFT+fSDGttpUnRjz;FFyCurf_-*x*;>uy`s{}ItXCNKQ% z?SF>MzrsiNNqn5%W)r&L<+WXD-4AZBXZ<@(pX*2Up?z}YyDzWVqr5co#=(qRx^JRd zq?U+VO}H)|y6ehT@3S7vFXMk`^8XP}{`U4iL(}3vmLJ3q>EAMs`cb;L#`J^O-t5N` zf0RG0KUn<3+vj6^pN`30e~-(5q8}Z*YI>uq z+Dtf`!}#j{=J$VO`u{Vq+W(#TpCS3Q{Efp8mmk^B{-5Cx|HJd#@fG3^|1%t^7x>}! zI`YTG4~Gwymwk@9`J?{8K9l)J>~CHRyC=PQN3Qk3=~mm@w)Q$7yp_9ce)@uYQMWU9 zx$c|7_$mL7aQ#27%fBP)59<6={PF&g_?zSFe&hvz^!L29CBD;+>qqwj|402SKTh2~ zJl$&Q{nmU_Z)@Llw`+;l_81nf%?^4oYunT{FQL*AHQ>DzjSG z&3t&D-*3A(=aTz&t4ga+-}TBDip{=%+iKwq=2!kdH2VLDp9b&nV>SEX`cS^Hev5nO zKe?CkJT|7QGw-Qgtug+v{GhlTck}T$ol83F-fX#Be(ZDXkNzXVS?5!>UoS7anN<=$UEJdV8?>CeGl{@ValNYsA_*1xI!&9(WP>5t%#><8sPtlwfQ^`GJJeEvWA zkMdb%%OCAObe}&fKBLkkp6SQTud8d7xP6VHPjCP5=APn*`yK0TtanL#S-EiQwoO|% zG5l2i$9efb18e->we=6C?-#Sl{+RqN?W0|5apgySkF6K>sr>MM6xUwu1QBb@zkZEAT}PQLuj_J{W4^-T4Lc3xbw z^{xHG_nm*@AGlnvQ~smA>7(wmg{!i{>KH#Vy*AtBb^F${(lD6(7)^UKbTQ3u_yjx_qXJa(`HQn&mg$v>cxMmKdc|I zO)h^lpP|Ni^R_F?ua@lIFY=@6e$*ws^y?+wx6iu8^2c8;?KO_M6dZYdt!`;x^17^T z*ApcFGi($8BP#xF=ieRmS?BrcZq`ffspVaNY(7J5gXpbMjISwdQ?UE-P)$xi&GxR&$z$2{-}TREc3VF9}Yiy{H6}^f1*F?JMFz1AM`OIw#e7td*`lerrtB3Uddaq*Yytc6 z`0jYFCx6%0DgR0P+y6)T(fo$_oNND?X6+ZKPhZbEz7y8S?2H_HS>~EspK>*x$tMvGs16wd$332kgD-^ZI{S`2XWl{?EY5{dcy>`J3^N zuXokSM%EaZ%SrA2c>Is{e#U;zAI*>UUT@KVQ<~3z{v&rlZg%j7oo&~nFK>A-cy>ue zu;+)me*2D+wZ;M2{(7g*UAw__{Ehj426ln^gYNYY=ErYNTl`z+5BJCEhu?ZXToUx7 z>Gc8m51VW_YGT*dDDL|h`}f`YN1Xp;v*K^hs-3%a?!CP$;wz?_$O~uuQ@x<{QEusq zR~s_7y{}z)cx6LfT8+$)y&oRE{mz%qS5p0l@>};QUyY8rZlm~6ypLte#;d04 zSH4aY4;S|=Us0d7Z1XzT#m?1yy<)r9x-WNM8Pibp{jKK5>Tl{FufI9|Te5=tTjk%` zf5Jb^{?_v&@nO77CTM5h(Y4t}{nxyhFTKZeRhE>@JfW*5yry4k-QR4?yl^vnd*5Nt z4>K<%U3>W9)~z3tc+Gfg8nWt+&zG!Ev~QcwUZ2y?^Y2>yX7=tno0^CpQ$Hly82?uJ zaro|qT+i@ zeWMOAKg+-QpW&!IhmG{d;}6BV?QH(oU)iVmNA$z$FMQT_JY25b4-%E$L_UVr32!>#Pc z{QdSR_giKiKXCr${Uh`Ac3!a)tI@gsHT>$=dd?pOw{AIq%f5L(o(G5NdtpLG49 zWA0k zf1~@c`Ww;@^AEqj_1^98#{FshQh(|{{%1In-)Pm|`kx`yU#uej=%pW3haVh2=Badb z(e{t}ohB=4oF6{9QgV9s?(_UvuOG=&O#AHQ&v~z+j-NGKY@g+oQd<`0C;Tm-iY}kG zCizF~qx$YRo-1|=HA)};GaQL$c|Bj)PCj$i;ulr(d+X$0#wlIa-;@53*FLKC@pR8U zALZK@T-j3X>!-PJ$EInYtXI5Cm}O|iP<8(8+K=qtGJY)n9b6Otqw%-gABF37EOlq+ zDSZ^3`ux!OmV0VfZiijlqrKR1VWwGCPx*J=wLfORdAp{f>2~<_MVEq)%=%*`x^J~u zckY__+Y!zM59AJh?0#(iM*flijqGpnKm0#T{+9Hyz41Rou0GR`poAKA~Db!*;w`{t5d_QSsG<8PlW%}*|0aNJ~bMe!l;hxI?)*1dVN^kcxs+uG>53*Y#$b^d&d4qRbZ7FT5A`a+ceAj>}tn)k7|SeJky9t>kRjq0c^UGnik@ z|Do0XN6h%!vi}TC+kTvX_`ah)!#kg`!u!yDPMtlD=^yStTz=@RaKyzL^#%Tqj$cao zXtuS~yyK7SqqX%>*S~gr;pcyO`IY#s^EV!BxTqWMbE#yb?Hyx=+S$Jy|1%sszyCv! z{;lwDM%V0<<>k!}zLw=aR>#`^aQTt{4Bh`^KD2kdsnh%8@ge%A>+-B$Y1?ayA4i{O z&z$^YyRYY$_tNn{st?{uJ>IeA{Z|epE1&Yo$_$nKe}uXJ@iG71y#K+}`J8|9KiEH5 z*7;%aBlW}l5c`;)Kek8uGygarxN*znt-9%lrqAQ~c)d|S=zQq*%Uh0J zGm#B?9B?`R-5aI74VjznWNFqhe!73K|A)5z+s+Tx-+o*JUFuLL^>N?Kb#<5S>3?MJ z+`3-&{*TqJ+3(qZJofw8;&eIgLw?5|^QBo<%G;aHwyv9+x+LS4-Qvam3!`8B(zsMA z?Y&lw`y_*{@IS8Q{~1{4{9W~*;lbK_8b69x{<|Pn$M~VX`Jdp;*B5H+f8>7H?Qz|t zpDVNco`#gykM1K@lYh)UGEeV%>3V^z%!QfLF2==Yz2}>l%(AV@b?uU>wpIBJc6)xD z{m;N|^Pl0M`#|A^oJcIrPv zQ(%q#kIoO%AJp}KSpG=;(0lnG&Hscg_K1G$K73E(T277p<4Tj)u}Z&>%8Bg#P~Tf4 zSf8o4cKOR)K2i6kR;w*so>i0?W-j2q^p5VG>j&h`D#ZVB-Ta+e1L{Y2#MdA6-*R8} z<*oN`-Pcsij=Em@*8ixLjMehyt$UT@KWy2@y8DCb^ERBIMql6pS9-H%c(J-S}wzs~>Bdhs8d)55p(j8m=YMs;*_P)I4 z-Bw+s{^ z{wO}?XMY`4w|K>iUw)m-v%}2IclGtJxScU8ikm~NDSGAsyXuSe4_5zY*d_+*@QY0U z$JNQ+v!%NK*0T4;8$*Zp9A%-8tHH~;Xj`d%HA&JoOhmo zD9+$R+V;fN=M!b8&0pj2Ywgj#&B@DOzBV!NUmoSNKu*&9k7)h3gY`c&TK+S92(hS1 zTzISQ>i0O~?xWYOuC8m?P+|4xl0N&v4-ahw3m;azXFB^J^haiP_J^-uR_ezG?)q@Y z=eyMIY~R0Y9PFC@Gc+yx&v3B3K4Ox_#f}lD~4Nk3@alQzmY1 z)pBb}gSBCB`-k%HeBTcX{xh@``+RKK$6hZKee*cqlIbrWWi0;4#87kgKLg9^{|rrc z{#}#*;OS|nnkmneV<-RP^TX#yD@*?RAC2eTr;{0{bj`+`y{n|#GRNe-L`7LX_m9;F zZ(VDC#GI;9Cbec`*7T^loy#L%PKbZG=&|Qzy*CH!Wd3fq&pF>4zy1AN<&W&&mi%2{ zI@dn@$Mlav&;9;g@a6ZGl5hJGEAMn^%@%&%ALWOm^l!a?a@izS*!gl!j_!k9S4@4K z9z`s_Vrm;5d__Mm?+)`*R=0$sZcdNOm6g>Q9$PBgda%oWbP(!}JP{`N#Lg>mvJSJB z(Bd+$_qhuj4jY|V<=N%Xz$@V88M{Mag0s=eAPGI`LTSIpJu<>4Wtbe8x+^_`w-&rP zwdAD4X}t^XdWNT0uskqS(NvLPu=4iqm^sa3W+Z1A%Sy%RZOklBR-W1rGqoUa!+{2d z&RpxMT+PpUPDNf6=Zf@9D+~3XDZ%@+$bs2Xk|BShUSwI#>D>jap0P_b?=~N}b85jH zwj@zcn*io(*UFAvVb@cZsQ6iw%*cNBbXv%%ukv;dpPt-cFkN~okC(ftoo5GY;{$;Z zq5H~4OXk$QbBkDVTUl!Y$302qV_%PX>lH0w3vLSvnIKW%HHm}8%;3NTcHKSaZig8~ zKRfi)QPq!&!9!P3g|#R3&L);Hy#)=HYx6_{&-iY0I@Ka@w5uhJK~v>{$KIXRQ&J}$ zYEX_k*XD3>&e8p64u;(ba+GGf<*zui;#Ekyq?1&G|v}GpBB_usSQUOk#V~T(qv-LVJtQ=?N@mmm55LH!Sp2W?^_> zaFV6*(}XkpS{ynIN)1v8Gg4;UV6~2z$7B|fTU7TrRbK122gj2hZ2^X5_d<__R&rHL zGOA0x!^n7&$L_V}VU?MkhfZ)^VD4RdV8YR-PY-H2Z8G55#L;nQO}O<C3ejk zi3g&o2_6X^ER$~DbyRH2kh2VzUR=Q3drBs?c;Y94C-q!4XIq~&ux;AqB4i@Oam&3k zrhnp+Uguq_OOm=yMYFW>>^$b?!13nYvU)uYm1(}c>eD6e{T9e8-sqXLs_D#*JF6rn zFim0*h&p%boUq1Xo!gU{tv@V(_WNn_Bv=1^7nRS599epRIqqJ@?d)SA5BXIXI~_94 z755lzPMd5jqLC+|8DujDhe zoH;+FXV-52fHgfkB~SD4Ok!ln+%`A7+AY+B)mxPH^V12+EX~Tf)6}&ZXC_aw<%nZ+jX&UOJg&Zj|+No-JOJswJHp3Z42Dw8Yl`?O!e+mm^oRXXI z=kbm;idUu@ZCdRyu_^YETv4+ipMzk|y17j%d~fb$hWc10a_v6lcBx3&C9TMK3t!>H z2^{~<+dbG9JWcaiF0=0D)1S9bl{bjj{yw{dIsNRM0Oonu9!gGJY&|{g0oQiD-BWm% zG!=etP+7?uoz0}d%1&Y2 z7#ZuUm&2;Ll^pc#NktyzdM(>t7FxhVTIF(;A=u~7; z_*6;jMs^7Wq2(3{MmrZU=BAhGbnbOl66)zOKmF`yVegt^m3cl31yb*<6A?Pa!93~Q zGq;;EI&UJDUC}yT5SAmrdEi#x!TwV&MFJkZ-x>^}&m0hxd?IArcpz!Xg9&w?CVpjW zP^s45(UKNjsVsD8yWmXmgbJC5sX;eb`M8z=UQopWrZTtsExB{ql7o!#0E zcJ8+r4rUj4t>`>$cxZ)Wn)RQJTOD-{E;*BRK(vzCZON{w49rpITG%_MYpvlLX|zvyWuFq$wZl@j)EQ`j3?7eb-G0Y zJXQK$OX(z6rYAYAdaELI;{o#tttz8Q4D(ZWeCCM@<_vk8(Z|wtd)t&06{8*Yl^i?^ zj19QfI55rjZtYbye5ewhqIyBh#oGJr-5rWGi!2#aN=te+FJ@r4zd0u7v(54wf-&1H zINolQn0N5DqH*sJ)gnd(Mxk4ke9E17rVBPS7n}5c4|IC6+3-NXf_G`IG1CJaM0L|A zZc|CUEqQ3F$|nbA_Gf8@PZzC=e45(H?D1j=W89@@CY~xy9+zD8g{`JU2G~!FQ@Zh4 zxc>BBb3I3f4+RWIo5FcQiv&aGybBPFoG3bD8lz*TK*|ID*%lnX9n{RslalkFSk8cl%HPBHO_r=PwHJ`*Y9a>}#hZE!NXz^JosiO`G(l`b5Y&OS}@ z*Hh7v3*(6BxUe#O@;habu!Izc1H59JXZ*P^fA_h&w;w;;@zdk(gu~i9Jy^@t9+XoxbpRoFbUY%uT?wmqXd9{r;#_N`^%rxE@(x#$s!q9f^xy7eW zdws4G$8|Szw6X{Kgcw z`IsdkeTT(!c4o^w!#O<-KHJK5cjvfnJNeXrr)BjM{uykimPOQkcw{3`{-BG&G1Kzy zC&n!keoivI;o#)_u5dH!)w-Oz4?p8yGX^k9b~%VlwWw)%lDa|UgYi7)b(eb#En+4( zFjU?4yWMo;$Rgtnj}-$Ro->|5`Mt%lK*lmg#w-T^&1YM4=U65z+0@{bd-tTp&vKs0 z$K&5^W01P&(b}xY>KVo^J!K_BTioKwjT6sXoa18*4!|O>$7$;9MxWT|;v^kffh{5Iok4(ao2?ht6mRlq^O0onn_ilM)Z+_Y??(OWx zAoB;p@+|XB9eFO+uW_}W>F}&U;PyH5y0`lrxJy)+jCV6USs{^hSV1#PMO#yW>Gql5 zT`Lrw#d2S6f11$F6P;;2(}7`9`7%bnoeXy8%{&Ar_tnLixgAtgSgF|Hw8rBcvsyu{ zBZJhvR13q&pEjp@?YPw?JG*F_w3EQib6gYL93>gzH{Hn#6*M;1`xX{2+cX)@G0G(KEVpFb!7!;ng6;U-zw^i;nSbhj#b4qFairA2Uqk>{ZXZw&=uZtcZ$ zoQ^YyNye<?u@@03c1v&$Q%Q(Qk&xAQa@-1gaB)bd`^X=#MPF+Z-!7mDs)S1vQ> zY2eN^E)Xr=vW;gW^OkfQadu^PBhEKHJpm zU;Z<2-Kw$P{xNL!M=5!M=-an$_hjDsQt-#gS4foMsr`eS{~5&pGaQuplK(@szhnBN zbDMwbWYo**l=63$+i%PKyU0|&ZQ4Gi9T%jJ&U*hx(fnbZlK=CVKkD^u<#ECrHdy8E z|HkpLt76irm+Bt{CO&8Q13E}UCj0&eFK_=Jdhu_SSNw6``6GE*-I?!F>yM}WZA$s^ zShaWi;n%)JuOEv|%>FP}Z^_4#zIxY>^nR%QdRw2-yyuqXxrH`j6Me;dmR&g0AnX4} z)_P6-L6g$@+uy(C{AXyZ+4rNgc-`%c=>`98i|^mOH99usayw-X#k$Gr{9v+8i#QRX+hAY%pW(rdI>Y}AP0I202SrVH=Qsb_|E9LK zy86L>F8w#|%l=4BKOp?aY1_xO)7C%yDDdIw^tPTl?MrV>&I@6M#y3})%zd&zd%C-EYz5xEC^M5G+ zXXvPZFlATyAMyVTkC$zJyzNKwl7A=8Ka%f1`&%^bPsDVO4L{&>0)@8lh*64A}Wt*C7yN(_@@<3i4 zw6pHke}+Fo^F9AI+Zn1Ko16VD_R@cb%#x4Gd-DI;tv`PIe7o7Kr^gR2*?4*F9!cGn z{|th6b!R`!UD|u}kJY7&zFoQdJKvXzd`!``z1H@bq0Z_*!$BPz$Nvm0GcMadc(}h; zYJcClI+@J-8>h^(|GP-~o5P+T3-*a_`=Olpc&AM~OFj3qtNlF;AHL3Sc=BQ5#|=Nc zemyC;6P0cJ$I$oNtUJmH?EC&R9K18@{U71Qp???Fr=G6A+4TCs*Z&O1SM$mrJNer` z?hntE8vD{^^@rs2Yb5H#GUZI`x$byBEcy6#qP#$!?&6YdAAPD=pUt|blD$^1p39j5 zbiSO(?ql~qI9Ap_X!~cj{YQB9Z_W8Eul6&3<&MAc-T6mt=>x-$%dS73QYYJ0->!7( z(1o)f)lMI?GVk{5)j!sLy=HBgE=R>uo63TFDNPTUAI|?_a{S2s58iW)zuEp>VDg`# zFY5T4(@X0vul?wJWcz)g$A1^R-zRnR)%W>Kx;2UqML#?}-!}gd&xXT=AI@F+42$Uf7Ga(evZGR*IW8U`7S1RCdwtsMi|JLV!oXH=Azj^(5`f&d(@khts-2I^bZSIvi zt=CcgJvIKIX}^1{A7tP6+3|Tl%;&sa;q3gOylYQ&<&s-j`{XZ|O2>;WTlpYY@7w#} z+^lc8ald_cY`mhQ6u^Hv{-*c0F3|C)75V@8JU&i89{t*$u_FB&&z>JMKeBy)@NNC0 zy;{fkTjE51%q)E9vf-kglKZy_eg5g*QLA_Q!bL>HK9; zFYkZUWcU^TkMF_X<@FEd#BZ_hv`_i?Tm6sY5BZPtA75`5K7RkhRy)l<>>uTu|D-N( z{cx{3pLae-MfA}+iw~x6LFKIT+S8AO*Jeej&6?G>a>b9dQ?i-)e$)B3=I2&0{to>e z`tM%-2S?xRe;l!o=eN!0t5f)QM!ZgV|KBP3e+)lZf6Lgk_k->8x7-i;`^4?or~Cg% zx7+yPx}U+1&kjGDANqGrdKtC9$4=v3WEzquDAP?POti5?{!6a-=x~C`vLnlURgBTf8GJ+&%qDv-}3)w zV3~9IKf{Bkf72gN36tmFV*f+^%zp;)8t=jfz1h*1*69A@dvRYV{fB$ulIR+*$%o6W zQmV?Pe^~eV@$FZ2+8-{(+=zTx#ky_b!}%hyw_n~0n_Mm1!G1ITL%I7~@Ba)ecmFOj z`Ok3E@A|ihAJac*KX!jB`tWY{(aROLAL$?H7yhAt+{$d~hqhd`YxjLV?C+_`eUN=O z+~vww>;1we;{RkXs9eq4s>^on^pVN2FTWYQHfwsr{Eh!n`rG^e8CV4O{%2?sezl+b zkL5@1Zy9zhJO568_ThN5eVY8+_bhk*UEC#qEbK?UO`L4@`L>TS^E=GyMYink_|K4b zWov!srpV|h&(*Hwj}AXt>vYIFd-ekU)Am11CWH4UWd1uJ&$U1EuJpHAKUV&>|0Dc& z>DGGr+pna;`~F$}*nH5Aqxon(bMT?4*saT3Yos5`%f@H!@)641r`LWo{GZ~7rESYo zSG@Z6>)7O)TvP6Bqml*u=Y6@qb^K>&3Oe|of!*RxP33&=ADQBRgs=Y-caD4U>-aa- zSv6AIel)5d%-9nDBT}fM;FjZ(s6XcMw;aFn%QzO4JY43O@~ggc*=DJvdoB$24)zb; z+230Kk8|N~ryBFW)4$~3oct*L&Cid!zfJq_y#MRm@Tiaf84k#cRahV2Ro`C6apy;P z*p@3c>W|J#{tynjvPF9L-S}ha2Y=0PudytxEnf9v+Ah1zIs%#3!oSPkQjlM{Pv-A# z`v=V{|1+@2ZT^wE`j5xonNL1!k>A8}PwZNKTFCW3yt%(!V*cd(XK2m4R<-G+Jnzzp zHI@Gvxb=g7%-4OlGWDZuj?&gxr=K<9%dagf?KfXTdXRDC+x^BmRGh_0>;)nihKi(hPZspgz?3G!Z zhSk>ZonN?Pz0L=G^qsc);qLt#UxodXpJZ#p{Aj&g-NpSMJfr9Tu)K8n@zZ0|KU@y~ z&(KtQ@ME~k&g-SN?1%E^v#+ny>~Av6NPkpZk;SrM%auLs%j+(^Pkh`jkS%5=E+>0E zcgx%8V-sKgQ@x-+Cp$rY@%|5G{crRCGq4=_yI>!4-Pu{}ZytUO{#N_r@o(RZcpe0&qf^Fygd`;m0Ka7FsDI-L)y?^Z9Ka=j*0y;09b_o1zGz~rFIsu#Y_ z4b!&f&R#upJ%hdT{_V(7l@!9voe}-fFH@Cfy z`e0O3_L2Q9WA2Ar(*Ba3$seA*oc@Ttqo#7*pRnI~OCP@PESqI}bywV-m-E#6qb_f$ zww}G#fcca0KVI>_EB-S)nDw7w^XGq8zSVC{KO*0_pS3;=W==H<%JO4At*Z4o= zXU!dlh9;>B?uY$HeJSXzKY|R{P=j!Kf~h>Q?~w? zSf2jj;W}MA#fsxc|7m@EQZ;YmgMR6ZY2j)oO?idGFYR$&aB!*HEDiQY`G2I0fBXDr zIB2y$eZTC#tB=2J{?Yuw`@!>L{~3fbNTN_<2W->O+3M<&7mDA1(R#zV(mw z{-?0#n*JW`6 z_^f_Jf1H1~>>k6%^f#vIAEh5WxxXd9t6Z*e z{_yV9j~_~pAMt;SZGGC`r~2W1zo~V^*BN_Uh3_-et@+Pz@Y?-t;(vrLKYsrv{Db~s z`OVjl-oLr|@$|Qf%l;&PxVH8|ZuA55u08CJ=awIs&#CyKy<6eq)ahL|?hn$gCVn{I z{*~A1^&tbt2Xpg&`(0UII=hj$M8rCBX3dk|baAUc$E_C|Zf7*ew}AI$EB|L;`Sf>wea8D+kHh}1t||QB z|B$`E#`od=_JX>z`vvQ4ezYI@CvowMJb%=y{|o{tKNde+tA6~R!QRau{xh`O_^x%W z{&2ASYab8 zvt9PJyN6!P&nx-Bzr6mz#QN>-e+0fiy#Gfq`r-Y<@4Mqg?NjHo_%HsWa?K?F*8B(U zN7niu5UUrrQ>k$iytlsZeeOS**FyD&@;~}LK33!@Gmr1e7hUrs+}TU^&0P9&`>xPq zGwp9M*z?*yxGH}u_#fxa58U7O|6TnRv|#OMJ=Z_piup(WNnKF*J7u5JpW2V?Z8r7~ z_jCM+EPXFwf25x4EmoGi|d!gWQWPge{`FfTCI5H-_a_@ zr}IBV*x!==#~JyN`ycnm-}ZlEew6L!sZ;uQe4gyp{pn?YH*VeEYNfhw{fFm|;=4`i zyW|wF{L`vvx|Gko>g9f+H}Vgco^RZ8U*uJs=ijzxZM_JFi{#`7PhFHtf=GxBU$abyEKs4z9hwCI64G z{)6zhyMKrLnEq|BCy* zqw_Rpojzjbe|Y_9PL%x7ou>62YZu?G$cr@5 zm(Fs2@cx^7d+A^Q!`H>deto$X@KUq9cx6O<>?y|I@&6gvHUHh(|H1t~LvQ>CZ|!`B z8lfLMf9w58|M9+j{U5D8e`lJ+H`XbAaPRoz{b9D>#*cd4rH{4GhMhme@+zL!#!&w- z#~$B@yZ3WfbjkDkRzKqY&%l46#{%82RbN_?x{|s#VA563oZ`fz@ zC-U#qePTa;ojUzU^Y7|?a(}X})NrPMWPdbmYw06@;k$YZ<(vLE%FI*zvH#)Q_ygCa z*k(SQD|~2Iei!T9HHAMCG6i0Ji2b{S`NQ{ryxadS`p@uShCHkNgBA8$#ryVIXT|f` z3H;DMaxG3j|HkfP^W{u=#XUacACBew{YY-i3sxBM-R z+1r(oTXJ3~<8X}k-YAPw0iOFmEVRM>p@bUHXvKlLv+;uexZ`VVer*2GefaH%Y5upv zkIiS*`{Ema;6H=#+Z;RXxGXuf&6l>`=h`yMz2{G8{)ahp*^ksIUCK|1jaB_{Z?f9b z`qG8>Z5-sc{%815xc%+c{|qb*KaT%rVCmoYqx=K^WBxbuKZZYMTfFvf7gSe-z@QfA}txZ#COz^5O5Z&PUz;d^7V)wRxp-)pET| z+72+i|0BH36?NyJn~VBkc9Vspnhw*e?E2f2hiCKi~A?W%n~KXZWtV>uI-) z@yFEv3@m(q7uP?Swx4tVgH82Y)sNio3ZKunPq2pJqr;DL`#0S0+Go9O;?fHHhrWHA zKj!;iv0+bL`>IZDQmp)edm8c0-h~SyeoT(Js`KE|IR7KJUb^o5uj%by{ki@GT-3Ewx)tZW zAk$9w!_q5J@;x<`sUHsAj<~eGCV0uKaKEIhHJS@guUq@PJ!Q+K1NJ5L5BC0N*slIZ zu=>&YKZ5U%%;)>ZQxo}j@;=Rq-{F_)q}IP#_~G+|t?wJ(@2UT={fL>J!iTR{cJ@&l5r5Myzo`!_Y#f(n{|fIa+0UbUcI}aSB0DzkyrREwYXHCfe}?9){ki-?g+mHNb5M5WF=_~#C*wzR3TtB)W&Fx$GXuUvOh19Ea$Bz{s zTu~GB;pzUv*7JFPq{;LCD3i@ye5K@W)KL?U_wWClbz%OP{g0#aw_E)|eLK)-X+eF) z{4RUpJUh8RT7PHPxUQw)o-v zV;VNbiGf2>O%-sjEyXZ>N`Li;uw|HrlFhs^BEx6O~b zl>dIdZ+qN_Yjf*Y{rskxyR@P1P5r?<&?Yk_^P~1Zl*4=NBx~eDpqVixL^98VcqHf46OATk$cKN3VnZkbouf9 z!WG@_&X8$Ko`9%$Zfz{bBpV zss9-|>~x%0)M#A&8un2pY?ptt&ix06DyAQfliK@Hd}`{$?zd@&&OOTJb+hJm65x0E z&(OTBK9m24diqOGG5aQ58nM}N}~n_C^-wm4O+ zH*xjdAB&wTmwfoPr)1_I*M%>)-FmyPd*!_spYBE8JZr@e`gewXlKrjxZ^t(MxW3}w zx&2(F^_|n=H{U<>HvTdD(atYtlMhQ-{RlpA%fvif+-h~d&}%E}>~Me4*55ZP$`2n7 z{;0M>=bhi|rFYhDJ?zH#z>ejEdgnj2iuZ^1^V#q&jQ`Pptj_AATl<6i92G?e-`)e}%_;%5PeJ-2TS;$K}2D z>H4>V4}{nA)*1d_$yk15pUBSZHpY)kw{{ z+BVtq+E$LKdPj<5+zVN*Jz#!rHNxg=|1)^h|1fJlQvbnyt^PmG?6tT5#9XOQ*{lB8 z>(zY0clJ3a{xf8B|7U13sAziaYNvMn;fK(pH-7Y2yf6MRwNCuxLv3sATs?~#L-{WM zo~VEK{|R~D{<*Hfp8r3?gSLH&{~4OXZ-CYx?$6l$pP^&YADv6}soA^N-;$|2^nJeU z*}2P`b^dslew=*h`GZ&9`u(9MXYFI^Ta=G(`^TRzHADYD!)&oJ{B0leqwKZ2RZN}k1-8aq+y4wL zGq-5k>1O`plrz|VC8__d`L}8NKeR=^E?Rm>$B+SBM%5&Oj%eQfpP@)Y$%K;PKAt8Nk7um5_2(}?`!HIT$)hGSJ#+aD-M8zHZeG14W1Hui z$ckrS_@9Ag=4JZ_v*YZx{5ZL`?$odNw~U#O)`Je$-KzAbYWI)a z+(*{u+t1b;Ragi7XXtLVnDqRiOC8^|W7k|SCrZh#?>ygOzb$uq=#K3>CNO^t{>SnD zk^B!;*7<+L??1k_^rP%P$;|tkT;{3%U2y!3fAPnzSf%?9dHZ{AKdS3BKO|Y|{;^uE zHF3HA;l{__{%w;hX7F!%b@_)MzxF?&%93ka8NT|1W@Ms4h3BjPpyM(3{b$%*w(!HE z{|t}x<}a$4cmCV4*$-T=x9gc)Tyi^mA6I>QnADej0_WTAKZ;$}_MbtZ?n1Xp)!${4 z_1hIcvfpf(rL}v(3p{`^F%AO9}-3ddzz?9UMW$dY^{up&rb{PvHpvyTSp1U1ym{LjF) z?8owdoQX^R9h8?`TW9iGe$&7H-){Ox_DkRUQxShl?ES;qW&4Xi6qfjM>2t@tc>YLa z!|EFKts7_klXdcr%-iy#CfdH^a$KK;`o)_J_6qh7rdxggBcyd4bW!2c+K-BMk~jYy z6RLRqc>N#4)bssOr_Mj{oBQ~8zx+$*hqm`Sl0J6t>ra2M;*Z!p(``Sp{^_J_-1d>@ zKLfv>-ovf?`%b=Pus!^b@8k9Q2lJ$kAD@47lK##3z#r!2znwCF$@f3_yLc;qW4N8* z-b-)x7(V-9{ZF)Xdf(Fzv4=9|f7qa_=iaNg=FNMR?jzg&$?n{mo3U(W<-;C^x)=2a zeL*cy7M1D$I1(Rs$G4jQXV~QPkAK$>antp0>X%h8eLkT7vFY~X+GX7mA55%50=N8U z5YnxQU0yLE^FPCfZPC%U*Q)i*Q*S?R&X+P*h2cl@KVGYk_J4#fd;jgLv%UQ%=bC-m zS?zBN{xh`pKf1lXtLWc_?e)ARXNw*`@Trsd@cfT*P(_-5hjFFwAA_xC|CH**4=0<< zD(%~?`=8;D$_Hug`TQmi*(S9Lm@>pLfwmmw$TGAAY%~+Wt@Qw9`KRS%25Q)y<7- zu$%s$p?Uw;^*_|-t^R2JEusBe;=UiMwZFYGzsU!Rh&-aI^M<{Orx_hj;UL z8dsQAmVEsfyY*E?)APe(+uxbqu(7qhGCko&%!@n*``rHwdH)&2>ko3|**|EOv)TG1 zzV^44=ZA0azhz(3wNKOKcb@(5%zhc2{|p&sACk=vAMw^zOMLuepYBULTdUOjN3LJ# z*xP0JNV2BT`i1blB@g78|1%t1a!=_$LsML){igSC#rco(Mt{q__@4oEdrqX)+4J4c zfBdZc@auHc{3jpm?(ewdoN3>*D$6NV7GJ7A%R_g7ObpFr4^0K~jp5d+^=_~#?TII+yCj8i8Cwk=CuO+>! zr(VeUC!l`(M2&Cphi%V80v~ew+Wui(aoWaCs!Mb20lTB1a4h}L@IyUq<+{34VgDI6 z=l*9nFxUIrtZkEzEq=80`hnByS>rs@Kg|Da?_AG*qsDLdhm=d1?^m5&Z+rH9W3K+5 zvw7ED<$ z!V0{ezjm!VliRm3RMJ)0O?{y4v41)ptyejgGA;-}51K-g5Id&$A!s^Q>L;L(N}4 zCi><2#W#NJ{8RVOUP`1eUbXdFn~m4!jApkC27A*Q{zv*hRGjM%p8d7HW7)1BA@&dE z^naWkzVE}mg)5IP(0}vIPUSztq4|;F!Mi3^3LpCLN7jDJ_HVuK+t%IE|KXH;A=qKZ z&Nh2?+oNX~AX-kWv&k8s;}$G0?Lf4FO(srGmA@xzfzT9r3^cJG(|)<5|X*X`E~Sz?Xz(rV)a zE9+!^K3!mc%Kk@6a9#aDbCY_`x+~hh9beTme0hIs@1OXkCm-Ilv-W?mIx_tSJAaGl zy2}rb{IjW8bNkIbO@G1j9~HM`{<|*MEZ#OhqK{Xvq_V+|aq*RvZ_0m`}oTb{^cIO)}C2)sUmhC-`U@Rs_#}-SM&dges=MR zxzj0srcTuY{!8^gtdAdw|KM}h|DpVwi`gH9{xdvKTm3u9{Uh78SGmp)e6Kg@EouH% zGWnpL!sNjIM>_Y|Cq264?dyE>bA{CNhikX()S15V=K8L6B}TJyk1c=BP&c{8Q7nytr>sJ+x!KVNX5RHUN7{Y;bGnU zu8DD3b&``LmSnV)MQv-Z{LjGkh&#LEfW2z{gA4fr^#{#MuK!)}W`D+7=Lg4T{|>+N zs*3rK{q7&{&6{%HSiopI}moKkM@-RK!>B&bBlmD&@{Ugox_=*kdNO~O+ru`=(>z5IOh!K$1TQ2M zACT@AV3Y`)<0|$Bh3;=8WOZWIWW}TX<|6^WI_QWyYl2?R>>;! zSE@{^n#EAE*<_KyO(heKC7TVo7=Ah?6*M%3vWXfwNGxDA7BbwzGr`aJBr~(+c`mz9 zW(K~UmMV<*GbeK~YHu!dlKifj5|YH3#P-xBjL{)AU@zwhW}$FTmaei5K}IwDRs{UL z_{U-Hlf>D&pV_P%m|c3-^(|TL=Byapv^2UxtMVX^r;Lo;PFn{R<4H%ixk-FgF#0C* zFoMCkQ9+r#u6Tz`087R*;~k|e0#yQzClnrhmwb7J`3e_9yy>3fljoj`?Bw~~n0Ch_ z>FD;-pEEW-S683KP&i{VTPc4-_eslJU4kJ(9zR1vG!=IQNEc1xVYsZR&?G*=tHEV-f`GFJ8-StZj)}pV4Np$%<_EvzpV^r4LtuD;;$tC>HoNZ zse!5Z=j4C>RSYp7`#<0R`h5LX22}>l=iBVh|NQ#v0)wAlL+l>i*Af9XK6W8RVaT`m4e4pTT}EgNB9W@rMq4`V-u&e*b6qx`J`1g|a$JlY4UC z{AnQleD*f~87ALq;9}tXH-EwLpYjhKxF`3d_dJ)~WW@N_7i6jB@eRj+e)<0OwF9H& z@i;b1%RhVBw=$^JFMm1z^ZbtsSlQW47^W~RdH(137v=@b8$=jpzJBk(R(bxh1D92w zJ^$zV#~OqhL}b#}+sGGADE!$_&&A-yz^`(FasJc(>-Oyr|7I{2IVf*@W!J9MAR3)l zaV~=)?QWl87eoHf{|xq186^Kj{4M_R&;C*a<3SOI0H(=vs_Vb*u3LT`WNiRb^Jg|o zo2H^ln_m;^ZRD3SRIS_k@%&@?PCJPo_79eK*lE@{U$5i)V|S%=Z}NfpTwmu)Rdk2^ zsC;lX`N(v&_eZ3*x7Ntkt~mejKLdZ3)!mboU!(6T9Xj{yUuOEGSCKrQ+8Oc>$M?xg z*zq2IT%F(YpFyNfsm5g6kLeHo`agVs#Jis1$L%Bc__~kVv0R&;w*J9;vEvW#`uq9| zR-^~}-@X5$>d?KH(+j8fRfmTcIN1DW@VHiL%N%j}>tDWKpFOUX^g3M+z4+Bv@7;6% zD_eT~4n4CzYae)7|E+4*yJP)EY}Ng1krA?^Wu#~&>}dcI3uW>4`W z{w{gRe@Ye4ADut4U-WdUedj*K8qbe;yC2s}{b-7{_TO?zM?UJKd8dA8emf`o<6$G22W!7D8TI#`U!}Gy_@8O&V z`*R6v8UsVy_!!TBEaFt@-(%d9$F=Z#vI=9jvYKI^bisLxgpemy@>6)e9#r1T!10@} zY_;6&O%87_2+j{Eo^ZgxQiQ?yNiLU&jBwMPiF1TF4=Ss(Jn21qj)Q^GWz9w#XT=na z2&MxZ^ zUaJ5MUs}Q{<__5NXzv8+NR4&I37*9L?Z8!iUo(7zA7y6pJDZnR^l# zTOT}^c)`AcnSD~F$^_O*?v}W&hE_=n>Aq(N9xy~qvbcLrat7-G=0D$FHufHPtnz@t z?c(kN1}29S`L%a77{$_JZ-?l8Y~Hf#`Li&|!%R+_Rx{3v6r5kkUcej{=E>8XnIt27 zz~JvABSVFcyLmipXY&{~I&kiJ7cR-_p`5AZ$P=yDeumGlc#`82xdsN?^pbNIR{1+E zSgrY(RnkMtLE@aJ!rUX1G8ns_I?9G_+)?$|;uXhJ6VqjV7=b-%Jh#ZGP zlYt`(6JO!P6Uto-?o;Qm+Lu-YUQsxyqE-~@X?cs!_VIh6f6H9{LHdE{`PMz={~0*0x21mEe#E}xp3ROc zZ*3$W_;b0gxcSoHZWiz2m$%lfT=80J+w%JS#042qwUh5;1$VFRT)yG5$y@Kh%?$7M zKUiGoAZ}MLIqUiShxSOvg#5VA6*`GiEhgtEr=l>a+ zYW^MHTz6x>P)+uS=;LwXAG&^I{}VTF+9#JSr?&T_{h`f|{xb+<+!MXHY|`_v56@;U z{;}`a?ITwIrZ36CoQ=pgq3((`3EBt^QH{ zSiVJ`t4{va_Iaw8_6bJ*3EKZLb>I4r)qx-S7FRBOef^fvkK)5tPuF!nw2l|L5&t97 z#(RaovG4lKS$nvbuAMgT`zMB)*x#T#tmOA+&%a&$?dHe(ZxkQARd@JN`LX`N+vmND zf4r>z$oDHdpZAaI#vg@`X6Lh3oO>O!^Fr>|)n}ugoj)4>E3a|!%?z{YFW0`ZYTbRf zGI#0ES+f|j>ks<&~> zXpaBSkiY*%`8T6oe>eV9s962rdQV9|Tc%ao{*Q|v&Trhhx9;_^dae@RxOb-x>H08X$I|8RSsHrX>%^@_=ok9?i!)vKc$3PEQH|1f9%cIiJu)7*dO zZoPkN{^9$v{T%-UYs_lH<3Bt4^<8m_AI_e+m9}M8NSxS*e|t(|nGVM{ z#q*wB_bPGe^yqE-w{E<&KIqt^1jdio-}L@xU}^fhu0H!e!|naY_J8pA=X_Zw`N96U ze5+J@@1q|_iyyCz@~-FpV|uYpx}xj&p?yZzw|c*ox*fLns=~JGUt^_xF1#(tmOi=l zcW=7Sw7YBHs7?FDz&HCJ*TVk{EE#`S*gu$JFHtA;NAJhpl|SqsrA_^~>3Hy-AGVLr zx1L=SmpRX5_l2#z>fRsO+n!uB?X~y6Zn9o1WBNRW3u5oxf7|{M56s>bvMX%yiq3Un zSD4@4zjgf|Z_$5-rlx-<M61$2C=XSKH<+o(5l zSw-4!*GpeH_5_wc;^&uU_qGkpS7LY%-uwT-^*_U{;(wfqAGsfHZ?!Z2(R?6Y@Q>oh zY3E~i{D}Xk{&4HHb-&zp)<3i~@u&%ZxHjJ9`j@x*F&AvS7s(6#D0*{icCEYTm6E;H zetRyL?q1opIDL6T?e5=h^#|AdXZR4%e{=aa`H$>x=RURS==w%G z`73|iOFzm#eBbzXZSMO&&eeL$&6@dJO#fyd_*JsqYJTsRSp7(Wmtj(2Yt^Q&dc9q& zJJ*1{<3Gbe&HoG!?(M(z`5)KO5Az?%AN$YH8fW>#|HJf$^PBc~);{nRkNdH@@X^xA z$K$LoZ}HWR-G9kM*Z$Bwx$Aj}7fWXQ);sn`epJ06;9R`&?`*G4*Vg5%f4H6DPxId; z{~6NjyW}^OAJ6Zazj^wxS~*$vN9_mCOJ?0;|2S`UcDdZb<;zwtIlCn;)2b&r`Kn25 z(ABqJW2J*Tg{vg_546LuUui)p4*-(1f$l_hjx;^@tOXFYv zXGp#Lp22S3e}<-`^#}Lfm)rkf;(dNQiJH`p?8o@oYD#~2eq=s!^26`IkN&NDbl3i{ z`hKupK+mK%IPOwOZnE3eQr&`>OgqI(Cj0_-Gj=_ZT48j4(Y=T}nb&%9?mb|BDc|~^ z;X_vb+y0N<|2SVSth>F7{}_K;V8!`kpqt6;D)heqvbVke^Ooc|0>f;H9u8JhNc ze~dq}kN3yS2lt!eIqd}RWY)=Dw^RJF-SMhTWa-);&to>t58m`)dg~s;W%n{QYYUgX zG&s7tVshZ#>t7Box}7yo{!_}{HyFAY`Fg+U;TLh50&|c?zioi-uk}h9_NqeA2uJCGr0Uu>h-Sg zZ8iGqewYV-i0?1W6_5GI)}FrN#jR~i*DZbCUb@!$u+@jHA7!exf=-t67b~yw-E)20 z(+0b~AH4q=*yh#UikGubu;>4GQ%d}3eVdhj-#?`qqxQ%0hu+^jAAZTS^do=w9?n%S z1b@swR1p8iA^Sy*{lk3GAIBYM-4DKKQr)Zf*{df@Dt6!QN3n{}_DU77AFBW02D)7D z`EmXq+V4B}>B=+z@%qqzaD7`n>*5c8m-p;b__5yk${znp^Ddi6vCV}K-}AhX6T0-i zYMksBPgwb^gql?Jt>tfyitKOuFV$)NG5nCGGrb2iG^qDPA`1 zEPXFsAs>0!ZTExSy}`%L!uQ{h6TeockP&?2b)4j7)yt+k7F__(iYelxBNfx5+Gj z6m8G`Bi-x6wwWt4i(liP%0^SAQ9 z0i9^B_#yv~uwV6g?uy@!wjZ6Feo*j_^v7v^rM%LI?5sXaKVlp9+w;Po)Jn01BEQ<( z)@-R>D}5y3$GLMAU&TLsJ>UArnMb_pOj+1>@do*<`h!vS4{qDva{tGf_}l77^tV~p z_L+Amv!ZqUN zNBEoSSxx_%NAJ2?ll^dIQe=&--Tuek-W^-cv)!xMFPGMx`FG+YiMMa{wsfD3tF^el zcj*rH1NT38y#JxpfBgLqZTVyKyX*P(C_lF6t&#k&`~a`L&yF9bAK#bE6u+l1{jk;X zj(2;UmsNxxipp;(S$)>(ci8oYKbQGptVf>bw10^77Wc`SSY$U+}k! z{~4N$>#oFa{Lk=5B>dt2Kf-;_z8|U=w$rRB*!QEvj;qAp4^Q1riYyRkd^w##K z8vV6dnQOiBY{YW*_%8U-?Df%g+1f%ADWwfdbt84j+g-#Y*8_1}V-^4s*= z|C!b3{K&5SsPBE<^nT-hZkxa*uYa{Ie*9M0bNBvBC9nT8h*Yfho|$7OfAmpqwD;a_ z@4E|sEL?Ee?)bV_y1BeJ+XVQV>>o^t|KMtW%l;qN+~1BhQ9rC7@(a9>XScEcZFiL8 z$McW;$99D+Uwin$+~km}rEi4~&lB4GVcT@wwa@lG+FKs}VSA_RzWIeKGj7H1ZEyR! z`u5(~B~7P&uN<&^fBW-4j?2Hj|A9{bKlGpBMsmzm8|{zI$KvI`>bGB$XV{bcuzy>| zE%qahAI*>Het3VlF!pBFTGi*RU!&utoJ$vFPZKxJGK;F2p!#;*@>QAPX5WJtYOnui zUe(OuXv}>SeZTQO|DViVVj|Mo76J;>fv z;xCxJ%WrY&;+Sonk2;@SnsoVyP^4>meNG?a2mU|8%Ktd_gW6Xf{~5CGcirF2*S)@? z`oWp~o1aJ>*)Ly{{n*=kZPN7v;)$T@i2g10+Uk5;o!*b`Zw>XF`UmnQ zE1bB`3cG*&^Uw6-;y?D6zI4wu53fx9aBpwwqoeJ9SHJvRzm?lsL;uU}8(Xbs=ub{y zzbpSCnE!{F`Zv%OeR=zJzB(O&t;k7A2IirOfzc`YZg>#FJ7XqSBr zHMSS?Ya$oCx~1E)`Q<+4t9rU?Rjxg`@c6R&O%SADwod5OcnU4E=TdNKA@RsG@VAH;hf{wQ4ka#~f~RF!f$&FfD-?pf@9 z>C0*FyH@v}UE=kQ+IdxHP07pc3=#hsn&$jxI4EYH+5bcB{@dga%fE$RGxgLNANX2*c58lQwsL*Ntm_9p{_u-s4xc;!@IJjQA9LT$UGBfkZ0X7yXVceY z%-T|Q*nP#x{0zqL>;H%-|KrX2yQV%N{>H5L-wt2+r)>}D{|C#TzgaUdp39U)E;Kg z4cGDY2RGf{a{Q0T?r%Oj|E~Fx-+Q0uANLRMBj5MwU8>{Gh||6J^}axSjr;x&c^g;% z2#(luE#D+^arc2=LKWxT_kOs$_4KWZ_Q;*n&Ij)Aca9d>I6HvhNAov<{|rrr|1R5a z7k~TuqxmHH&^rG|F#a3pRP@-oi}sxV*nadM zvEn~=*7{p>gZY8^Qa{AK_gzx|cx}7b=0!h>LqFucZ!y)r{ny>`W8Tbl*}qgbY>fUJ z?Z1Efb(7iMvQd#K`!-&3u>1XYcm0E_?{D4zrtr68YyYkP3}HXcf4KapZt;iPju&lw zKW=^~-cn=#NIN$Cz%99jx@-6CUzK5}w{!QN%Qm^G52{x`R`1lS-&CV7adAy)ZQjYe z2bWX(9Qe5Zafbe9V7v0~BIsZ+{+rG5e^+ja@BPofyN~^c-1FFv{H=DPw|?wCyieg{ z=zoSIy6J~DOY@ff`nkpWw8tOShuywqmL=A;$)`WEEv~N3Txq(kRd?&wjr&013!2V0 z{dal)w)KBRq#vdK5x(wpWsmBI?~n8k)aE}5oBi?rv01;fW^He&VO(0H`f-ju-=D-a z5zA)g%xdrIz2B{G@>ls#Sy}dS$MvUIM%}rSWhFOBZ>zKc-~NAG-~ThP1pN5-vzIC znI7-@V`$`_z53Oy+<)`eMcs{#KU^sKZEgzV_xEpm|1+?yth;`H+5dBDgHS7 zVgIpvsz1^n+L|A?m)ghiquTx2+tPaGAEk@7PCoJ}L(X*Gr`K*DuikH5ykpYuxNZF& zAO0AH+!jUSdj%pC?#NCOGieA0- z=$Q|o^zqR~`zxW@eA4@;{XYk(f;oIzwO{X80 ze(%;R&2N#K-}O~mSJ+|4Rrih>(?z#m|C8ALQEut7inx5i%uP<$cKutu^!CZ6TU)m#?!mM_e>zE5d)M_kO!lPm5WUHYRc+BfeB^JD!V>gIn0 z&wpF_pP^||-Q{>e`?UPF+=uz?QP&Sfe6`$XR%KC-<|Ve^$OZ?)YvG`Ls%GTUtPt99zKdEfaMz5j7>|7T#S`@7oy!94q|=NtYr zB>!jVDBCChqw}$T&+|j?KS&>y=UcXK$A|MrXVfG7%5&MB^Qk4<2SA5qW+y4xlYU`I(WQSfish-i= zx3xZR+qS4_(^9S`GnD@kKL3w*>EG@34;J0uH03|T*7=9~W&TKmsxN_U``Hii^VPUr zsR?A;{n6|3ps@ zJ04um`{JMCm96nY{URpwU-Os`SC4L4pRBfaacp$_{g=Cr&8#hB{PzCs!~YCyC;#2G-yr^`y7+fujpxVZ z2j@%uC_gx%qO88xj`aiE`bXOPMQXJAk6Im@{_w6*+3ss4vdV5Br%r9}Fg>g8{*m{2 z`?^y%&X)gOzI55{hbNX?XJCu}Bi#Rwcg^2z^$(WJ-^}{{ZRCf~$K%=diPhL$TldF% z=^v%7KNhd}u}atf(YD18wSV6gj@q+DPV~e5j)fnNw=Vc-HgnaFJlpz+^qsQ=q#`ez*HO{|<>klf`XV>3u|MuYH^*6!~=Bgi_&;QS$MsD`;^B+(Beq^@Pc6$3h z;g|cl|0FK2;68SJ)|FYZ%eJn_`fe7dfA!0L{>)kB?QgbnZq4-$*gnZp_e;n7(rg31 z{(oGcZI}6fSMGl>ef}2y!}A;EMbkcPKeFt{)s*S?S@-0AIQi9kuks_2=CCan^8VQ8 zF3b8BRkwOs#bLi|TX~HRy?5Bzms)nsB=gH(r(gH}cnh%KuHV-FN1F3@Ro#*M8;>pi zolx=m=zR7+$;&I`T`t*pew_ZWzS&e>#P_j&o0Z(!;>2XVC8e_dhqu{Dd@SAHu5T)# z^fB?m)}UK1o~6}UyO#D&eS35VyYhbqmQ$cR+V|(Y|DpN*&Fl65F3;z!sr^v?F#K@g zsvq5l?fLFM>V4n!_^O>s#q>j$O{eXcX0}h_!?Rtp#M^RAQzko4KeB7iy7Q}}@7<2h z6z5DWG-9a9{;gDhP+@=e`rF*!-h5pCNBI50U+(QS!XIZpwEfS}_vpvvl|P;b?rc63 z&t0;=txk3GhyEVZ-lXr&o3E|kS|NNSYTD!_KZWpj5e*}Ll5B@OyNUrkZu-Vls{>c7l{qXpAlCSWw%^BZ+)lGgR z&v$ZZ_B5%y^@}UkJ6_p3m(6;wR>rzD?{saP7u~y&b=|wTSab&aZBU;5BlYk<11rzp z3I7?A<~n~n{c+j$50j5htWZ8QZJzay(;vbQN=@(D8sBNpU888Ztu{Dv_bh)N=ZEWs z+#gh1KE1L}=|4mAtNZC6?oD0x+UnT+@<+VV9{aB(FnpT-L-qO}>FvLh{xcj5{>S#? z__y{Sv5)&(=5y_ndU5OB&Ik8dzPHs` z|Ixp0Q@p$;aqq{tl{R&pKN1($SX_N@VNdX*yT#ELbGCMO|7Cgg{g&Cxhd00E+Wq|F zUjN9S_h{1U{1s=*Q@{&_Bfx!<<9~#VkI(0|vH!UFh#lLH_6Pnw)1=uh-$eMwFiXCVDL8bjU@_q6j0(p!7an4@! zqx$eYxxWi*#2@7gW$YK!w{c!|SHH1_ePM-*YvHCRQ_Z6A~p6uCV%+-SZ@AW z{|Ee?^?WMVgS~cL{bNwl_(-4o^)35OlfCL87q-6E`1)2oXiHD`Yu;eDJ-=huE&O&{ zH{9p)0lSkwZvSUs+y0;7prajg-Jz)aZ+AZC7yP5LBcHX#a`vPD3`gcS+{*pW&?WvT zzww`3#EX@We5+o)`yBG&-Ry_8!AIrzKm6Ezbx-2MUi*;J%ZZmwt#kFmubWCscdIab zIR8Uc|Bv+h-!cCg4o3Ww{p0xK;D_zU=5yBx{n7qlx4rhkZh5gk{=2U~oqVibK*waQ zeZ<}m&)c{7b7fjRTc%S|t()y!TYto=rEJ=^psZ`(Zl$ivC$iAHL_yu#>5mj;-<7{gHLw?9$ru z2eIA<<|lpZZ__XNEA=|;qg(&Fic*tWrN@ES*6Mh$um8_*F!DdcgZBMfK&R5E2mXkz z{y6>UeEvGMKjuG=puwwS%X}@x>?S5!4^fFHQ(wFYP+!1}Z!!}-_t?|5DSK3GxKd$fGUZ*;< z#`UA{Z?@w{k`KknU$oI&@Vct!ZK{n_x3+nJO6`I{jKT$IEAx696lN^ zUX%FY`J?HF?stn{-zWRw`;qu|{znozTh2?poK=*yeV@=rz5YkG+P?cP>+6+VEMJ*n zKJ8ML*sNpomu7tu+q8Sbv?C3*p0)oO*f!N4ba`W+Fnj*(uGr{@-f>YihLazs+}}K> zs=rhI(SL?NT0i!f|7YOL*&b+AQMqMwURsgtb@eOv?YZ_|J2zgnaa8eSor~X%7zgq_yDp=qD5wicbcN?hL z8(V)+d*Q_z)ym%ikrn4IAKIQTw%_QF)wWCdHIkVh&G>od~`eE+h@p7@T z-V4OLAB?nr&?LWg`X3?h!}5IlRBFOL<{z%_mgo4V^m;;t z*8NBFhdpZ?bF&}L@0k4J*S!7t5B@WVyx6CGX-lr(Dc@w*3t!)QAD*?h=~bB25;^(n z%6tBu*s~!z@_=36hvR>w&i`j%<^LG|hM)iK`Q@+UIWGBnKP;)2DdE5QDmFcO`lEY0 zKg_FT{wH)>Ht5=;^KQF7l++nrtx*X+GAq8}$@RX)x=T72-@6}CvAevjf&Enc2haW= ziv4f>|1+=_O@36j^+WrSe~dq(Kb(JPdbO?nk$tDV(0_*Q_kFI^q^^!MTblTgpZBGl z?4_@^!J$_kTxN+{wzE(vG9u$v?Y~KH3rovaMMYY$AB^8F{wDh4@_(G)e|!IN{yTM_ z-OU#tYFvL*noU`%lN)`3T_6gTB z{qg@1`zZVOU1{%+%iE@hY`(Te?)(v{(!EdLxBYQl_-b2VY{suARduU*3}Uj%#H(W8 zu3N07!M^N2!$JT02V3UL*FRVmzt#WH`&;ox;^Z#=<2m?&GnfBBe)~UxeY-xgu5Vbc z^T#{+(McQUl|R~pyZv{3bnn_4o9}Z;Z>z5VZ*AAhx9&S!__8}!>E5+#k*N&7r~l(J z{?EYfQg_AvLm>Z+Nsqr-ePlnV-}aw@lfQ5Ahb{85cFI4@kHo25)w5H+`6Kv*8Ag}TDb1V)34qZ&o8chXR_Dq*)#11p7TFcpZ}40{@eRM z!$IT!3@NwY-_-W|q5eR>Z=T?XyZ5_I^&4-U@2&}7_ao~4!_fOZTXmg}L>-&GGUCOo z*!O?b=Ly$~zuL9DOZQ9pl^Xpu(Q&V(&MvW2?f2iYVf$qVJB7c^{}~=^iQlsPjpgs^ zny|n7-jw`j;IE1J`28(w`9~rD{(mALO_Uuk{8PU9$M@qDd4WG3)6Ao6j6EOB_0yNS zkvZ#I^U6%I@@c!YZtm+>o4D$w`Sr4Qj7$GBuqOUzXu1a4q8Vy`i~XVfvH5~O(mj4i zFRWqQI=7Df2lK(Iv+p~We6ZWR;jn=a?sI6k_Y9rodQ+pStRS$>Dh zZ%f|uzObs@*C%)B)u%tq@Auz6{*Q0*-_`Yb{Wto<{`P%c-}Ik>*G@97#^u76c-}wx zKQ=$?^^Ul*m0$kFuiUGBi&sSch(Bc25*Sru*?z?QR_>OnU)%ORcHethW$)#ZnG{m;-m{XauS{twmnZ;r3}cj{U^YYqR$#|O-!u6O=%{;=x#k$WoFY!oX$ z91i+ew%Baz`p7$KGnZ#;K91*(uV{MpIqu(qO!blulWzQ2 z^dWnnX3LHER})@Hm9=dT`*@?F;{6|?_+X~EN`l_t+Ckg zk#F;3|F)Hn@`Ydh|(r{B_bl!lU$?it4K%isF`XJAkG zcVqrm_BW=F>~H*Mi1^|8;r*d|Mn5J#;O=j|FJYgNs~vt-<=UU(2Y0hQr#pO{^Y2ed-&H?ymy-S-Q%O`mpY}7YBR-VuYN67rPjT4@7dmG(-c{kZ zuF2W_IJ>;YZu^!CdF(BEU(eeAu0A}EVf&SLrW{)qFZ*G1{h#1|4(FAx#EWin^XmBq zM_fsX-radX{qOEQ?!U!;%>TCVN9WrA3 zqy6A59yz1QK|B0~E6(|Ex>s?}ULZc>Lrlf|@JY5>)h6Xz-K26Cu$O=2KRW+|-|X`@ zm>;ya{*(S=`QziV`h%_|?>qmg?fvobqj2n%EuMC)ujliq@Ax?5!@ubdr8&0#Uh8OT zU27HJ_L9R+_`;k{qZH%+T&F|Fp654o_X4|Ffc9%cg@m0F~`q~oj z)j!thE?1Y{;<5%R_& zT9{^LzV6!I32gVf{`mf9VEHxwALsWE`w!jY|55$%*ztc{#)%*2$L;^I{-b!vRg;tZ zSYOApu2cT#cyY^m{>;;lIN!hh`a7ha*=fUnhC|xR@3_{dK6-t7y_=z0UTc}PZ_2N)OWz)E zzaDrwI%eCY-nr{G+D(|QachoL=+mov&o14!Sa<$h(Y1H4uGye^vaNEhYOfqS!>`}} zxVRtH-*7*=e~aJp)%6E0|H=PJ{E_+F%)Zq|o^S5M{yy<*QtiD-AA7fTv+nPHuRQDY zkvjcJx?;Zrdi8X5-`9AqjN$%vRZro!*zDrvnbQ=+OZ!FY4g~#YIB54r_hGK|!}rI! z>wjpR-GBUaulLdYg73caTK;3dUw3}-;a_LWs@SG4cYP$k#r4sYYfCRIpK|gcN8Zi1 zU!^@f4&IFxEjrm%VR->(1br$Kkj}sevmSs>rZ;>!?WxU?3OQk zDW|jT$JXNF#s3)&DXxB1ZhlYs!{XBqZ|kbscnZ7oZK#jS znEs9VWBBpgx&EMYcK$Ow4nG`!^RAuj53@_J58sz7)#mj-wyDNrTiZ8#@mCZ4lOM&4 zb9~8u;no+VS$cEo*BM`TpXQcT_S(JS(pv_-NYJq8hy5S?&!!)`zMprW`Sw2|W$}F% zvZC~lbZqT4_`}x!!2Db2lHwQTvjVRt@9#ds^x>6sLi?fV;_p{%owleZQEtkl7bc8f z&;JoB{>MG@ckq9PgGvv7%zn7P=^rRjSwCz)THR|O^HKhAepgwXQm)$JW3xhgW~EN= z{$sx4NA!^@*?;}3PA}VImA6%uli$2X+T9lVCTGn9HZpvyE&j*#{&&*r_zyuI z_77GbKe|tLHBS7|G4(` z<)+08c`lEOyFXvp^HHSm5e>#K?SDkG|8c+k9Z`QUa*zCn>kr$H?&tbr_9Ohm{D;@} z{4@E_&?5b?w!dSRy=cXMhTnSMU7C;nQ@y$;_>paXz%Hw_iL0~2toWMKReRf}ifvte zvb=E3otT?vmod~bez^aSujB82JIT6(x5D4(KK#r5P{zzo?1wM^f%B|?O0C#C#2;;M zvlGw!C!WuivCFl0@BGd;HgfmWF4*X7t72<@oPI&`hNzN zx&IlO9@JgA|H0$*`%W`^)-Uo5H9kKIKgR#o`h4_0Po4VB>`fWok8IiQeY))ZF}{5( zKi}(Fb_*B2-V_yoHhGO#{T7|CYprhkZri)r=;gKWumf_gAC>=cJ^vjKDqEU5U(|mH zUYmThPX6+LhD>?xip`!M#akX-vaz*^=eN;b{K6_bBdWZ?q@L@3c2(=k4=VR$c3(Bo zjrX`>`unYaRCCzX_oeXy_p)}aJ+@ea{j&Xs(EcC#@872WXJAeJQTXBaL;j=t+5hPM z2>o#Wpq%E`k6-V%>=S+YPerg|{qbp5y5HO1*tkF9jXqkZ{E_v`w_R@2SHCXEm?yv4 zTiV<0YTCSC??dKkS1K}mKi?Gpp=AFb5$^})ZwwdJ9r-74xlS;SpRLBc|DpWh`F+2x zxBqc1{}_E>KL497%RhW;To&p2pMlfkL)!W^71oDerY`wmAAiNhXyvlVhzR$UFK&H( zy8hdg26@oINow{Fw)|(oqTSU29$Qa@wtZ>r<_lZ2wiaKDGK=^-(f`|ETxf*MDFh!^Ik2qxt+lxLrPc zTe&)`_)djdhU(QvALS;m&IV$$aoTzvZ8z z9LI<9Kid7R^)fcG3x9w%<|3G@0N2t8e$h zyGI}XmG-`DayIDrk&RIf9~f#tb)FZ^i%Pdi^`%-{t!4{~22AA1uD7@=>h+p?!Zm@7DLsul94ksnJ|qX~+Ge zbkQH{E6=a)(SNX4-RsI$?%%8bGw>ZQuUvV0-MaOawZ)HCZoOOO>*Zas)ofb}^XvE@ zn)m<6#QskI&u}p4A4^62x1t~GAM!W!be+k}v0Tn#>owb@z|^$NcuS z>la>nSNi(BNX7Oe#y5YYoqG4rH|h4FYdQj1-)_}D{kC+a+5EH(3{~lGX8vbjcdWZ# zzlr@#{E~mi<(Mw+6Zz3~`kdXFZW0P!`Vak+%uQmjLiI}x#7e1 zuK9;G%iLS^{y|6JUuRo$doZvcvH#$m|3jz!t@(cjmL)%Sf7E|eesH!t z=O5=w|4d(Qld3;@pRp#OKW6{Mn#{t*aapsLxBqcgTfQjU%5Hb!qy7A^qP};eUH-b` z{j;tJ)4zGQzI)HJb>yAFt`8odjIGa(zxDguyASezMAVP&y5CkK{Bid~(fesdDk^dQ576)Hbxu&;uZjki! zf2Vgg`)pzO?fj1`_CEu=BIww)VEdcb-T%(mr}!iKVfdl?w#iw)_&e;>qHEMH?}=S@ zw z*!+*L>hC`L#QQhi`~U6xCsUFC;C^=rKTBrRxy}#SkNo>>tk=Jiv-lCcBs=_9+mezW zx+Uj@Gv`TutXc9QVckHrX5A&n=A8PJ@#LT}f|Ig4A zT~qX<_v7*hZ|2E;{1Y#gBXs<+{^9=&ou}8niWB|OT$s5g_Vq6P&aIjyvDt1{9(-Kh z_LVn0K9>22Q+0D{@}qp=NdMGIvCWDN%+lW;gKm@m2Wmdff4lHO{O#^XVXGhRXZ{oT z!?OHAXB}I{d_I%v-S(V+G`9apf3(x4{K3S9w_pF-w{vCutAZcLs^<&6uG+V~ex>)y z*W$q<+qPeLaM{LoIYaIA{|u~>{~4Mm{%6Ro|DkFA=6K?7?LYn>+7Iq;`^R)8zs7jw zpWwYeq96We5W0~qFSeDR{gs*2^Y+KrQa`G9*SPYxmvF}mWmd&^rd%+Icp~&VDr~y> zx2{_eu4xCP|1)gJ|08qzcSZd{`~M6nOaD&XCsUFA;C=rd?T0gJy!j8wOZ@S?u*ZAF zkM$3I>Qt}1FWvcOjnbt(!OLFQsa@RiH$U|1)?9J#k8G1IFYB!ry7uUE=tW(HO9G%^ zRyY46CJmko&--^dUa&s%eA{(9*&n@+bdnR z?q)mexo=THP=Gqwcet)W3~u@7=HHs95G< z+M9gDs%4Yi>e9^R$7S2*FM4TJ*2uwrbN+{b{|rAY-M`)X&(PFgchd0RiTffo@*j#H z{%2_3dU`*{y+2{s{z+%pDQ~-4r+w##a@1uT?*+YX`hpe9V>W!4H&?v#ORV~l>s9gX zYoo09#`|xHNW1)Hcgp1yhMoTzSiXP;LH6g_|4=V~v*i9J_rveG|0w_Hd{EEt{YZ14 zQ1*Ty6MLcf?Eeg0(nqbAn zz3;NuW`-ZDzX|?lXgc`s>i!M-Z#zFqf4lU7Z}G$Xo%4nMnb#jmls}$p{b=6o@|79$ zMUy{1@6NTUeAIn6`8R*?MN?Vx*nJ;sr$~KWw|?>c$l2W2b~e7v-!gmVmg5W?|1+?P z{%2@9^6%>W4?*^~${)^u>-y2^yW@|DlC%Yx*|n=AFr#uj<BSU?v-|!_t;?yv+_Sq&{51Pb+_(+@YTOr9`JW+op^=! zL-r%{nIf|4)UN%Li1`z6(I&KbMUCc_Kk=($vS)p%@|h>`vA$+D&$K3z1?a=8XQhAG)#ohE=oHecN-L&u1 z)L#4VWVrpGp=ri{hJ#7}8M5ksXo(+x-?m?_PEVe@#&Y)$^`xAAJU^TthHl%qEMq>u zo_y4nkF3}BX4bFEwz{@+RmI}qD__paJ^IfOJwITZ&r7M0vZ&NKVbXdHc8~sU-T&bE z`rFsP-T&LWzCQUsgM3CjXN`2?qrb|o7wYu?`2MJWc(>f`y!o?-m>{SBz&TKsqQ{-pm5w^tvZzx_YM;lJ63?`izl z|FJvr$JA9dc^ChrF7&r=w$r@zC+4H$$Mbz9bJM*qnywZ4?%(wK*o2q&G%l|DlIwS< zRy_K$$;)ffdyTHWcD-`I%KwK}{vR>>Z*%@LG;RD7^dtWx^W)O>Eq|CFzHg3-TfDqR z{YU(4#*4{zjejb44*J8}67Dch;j z<|cgG9v(G;{r3E={eNV6|1+?%{$0O6>2LBki;v&BKU5#yR%iX;==5eg{hF8$>FqVn z%lV=rp(xX!n z_;ej?;gYAkJ6qWYOa~h)4XECF19~Wzx&I72Hx{k2^+RuU-!pi*VJ{rCZD3C z9anm1>uhpo$PX0)JsNWMF_N%>r z%l7VrzgXn-|6Q7AFHo^}+Nt-)HvZ`Q?A-PG$git+qI8dT-8s+mAo>fJ~QDT^UUTS{!Tl$UdgS==Uce?^|tM|SdVUCD}ST;DF280^S09SeVhI>9CWCY ztI7M(zTlNTqfO?r8m6s3)K^AiPM;Fx;YYPh{<4|#BzA7x{L%D3L-XRR zYwjQURwuLP%7S@^F30V6y*2K?g?`NcHu>W` z&{6E)Ke!+5Z@<6k@7d{X(g){B{$KDU8AJ0GR>stak_m=%n z{e%Auo%@)sKH0MUitd;D;y=WXh{>sa`1R7 zzlgq#?CO_t>X+V>WZUMQySRt-q5lsb6I=Zwmop~4iZi^XzT< zZ#&$qwXr*JpRPHp$clBl=+9VI7Bx#x3N ztoFQaDjWCS=W5&3buT|2T=(i%?6uC)Op~BvjPd`t%>Og6F8;fu{=vfk4BOHVoxd#| zdFhLO^RN4Fe(N_s{X2h;;KRt}AH`;Vcva8(W1qhK%UjzPuE{QNw(s8KT>Y54+BZDp z%DOLS_5FAC`(Ik~;9}0&E%&uPFu%M1Lv#Kgng0yGJ^wQt^r%nX_IHA*f8&0BJKl^s z-PwoC?WJrIAI=we9jCnO$E1He*Gu9>UzSPnHLki5dGNz~m-$C`u}yx&{oDL%P43E5 z*92ZpcwG?TUACP;6m;hO{tv~^-&+4?V0rp?!sKtBm-cbj*#0=Z_=icwf*Ssh?OlJI z*S-+{&%kr}=&W@sA9=qukH1v1oBPO*_J_CaMP5tw?OvW4@M<6X6@~1TB|lAHcN=DG zW02>mKWOov;lbwr3|rj)2w(no?Z?XB883IKwSR0rV!wswp84!=4Ijcg-z~jha=%mf z$M<9WkN-voZMjym+j{38)wMS(jvQZ`S^n);-o|W^^uSBYFFoIM<>uN0^8XoFL3f0o zue&h+L$Lj=Nt!&{zPQJib$aU`)0I)TqH0VwT(faq zn0c#jr_`%^hPx(Ry1S*;?%kC676eM~)7noH#4qPb^K_4DF4IzYp(NAvsw1v z+MBn|(EA#j?i(-6&H3f0$pKK0JK_Eho%3(ifAjsjUoX7>!GdqQ(p&%O{_+0t^*;l* z+Wp7J+iZgK^_mw~{5$+;e{V^B$2~o%)MrhhHqtBpGaO2Pc$Po>;(C*}YQK(q_T9@h zHHq#u^WPl6_Wh4&{6FsSkDvc>P5kZtN9)J;^*_R0eyA_2VcxRczwsW!hiU6qRCN4M z4!62C_fc-}(W>cPU!`mH;)N=f2Veh^D^y%B*SPTIwq0e-iz1vCz1zHN$CUJMsw?q|7wOZy*Z>2Jf= z@tgL|{NR7UUZ#dMeQibmk@;MCe*z!r79aYx{%~#j(dqgtvkT()O-^6o{@9=M>^k4l z_3CYt{|VX#mRkSyT=;S7)}>d%$_~i$|2umBLuCD}>~Gef(d8QV{|rrCQMZ58Kk`4a zzIRvqk@g?=AH1!zo~wR1UZ6z3Yo7jwkM}$OSZ5Y5KWTcsC2`Zou&Il)>qPc$><;R8 z*mdE{Y0Hwm!JgT-r5ixY?eG6*U|s(MM5g>W{h|I*dG{XP1vMfc-XGA8jkFW|!GB0< z@BNPFAKtBfWPP76D^B&J-sZ==eusVLAE|n_zVe|xA4ilfv(sghYFnvuw{H9Fn&o|a z_XPfZ{~2=ne^|Z$cJ%N1`o!A$Z`S*Mcz^iM@Nj$AKlKWe8tWg=KYl*6PvKhn)qRRT zv_2W8KRT=K_<{G_e6g7i#D8=y;rwDRE&5TuYt8bDrMcC*&?xoomBGCrLQing5+;|6p_dR(J63MbVG% z|A@>w^P~9Ke+Kb7gV%KyyMA1Ls2gL zm-g7MtT6j@=-s#Hhr{QITrfYnD(Yx)t^s@Ye+Jgx{|rqxz>6~uR=j^0xBPK^dz{P< z!wiT&HV-a-C+FTP!yc5R;A z1(UbN{#$;llx;UYU0h$p`29QR@*ej1x*PUe_20-piobFDh&)G~^pD`9_l5q+&Tri> z@t+}sS2+0Uva5fJAG~$;ja5Gyw#)wTPu;a&>e*h~8DEzA?EO(~gXw+2%=5`BUZo%Y zC;8#dcC*#X87ln$2$}!mGyU=RKLg96AJUKOZ@zZ>Vg6u$-+u-^)Bg-2uj6b#Dl;TyVd-#y>+;orpMMK7aN*SG2~+HqyQjeX_Avuy7kmNw^>={wk(|KryF&%k=@ z2WS;g>qqfJsUOP^>+{y|{fPaLr7v`+!u{d$wmpvZk0syPxIgCjGQ;#ggYb{ZpzVwg zAFjFgce2~o4Ht8DYxI{mE{gJBwd!QO+1f%YhT78~-~Z#=|98WGh79=|?D2oQ|M-5G z{_t&kzfJvzeg7FAJU^O$E9iF2we_X{8Ki%RM_yR=;dp27_QR)dhhO|-{g_{(qAI&r z|45xiywIy}O1a))SHJl4WQvD}O}kaVet!N35BVP!^B?d3BNYEo|44jel=;Dawm&sL zLLc*s#Md}|IN!d<|50!JL970Te~PbW`AuDvb?fN7ol7&TerH>;ZA@R3Dg4S#dgq4C zJNkb6{|=kHKC^#CJUhcr_lL*-h?Rd^|3UtbsCdwShNgA*V=DZQ?w70aexw`p`{+N3 zD<#q0{O!N)H+_|7cmD8zD4g)`dU117{!k zwQO!tX##)pe}<-$^#{xT8UJTEnDEX{Aj3}gN9e=(jrE-WczTn=Km2Dne4n>sdceo6 z@76!=7jVu^UiD-15j)*0U;6n@R!w**W_ByCbyQEdd?O5g`=M6Zfb5 zYE63mx~zLbv;D8E{U;ay!|L~ei#0m(yz~9rqiPdvY?D8ny6pF1-t1Dft)*+C%%(4U zUGz$P*4%l&8~7L0KiDMyA?|(8e})g?XN@1mH~#Vb(0*t>(>}pJT36TGL?%Aa=ld}| z=;OYHkL2a!UvAEr{A2qOJN+wL_Vb-KihOasYTDcdFT$c^r$i-X{aW+=*0Rlor7Y7F za+q!mr=aLV?KQbyzw`$k7 zAG&KYo4@n@!%Y|dGaM>r@09lH4$)t8@WG0&!mMXEu%EF1;MV^`fBmuhKa~Gl|1#@x1BRr-4^lYeVf?)+vw{iWBAZ7&4;m{hJ zz|}vNhg{miD7&nG(d&Im8$bMOOn&&<^m1_2zWISyO>aKko~^*Ya*zIh22j~s2O3bT z%Dlgkwf~TO+xo}7;t%fbKcLJ0ZF+8SxW7PVJnx!62J<*BFU@Zcj9NCa{v)@~hZcj( zvVErY!nY$%uX32Dxc5Wv)rZqG9qcUrGc=k1XE<2#&!YaI{KFp`AI5Y3F}+}C`A6l) z&J~$;%Gd0qD`p=l|HnJq>EpV|%k8G}ch{IcdTZ_Tk#pmYD<+$BmsYgXr}qx!c^KTakuc=1o~N26MQYUac9Jg((p zjSua)Dw5qdefZDNTBEzn_V3lZU*AO8O`zQd5zz`?QKKls_Ts7k-Gt}NEkDnR2rRd znvu%2E}%8(V8mmV^(>{2=dl&;_^{K2;d6EY(?nr|RqtrMwl(<0+vsyEKKcF?^z}1ue3ZM^WaI3{FKi}8EkIFu<|AUNc8X% zO%+21p>;cLr7Qy;E}Wvk)NMWO+>M9^r;V&-s|C)idOLT;8NpMlC!F89t;ul#M~l&h zI|7f3G~RgDyggu{BD-9NF;vxAAhL+zVZNb-;GDPRuNZDhNlG@VFzpuEw5{NWBDY`{ zL;h4X#v29U1-9Fs=e_xIFma3gqx>nUI~H)Vu03eDqO;q3i&Br6&e^R2D=Z_W+ahjo z^5kr4{d6~?!Trg}31)vEZ!vQ_u@Ym*pR8}`ChauI;{fBvt(_K((e~OUc}E^xV7_*%?3l96iUMv8)o5uC z)<)IFdy6GA8mBNzO=sZ!q#=3n zJ&uD6Jwk4l3s}TH8|QLy>OWm4XFu~ml9TL>np}k`I_4rV?m6PTlHd5llg zQ74+^trnxIp9G)BQ3lU%-n~n=F)UbcC(dBWi3qhzF>LI~lfo54BX6_@IWULaOG{d0 z#v0@HaKmibZjDIpse*G>Gw)evcus@y*tG|&YHEy)T7C>UxhI0>h_N8TgiJ&*)Jb@t-jydopb2;4Iqj&e>yW=zE9TRaL@}Wwo}^rruLtpB=aqW;Si;?b{=bbXchvQ9!lRud=t1I-RGd0M6i|%PPG)P)=JDTf;)2W+b3Qdxd zWfLSeO=Xza#gKeZiy@Zdj)yEK3q!#vPA#tq2Mkyzv&%Iw$3ky|T-b#zj-9xztib!* z=KdWU27m2V)~2@PIa-qr^BmD+FHm(fVR*h}httiT3G1IubMq9~$IY;8OyO&7unc(R$E@d|g#GL&tOrUBCP7w) zc?{?44_|9gjjo(KG0e!jXSsqlUnP%643Es52MLo8EMSN}<80tR)gvtTl!y=~!^WHv z$=Cfe?f1=YV9q!C`$YL!2*cDnXI}3$(<`?~J;>8I{h42}QXT`3P=hCT1E-Ek&x%`1 zJ5&rACU6A!MIJlK`Y(#Xk>$8V@210Ad-4i*Uf0-p=e~&_--XKHw-Y#}E)dI#CrPaRJv^pq)lU%qtg+=$h+lnT~guGO-p75*=*0f5q|zo&f=9}2gAeM7#l?x+Bn_#?lkXbF`8-@ zp?YdVk_5w&HkP)3LPbUlm!+EytiN;Yog0HKUvbmgW7k+#rxy43jinN{v&%j9w-Um54EqN!zWcYM~{KQXxPETNySR(P$ zUtt>~ODmVO4x_5u|88!n*1fb#OO_SL zax*Ypd$4Im&(o*#@^}xj@bB%B^Gacl z-z%BQC$i}Y<6558y{8)vdRWFJUN3s`ocmLg-y^e!%?T?R7~}3`e1G3NVX;WTjj%B3 zOClCznQF*7ZHCb59w_L<$q(utj16^b1S0unMpMx4(ZoaLu9I|%7cy)49{d(1N_ zL?PgIl+)9uO0U9wx%YzD+|s#TL@=(+E?BkZqJU)iIYRW=&SG^*~aFfWmK6zCFP@oU`MK=greff5M}mah9!M` z#qBZ7O`Z%XA04=#`0kwIcIJfa42`>618t6{-JYYR7CGaA2FoO~ISdxj{;hVw48Oak z9qQ40d*q#RhIIV21J*LlEjbLP4`vyKK2Fu%USM%*dh99bdE6Q*%uNm!FA5kg3QssA z@-`;VQsU$}hUe213~w3T4xKF>+QBgKZMvJr$F~I$qB^r19j8m&owH6dqBK0*iFxs= z2GfO8MI$FOZVI3Jvq7Eow3a&LQ7oSHP_1dqf@o8HBiwWqh2Jw3tnciLl>N~6>O zkHh(jd}r6x&OXzf_|M+VcIRi|0w(Fosm>zSerF7{9Pcfi!o>)1n`=LTPd}W9zLVcS(lAt#fiz8HAVuraMSywJD_T`*7~={^~Q4 zO2=o)bS&Vwc6m*&U~l)jH!VlHoZ8Z~UPQ_|tu}j-7PMf3$Fl~>ryh*fe$2UViVox{ z@U00Eh|ep_i@kAYCqp31(wR)Mue_!_u6ufS2g|ufAuCxJJaRM-rm`>?cnMg>PuO?h z_H@>b^LFoh$7ucUA;W|lYz=~oCh}c#S}u9;G4tffFAO+O9#Ecm+=QXYTv^dm^7i~X zi@$ERo(w4y9fYO58yr6URBd3K@J?&?i5SB>UK2O)OjivxH`Y)PoRPvXcX7X)oBGaM z%tE)_xO3Sv6t*!+G6d!uTP<3s_dIKbmS@s!roOt5`BTH+8y;gkae~Q2Xobqc12^Xg z6h6~Tl@UJ2$YZtmPoL<&CS8vYg*Z)!V_Cow)*t=+R)Q$hZ+{U52#AH2Vb z{doSLftBN=b4@CpMlll z@612SAJmRN+<#~-`;lq;1oCgYAH2`Bwj@^g(0+cC+xul+eu&QAHc#}!F1_Z5{xVtn zSay75TfI7Cmu+viN|b%;AIFuMYghbex%bLJetZ3gg7!D*{~1^ce>neVV9C1jr}9Ji z!}d2vKU_Z+R<@txKf}ZRJ>?(jP9NMh&+bQm>DnKU4{p8}_41zD#dmN2IDf2nxb{!{ zPQ~=dk8;}|)kYs(pT6j}_v6s&zgfplt~+394?0`d=^|e%Spj z@5kMb=L0{oALZ7+wdwogd5sVJg)90W`u4W%Uvs;{-0PzLp7^Zm{~7pfB+EbSyMDON zXyZrw_IF=auD5Yt@nhOIe}TRC+lBob*B{yc5m*1#`y=ur`#%Bx z&i@RX^*UGmsp$VE_TfLnG0C^3`BJT6Z)Tl#s5d5uKZ?{nLy%lFoAVcBQ- zqkUQ3#omvP5AS37=(q91?Zal*zps8!Ezi#{_44zb3h9GY^Ba^mf7FT7xaiw{yC%5! zQHJ`pKWCY=eNC8uc>LXI!ry2o_IKm{%=5he&PZMVqY}S)U+c%?kK(3&NREy^S*l?w z-(F^?do}luv2EW<>yvhCm(`~)349r~<f>~lKq{xjvvTxju)`g&a7vx5ib5H|LEM(2Yb2RM{W5a-;|TIbKd*->$5nf>aK0w zJN4@s1I{IT_iVddYCYRJ^Wo+ETX&d`*Qh`E&mdUw`Iw#52k~Z;-0&m+^ghn-Fv;ht z@IO3DwD;J`wLkig{8QSu?NSLZyW8cp%9CCPT=}{-?$c%8%elIbD&OU;Ti9pO(3$!xla4v+5ZfpHvS*{A1`mH@%*s+(fY=BC9nM@euy94=D$4q7VojuOK-fsm9cx< zL?@0?S@zINUu*4Naq8*nZ+ZRO`?vSZwd<0y7+%+e|JJO#@Q>vO?}yjl7Jh6$_@6;G z19Y?UexdApd_QtOmIqvX)_&lh{zw0|)HsR+dTr=+-DCMk z^zyy>H?^K`8MJCR{!aVPkdc3@@Sl?F2j&Og`|I>PKg>T+wf@Naj+GDc`R`?)e<;tC zvD9tpBi%@yt4sQ>2_11}@icw=SY>5ucELxFRg6#Kq(0o+_~<`_P{u!@56332s%Sq_ zr+W2`js3&9>~34C|Hj8%FYSI|@>bsc@|v|rCf~jPs(iaaR8(}dyUpc0XGIL%j(%dW zRepT@&G(Oszs3Dn{m8$`evAI$c+ncQA8Q|qUXDHz$NI6g^YYfaTDJM_Tdr@3?tB<* z6Lc%9%5HJus>s-APu;C=d(Q;lK9@W(g+W%lV}C0DE%ih3o4+^NXYCjMBlYoL=WokD zULW-~J~+=%ld}J!p5h1f?mgiPPp-ZF%F3*Nt%#w2A?x+F8sB_r(ch+T&$s zt!ia_wL1>=iR$@>Kqm%lascwMdI`**RP`%mscndYeUaL=Ce@poA{mtuJ_GjCcKlsnk_htUg{m18- zUa^y@Fn((&aj`XJ&*y#pZKBupDBmvz z4Dy?gzuEcQ;omhmooh9mzb$GMK0ZITUaF$_;6H^Q&Fg-|Km68Ny6&}Aep8LXuN|B(Hy?1%Zs_OsQg-FR7Lw!8Sjd)dsOmsTq;{Bca0e#>rczD!s0&W-#0txV_L z%M9Y)9evj5$?=t$->-dO@U8u=_wRyL`J0bR|6L2O=d8(Re{+BBkDvR*UdBtRUd$6d z@cq6((&fCb(f!=FU;QxjtQTE2zpgwXde1fUX&cX6FPk3V#F+mr@9)(54F6l-kA45o zz_UNCzB$hR$K8+oJ|C9td|c~%Xg_~R^xE>!4Ik#WKiSY6eEr>9-Gs8XL$`j1UMksA zK2_>_&t)qtdofUliKaN!;WLy$BUcW1O79#l;tKSXDr&49lhHndhVnfx?hi2W$v_^ z(sYI)@bBFJ3~BoP{~5L}KT`N%^0%BHo{#f;e?xqOsnJG{xPOb;G>B=pTdtRYrmyz`M_Nh9?#A%v235hhc9yS z*)R5Fg&*o(d**=rmFItCZ!f4nXjW3+7{A&2kMO3y^G)h+eTwh6#&+!RgWUa%I$JCM zGo(xJXZbNLHtIja@o)RsI$h?;%IS0;IRD|$=h${teVe3xV%xJ_J~AI;{H6HYv=nC_^Z=Im_L61$Mg5%{s(hr-P`@!|Hi*dV)bmM>u>F= ze$QckC|CJ$>GtOJ3+*|M^{$yO=J{dK{bPLJKL6eyHOlfI{59>gEA(zZPLUJcd%69((3ppUuVzQo_0|1C$J zb!~n6TJdk|rax+HS8si@k3IU2)t2k;>eHp?GgWMOU4Bo0%Lj%omJdGr)cOzRKB!*n zWSnbyWqa5^NyC?iW2+7LFUsFYKRo|~-E7v|BuYRSHF6w`FEDdi>Z7(aHajwU2E@o@&f)THD>o%g5omrv<4msFI?|<;tmiZs&)P;4&{xdMJAKYvIEgiJ<^4X8657~Z1W`5A)XNdf< z==B4Zdy*ele3U)6p1sj^Pf(3s^$Q!lTe|O-%53X>WSl79mJ;E%<2r+UA?QSbE&4wc z&OZMmu>Ihy?MFQJrxt&>r{8E*y8rFM^?y8P9|-)DaOt1zD=DU%HEx@(mdg5yKN6ez z*m6&)+S1Gqe1|;*vuE|S{Ns}6e!K2J!xod`3dSFf{~6f&UdDg$Ut9id@!ypu{~2!i zU4N*z`QtQy{vVwCQ(p7;2~-q4(q7Le>wWy~$HG6VnUA*3K78F?VAer9$FyOa z?Em`fKa|;uS!bH;UOiRH^J7v)nJn8&soKC)hF`^?(&piRh98R0KoZr&-xA_)muf$Xt&EwqtBb5Km-lhL8#NV#x*Q-yF z*q?sWPBQ9M)bYk;*G+!Z6k5tLCYbuS@PBxaU39d*AoZg3v9fu5|BmnK516`eH^bNX zABtt|{~1_9_WfsQ(#qU^Ut-IDhMQ8qqV;(yPTAjH9`xgA{)5b#%EU)Ax4swh3O*XX z>r47EGc(Y*- zPsMwgV^OlWU_+t(hq!M2AF6ZP|A@1HE8O_Q?>@J#9pm9g@eR}VZ{NH9VA-YchkN@L ziXZcyu}|mkJbNaaTfW=7b@%cAZ99K_nw)eK_X*Q(O`(4#A6src-ogHM`yZK$58wY# znFiW%x$eWYg&*2(e+#+%&oJgyJkQdi=<^UG<;gpp4M(M_2pb%wF+h z*=g?|jplC;SAV=vV^c4c8UCqa^AUlU{*SEZi%nktVjo*R%dHH9KUULSrtw-mO-#G^ zG3YK@&N%O_!!| zug*UD;dfSm_oGTvlLqE*&;RkcU$B2LVORYh!C%)yeki8?me2gpAd>gaPO$#4U`!2P ze0$)J$zc~$e)K;$_M=<9P0xmnZQ-gP5`Q%FCGPxa`yM_ipMTqCv&nx~D1R(HHsd;j zl>LW1w`S1b#1;Do6U}e!Kboz7)9U>rw|VmgRsQZQ0*u5wK{=~fA@-i ztElI5T%2k4@}tzf+W{q8KTMKkJN}q=`(gjLGfPifTTJ?Ak@TTCyKGbTbn^@c`%?P{ z`|rv9XK2y^T~jTUXWHNL%l}P#>Z8^F8TkL$$#1x}^n;l`)A7f?{z3m4T9zL+cvVzk z_4wdr*EO%^iTble|LBtMIRD6e*QK||ewH5m&%n9!LRbQy{XgE`3;Q2Tx(+(o?Y>}E zy!cxCRPO%_y#m?sBG3MAQvLB>{P4}MYo!l0AFcXSAr_mJ-|<#uSNx&PAI{xAyj|?O zm=ZtN{w~$)%Xduj7dTvZv7t`B{vgkq{ST&HpZ_5?{ua0Yk52i+k^dReo?qLaq4A#~ zQSjrY$G-Ip_p?t2ZcJSDE$hb?mew4);Gy_gox( zfj#~|1G@((D1CFa|8Xh*HjVhh*ZuHV?@G0K%71s{*r(sFWxIFlu=jt4mgi1VHrub+ zNoqc<_V3|-u;k;UY_V*qimr&{4@-}CCUokXtX$V%U-_Tm!A?8L{|rrhd-JI6vIkzv12551DoH{T)%udy=k|bmSy`=v%&w`LXw1-P}w6_`Bb(^*5Oq z(VOw(=A(~Mw>LFZg#VGcyP*D{=GOQhYWI(1pYJZK-}G$Z$A9OK)C=a>XI0Pd7x-f| z|Hv=*!>KzAe%L)r|Ix~GT`K>O>4*HAG4Yw7oF5+l=v=YohmyvbsyGPAiul*L->CNzW)pd`E}|awExq&vQH$c?&P<5lJg&?uK3$&>fiSA zf%uQPt7`nV*Q!)_{}beOlequLZ}F3 zyzoE6L7me28^(`q&mX-Ve{*@z-z7OVrH`ZE_uv0;A@Gmc{OC({0=F}zeC~k``&4F<~x1_ z{b$%1_2cp5Np;7&KXPR({h?U?QLrX;$41Z!B;~2Hkd5?sQyRo!?Sx2To^y<|B;FZHEmS4t^X}kQ+i7??~jW1s1uIf^m_fH+Msi{B$Pk8T)*~D zb4uiYhC@bb4F4?uGdx&yPwej;o524JtYUwc)F}REVEKFb=|}ei@`5$RJ1^{Ge6dfU z>v`nnYpK`QnY@-Xv2UNYZK7}S${&8OkC)F{H}S)vd*`fY>isb^Z(rOO-K;c;@zeXa zjK3}a~C4ao50|(0x*W=k1aH9r~mBA%DL~|IPc4)O$_++v`|< zoUC0G6a9nxsGaG>wsp6rx0m#$xd`07AKlMaF-!H+$*yOw56@HBx^0qoFLQX;4W{#N zKK|COyYQc3YkQ}S5>Zl!<#Zcy!!R^ z>>k10wX^QM4!OK0+2hac-Fuw_Hg4FsW!LUqO`Hri?!Fhd>gESuHi_l;-*?TlH|^7T zpG#Zzn%&C2{ntO_ik|Y;&6mIYwoaK=z54X-m5a7++1i@AW9QsQ5s!_$Hfpf1`FEjy zbNzAuo57F#7Jg{{*7YOwv3+yBu*qI)_p9&sh(F$|R4X6d?tOjBk5XOrpa|92$v0k! zU3#~D=l04(p`*napK4cKdS%gI^40%l{^M``zpXOlx6b{~aL8WvR^2%}&5uv+^VHaX zob}vomON9|J-f{t_wU>L;b`BA)7z?V1bbaw6dC?kEV@@YcuQBNzD~aG+XnT&v;Q-s z%HPudw)f+-l^@=JOZ%b!k@<0bUx_@&>wR|D{uqDw>^eX8s>yq{A7!~#mqV_vTl=7A zcC~b^d&DJ^6*F%>y;9nn?0K=|$Fi%Y+S=I-{48C^9%bLo=2$QCDpCQe^fBvTMBawDOb(i8f|D=CNeiRd> zH~EpLjpXCCO0mijQha-b&o6$p(OuapCNhp=T|(r-iATbxg>N&Li+#KFia48F- zfp_b(7DphI0|9V*{y;Q7eA;;V1Se}+S4`?rMu(PiH|`|z&XEg$dJZe5e`;qLxF3b|<<>`&DH zNHK#7bc?Mo|1)I1mcJ!+_`$KQzoWNpet2V_%**}Ki$8>Z-SeM;)vZ}M`%;b0hL6oB zvrE?;ePr>~>__+Cevx0j+nWC~aP&;_JAe3! z{}FEfC$_y^@yGnX&8126H^2Jh-u-az=U1KSH!*3s*EIIt2frIO1 z|D%e91{Kmt0{mCr|FGWe@SmY6{l)#w$KTwxKd2pl^R?&Sb$N9vnZ+4a^#@+;VNYEZ z`|s>_JI;?TzxJ(sENgyX!%?1j;?s{@{2^!G{{7d3__yryBjdSG7cRg3I&*nLU3=Z} zd!R!-Bz^xguz^m?4gC0|e$#{><>qe=FYLc19ewTg!{>kWvg>bk#kXtzXW*RMwy@I1 zu;Q0}&&EH_{Q>#y*TjSluDuhvu2H;IarcM4cNfPr)S1;ETreyCkMRDOkN+9o`sV+3 zbNrAU^mj#`_@Vd@OSjzb^L+4nbLj_A4_@$2jpKiY*23gRKW@A4`{=O0?c|^Q(k03j zP5QjinbXB4-hOfa!(=ALPwH?cKti~KSSErABxGpO*7@U%3eP@pYP5Ot@{sB=IG6vnx?912Y`fIc?;W{+ zxGo%4KfHTxK?PSl*DI+xLDA<||Kxnw_FF3ZyC6gDSI{o^56AxqbbbEE;r$>s_)(7i zraSYwAN^ols{S^8eZ`^p_DwbU7ysGZ`4M~jQBqb^bLT&$%zELcAD37ys*${$<8j@T zSIM6J_=9TCFK37F8#A~ zUl617kNdZO-?}>O`a}CSe)K;1aQ1nr&P6}!&DJSwpL}%NEYZDJ{z=|kG+&~f;pg^$ zT>jbr8CZHjo$aoD!Z&|d`L}PVJKX(||3T{ZxAEB@R{s->j&Gb6XCLcVu}>-bW%^;h zdy@SRXF1C^J^gWGmzK)vmGWJybCR#K1u%aL|Hrrg!hePb)1%bi=znndBY$~+TCMrF z89P3HZ;^k{Q_r;UM``M#==)9KQf3=$!mC#t{_r)q?fpY%XR)qbvd317-14gE(lyfO zI?1(f!x84!>wl=4xBq8&(6&APgY$j%dw;^V{GIgtL*4ujx&Ck0uKD93$Fl3k%%y+Q zXZslZxVrVM-}AFSnwP!){Ce3t`=-K*>b%wEi|2Fnw>3q-O!$1jp1uCTC0qG_oOjp$ zyB@#gYyVrx><`;Re{3qzXW_r`-uK6Q`L<==ZzZ?p%kMvO?Sjr8*Khqicm8-bd-gwE zeOPviP(|Bq-^oXdGa@%`yE2b~U*kVR)8@?mAH2Bpe^_WgSUbNZ-%jk{Z#mCL^^MKn zPW!hy`}wzq#aY{#Ups!(BUk?5(k~h&FKtZao7H|Sv$1XM57_ZBxpHw_Z4$=;`}F?| z4_=<2+fkEST`GTg zYNY-@9{c9Ry}!j)7C*13{wG{r&*{3@XR>!u1oOT62VMW<|1&hny^{anQY#$%<8bnC zrB{NP_ZUC2?BC9|^kYGV=s%UrdV#!B-t4!5KW2RoOZ&KN+NH>)OXAt34;MQYn=X1e z>4*A1t)2Ow7=J|nXJA_rS%1)C>wkvZ?cb(t{k!PFkMjDDDle*->yC!m%P#mZWtQ!` z$^RKR^}CgRH2$5Q{%GRgM*CypAD;iwesW>$nzu`@zAfS0Q?1r8!DUvXf=u`yvEx4f z8JYqP{%2sl^LNgd`|ZIs`WyaEKi^TubgS-OV2$*2-}Z-d`#a7by&K2hotbivbwXy@ ztv&Tz`ya~oKeCY%Xz@*7xopLxypK(Z>E0{f&vuNLcYm;? z=|kMBa)Y}J<@Y~0=raG~I9d6hfhD>-=*P+Bb*H4(t<79q<2n7?(_KHFiXYDWQ@P~T zq>AH54PVxOm~wI*XN~XjA9?<4Q7?jO)U8xD-0{!N&aQm;=f}sbE*;Dt<^S<^T-yI& z=B)jHL>7OWe%a1Cy2kG6S4O@i_KDX1eHVX}eLlQvZga)+@PC&g-qxhb>@xk&aP;ek zlsLhB4)=o3*-3}|Wj&YNs?XB#=U@k&2Fnsve^Afl{14smH}}&&?6i})RU_LS^ut*G z?f2vlo?njss86rvKgK<~zN4&3pXsz{@ACfX54CLUY`PCGll~pO(q#Lt(2wcgUQOV? z9sk2J{_y<|9=h89IJK9={}H=vpVF)U=65>3_Js_gA5E{1NpJ1de)#c&#D||Be6P*< z_;R~>Z?W2x{|q0dMP+45us^*2M|$Rx`hy0g@;|h{ztvv)Cw%h5eXBn_-VE-KRk3VANp3ccmM6BlinY>oH_0NBa63rx(77t z4<7%O|3{eH4|G;d-PvpPoU;$kDnER+{o&g6Z{IKfcV7Qt;)0h8?et>ZA06A+en{7D ze(J|r_O1UJZgD=EZMrS;!EgH}*0XbVEq--z0{@-=3_mRL5C3QQ;C9yfAIFPb{Rdy` z-;}$4?0Z-}FaI0;`VYEwa;}f|x}W;EzU|nq8g>1~=-=_7A1r=k-P7woHeF6()_bR} zkGiQ>!~Y3)mKZ9q^8YY9eK`Ju`(F2doVIJS>_xu(XSnI~`_Yzgd!gCi9_{+OAo}5% zTl?lN?-#F_q|bhO?a7Zyo+%eWr&=CbThBe0uh!_%`9s3l`Hwo+Mb<>kdNQ#AwDaO1 z%Ub&f)1%t|h|7QL+VG=Q{;>D@oBJa7*{y#gEq?rWugs6M=TZL|T5ms`v5)_j&py+{ zC3h=sy?)4@{q|vWZk^?Oef@jc*(I}&e0uru4nt}DhvIEh|1+>$zO?_rvii+=b3Y{f z;PzlTe0bV_hV8lS2Q$CGYyOAW^?daQMc>pv=$_TK{=>G*-~2c2+5e%vRr;IERXZ;I!$&_pTmNW< z>03UP?nC8rG7)?HS7(_0lFk;W378(7nVY*{a!|hb44aHu5p$T&)IYfXm;E27?wb08 zMte$YKUVwvo$@ul?^k)pKlOWc*S5#YE&h>T`bgLO;m>RO$sbyuE!00Ev3Rog4|nxO zwtmBTXLJ96P7^oy$asbE`oi)B{7344DA{iRBeW_0hran+&h(F3`E5aUXZj!6-{O+L z5m1x3^Wu{qvF4phKlUy?`k^4+zE6F!`<|=nKZ0zW>|_o;N}uKV$KjXx&CidnUw9C} z@BN>ldDWNrA8PNKe;5BdE4F|0rW%7wdrE$#?PhbuDzw+kH)A~>|kF_@2 z|HG8PA4R7hP5zPh{E_^(1--&Y%Eb&SyyAt|1bHsL@PA{y_K#rq!zuSgOyxViRb6YkB{%Wm?Aapm689rH6>F6rKYHz} zwmaKLg7$(9n5BP3)C_`tHAt;{RQ^CTIAgv-Y>w z`QHwXANKBXUhqew>9&77$DP~Wk6%2Ubo{}hiahh(S4?#;W_Ui<{1Y|n_Qvg(&M?UL z&HvzO&HqDX>i0k5zrQtgKMMKJz-?z?Zf==#3J|B(F;zb*GUPx7^Ae^hyXykB8g z-~FTFa$veM{?I3D`?b@?eRY&;{_^M_{lD`rAKA>Z{nDNb{KxBmsF%op@c8(jfnDT} zZ~xJ#_QPU-+Z2B^e~gO1nNj1qbE7=V@*gwuwj_Ueb?w*VBU!slk2g#YoPW%)#@t_M z^~>TrQ5CFuOLk3sYtB&1`k#TlE@S@(|MPr*3jfZ?wNI<&Z{8LER`hS@N0GWqQT7Kb z_82Cz?r)9!BUC4D-~Yx&Y2!<&{N5wY$8wiCuQ)bOu~sPJU1`NSosA10rCz@fXG|lSO@8rG?da%N z`@Aalxow$#ShIxjr|oaGi28%F{(odvKGs&<`y<`_kMK78j(HaOjb}f~J@)+1&?;K^ zG2;i*ftTBI?KUKTcwD2j<;Mw|r2TCF8MgAw6W?(;cc1i~+gl z**MflieB+vcXpBYx|1vOE-kqb+F+kupRLbTe~|T!{exyZgR6Vk<=^a7KVG^^$?|)d=e!SQFTjRD5it`VMwFhkIlJ79TFP~?VEjC^M$X=u3zw5J2_9@EgZ{GId z=-k)08tO9Y4=%g~>hv73XWXB8TEAZ^{ms+0b=SH->izTXw(r~ULpk6&`#u`c@0aM1e8!rz9Gb?3wCnM(R^ozjhu@b8GKKbWycaOv6q z3|wx{4^R8_qo}@9td8%Kj`ChP1u_2*|8g_kHkEjme$IHmuy&3D`)l{3`Mm!b4uaNx z9*}ysbV>BT({De_v%i`3>-<5hZ&T$D7k>P^KQrT>l-DoUt$j&+y>~=m&oG<^I*O?FYN`+n7Cx7e5)8}m-(~oYhxLBtg_u_-<-M)$W59QxxCS1;Y^4o#^ zxn4VH0JU}gKfaXxm-AD9Skyn5J^#b@dAD8NcU?;OJIDTE($NoDXCiN%wM_r8@P~$d zrvJ9%(x==HU%T8Ba8~+{Rag<@FUS83>@^wtKltWyKYae?X#NMi>ks;-{!ZeTX% z42PZdk380y7FA?F>tRyYS@DM_yBXxC%74gk1Lc4l*SGlJD!c#q>;8jt)4!dtQ)}|) zetBO;;e+qj34iB3`Y^X;yLmkK+M1N}xwWf~KYI2tX1WeZ2g`yskR6!yjJ7 ziiSH*KNR@mMRwcSJywekJAK_M`r%%Q;IhpOb?&alM|Ui|<#$OU!D^H2sZ%#pJ#qxx zI1~h&8623loH??QtK8UJ4?p zkllLE73ZoS*b@RmQG3*zs*w7TDJ zQ>m{*%r#$jY-iEth)p={mYxVC^#=5uqx(jzUrOY#(z))5%ZD;W(>hI!x znm^V*&OaV6|6~56`A73*<09_=*nhmv=*rWpdz{zbdATdLt#kS8*Eg9S$=bVJ`tnxU zcT1z}0U&7pc3le~Wr=9b1jn-GOb?tNdCf$Z^ZuJ@=H~DY2ptk3ajyk5AgpdCj4(0!dj$^r6Ct2eu$NEuk`NQ`=eD<)f?a7LhzV@GC z^E<`gu~YucxZALEYx`ev?(j=mRh@1Ry})Sbh!NB3NeahEy4_+r1T zeTICm{g&%T-V6On|M8#Uqx#YLV*j)%_GEfLvX}jFKWxXfJx@Og|M+B?5aUTe2_&5Jm>S!I{swU2G9zt#NM`S^T`)b%6!Ot0m6{-m$`!G2`c^tSgl?hpJ0 z{8Nip|40v+?>moEujKUUxmJgs962MUJ=;UA_2QLj3?J7@M_&13yE=2%yYAY`rP)zt z3zJu8{8~4+yfAsi%cym;OGV$!Uzz!R+9ub+%<9x~mT5%XzjCf) z5dK#8cX^#%-Klxn*K8_(TmFd#UGP~ill@Ql+8*{bKQ=!+-}zOaJG-jC>5r>w%dP8e z@A9{tRGRta_wBj*etU24vE8pJiTCh*!HmSzzsXM?cBDbPjbPIzC@E?XPJX%R`o5A+xy$G?oz#+eb)Jo z;F^fPi|e$1Jbr9`q`Oz%{es%Z`A1}{z8{JDoH2`aR>VuGsQd9Yla42+a_v5z*P2|t zyZf$FqTb>a@ptbs_}BjStEsQTkxJAyC2`~zj@s8=#kb!o z?|+xGzT|hmXylLDJGUKId1tT8Qm#r8nIClJ-Qw1~Qtp{CjP>dFZ zx4a*FAK!19XLn_f{@M!lfRE*e_Y2(nQ5?PJ!}N~2Gq?6N+TNYK`sDI^F_97XX57|g zTev)PSMTE6@e$E63hZ6BUr*oD{W&YJZ1dU^bG-gl%uV^c=b@pRqxtNaHJ6S#D;gOa zPddVU%RVFiR`%on3~!Sk->}oHyA&^8Q~7cIBi;RNU*b8OAM1C_Q{4YiZ?Ak?jkV0l z#Th4SS9t5JPEYwBzTMUFv))(kzs@21CcO^1qQ5@RLB09gg1-xN|1sYAcUs_2%*RJ{ z=fdY%`*&WqXKn8^75wn6Uha5iZmM|avkxC;M0u`sXPvw5TB#xDL+_)V`R=zenD}r0 z{x<9H0{c{ZdC!mD-}1o$FkfK#h0^)l{*Ugr$0_aqaBXwp!}|iRGF98UON*1Wq}N)# zT(EWLZ_`s}WyHUQN%d}M`*-NvGX{RK-TQW3(a$TDe%;-D`t|M3y!U$V(kj=jTlY?O z>5g4Hc1+l^ImX|5&t-pQn@y+Qs`Onlwq)p4*J3}#cPHm$j>2jA=_~@yhT2=FI_mF~ z+2_DqbKk|Gc+RI2syq?iGIasll2&H@(45rbgIa!^vV4p=OdKrFSsEP} z>gSvYjhLnr@?@pTbM`qCZ}faJ;(XR1b7`|ePvaJ$1Esf)OKN$lIx?_HD)yhbnIpi^ zt#%?3I4GCv_cRzV zNSt?I>Ra+F<*}s7ye|yvet&G`^4D$hdv;p(&ZH`tB2V>6HxwAyIV%EA$%YoJI@%D| zGI2{!N?&2$lIMkK2@Iz%XT5#BBQJ@s$iT2kL{md#bD}rT(-|9>B|jZ(;NC95Y}@A+ zZQUu$sBXcK-sr$*q?z;P(C1I@)SHUG&oDgaX{oR>M`%w{gZSCNNy?!~6{#ws5BjHD zGE6G+V3v@JpH}3-EP86A#fd!0q7Z`N-jS0sf69h~3QU$S=f^PI zjA>%nc`JCmvL0>1m#-62N3V{W#YG9^S?BGhfv*s?1?9ZZKdF zKXHuXq2tfaa~B&Y6wl#FD2XXN&cGPvqj#z6@H~%`41G@DWd0m@Y{?+A?r8%1&nG#G zWt%3R+WdvRsKHZbX|(QyBVt@0r-% zi6g-wXJyQWlPqE^4B@vpw5KL5Sy%YI$m-X_K0XU|iv=uv&eLrs@0c^~$+qH29C^j( zZF-s~_%e8@YZ-3zXRTCW;9B)0N5fG;;AdJ`?Ba}69tY)2BLflrq>woi=RE9GFF3!> zpZlc3GzKB3W(Li}>_H9|iGA&o42>sPgq-H|?rF&SQ6GQJMt5yyobrcnOIKv<`q#Yj z<*#e4YctKaOO>r&oNeX%#Qay<+NH0=Cb^a-ugttY?PF}jz1yC$XP2HSJkQdprOm+a z^W)=hr$0G=m;Y0KEiYl8;oo`B=*Pj2?neYNx6L}<_osSYT=qYueVZ<>Nw{p%Evdg= zC23kz-u&GB^wdnQ!toUe)xSv{GQA;(|opy z&Ns`h+1MA?CaM%|oqG4!%w>01{%43!uTIRaPBz_oMO%a6<6P_5D_e5IPRCqa_ijsX zywBb(SGMf+_S-e@+;5*NI_X`n&i(e^-S~LkrBCNRN#wm*mpD;v%Z@I~NA-3lQypxr zAK!n=|8e)X#XtI2)x`f@xKI7Z@yF~(dGpSDUj7;{Q}I4(_w_%H)yrQ*mF}Cm_{PcF zs!tx&}ua7FBC z4VL|zpTBkf?O1n8PXEe3g}Sr*#sAcQ@IEeez2)nArq^=XKMEhkIv=*WxAdXDfa#uH z>l(|e6IF^lm&e?>@pAhi<dV^ zNb|yse-d`aKbqIy`_UcupP{Ax@COqqyGhfxU-DEb3v-H|IZfi!n(NccclDm1yW;-+ z*gFmJ`L~vT>&U-|@&!LB|{=gjfdLdReE`8%+`u$#g#Wk}pg&wu|@&T(6FAy4kP zw;gQl@+uBIc_LKoz%|K;hv(}#XR8VB{<{toR6Y?td60b;Lu6WJ?A!x-$8`=cL_AlW zoe>Z-FLDE?%Dg2Pm`qxy1~9BzvW?yM2&+nMXo$=-OO_^PK?kl$QxqP|(Yx~`g-1?g zYJ$k~si_HW?oO@@mdoyN+*pvu!VsNtFv*Qmr|N;wk)E9=(@!w;SMtf!EcKe$!^HKV zM}Upbon7+l$!86MGt3wQ7L>8fncOpZa?b}#2H`vNG?;=5j%a8tQQEX<;*`+jX@`W| zJT3L=iVhei9gt$^jaixRE|JR8;P&8vfQ5q5YSx5sEj}Yg2F3}~`6qdBuszdpI~p1z z$0wq}!B@5-RCVJdhW`vEg}=AR2sqqX%<|=@NAj_d>FL&<41ER+Z9x$NhE0Vh&)qxy z=`BNY^Tq(dV+;ovc-^??tb5bfGQs%76DA`AfB6;975G1Q>Urqz?Nq%n>A-{=MP~1< z8`9LJ@30lzh+*J0OwrsL>UX*Igk-?t!kD)w8J@KAPP2LYGlQ}5!;V{=PuNtRFd4WR z_dSV-~fT3{%#{o;*pYqdJ*m5W|cv!MEh|ZCDC|dmYPUHD)9tTd|?ny|t?qF+R zt36P&BZ1-Wyj3C$9PfJyiZ3(INox>I(ca#_tB6CzK&8oRo|CzZQG+AHqykn3i?fsX zr_GURYznuqROQt07M>hk6=ne}IY;}ds(2z}%5v+}@E289~C4|^S1*tEY- zS)H&VhRIX0QsQTH!AS*XkBH?m3WrvzG?;ImU?sq9EyH|ej^GW(L%D0BGgKxOdFNZ6 zJXxw${-k+t*_^)Fkyjf;*U6sBlP_*noL$s=``o3Y4c6TPWfQn%cv!g@B?=@aEoeIz zuEVY(ldigP;+@I&EuS^07;iRAJ1sQZGVS-i_-W#ncee&}vAxMh$>EPW8_&HAz*NWMSvyz79X4E z@whqZEB@*2sh8(W+@5)_JWqjj0h<`B$E}?fKR2FV zc|eBAf4Rho<5dizhnl#a%t?5s%)rKyJfSEeW#SwrHgjPIzRzqTBJ8q7XPcWiELe&g z!si~&Jk)wXfpN|628jbK6O23p{K^)nybYY>ud>lH$xnkJooAI`9q*@dUn#qYTb)TROES5 zW%9fq2N-xOxf{${Lv@_il{MWs$EbdRXU@ieg`Kx}87$Q+ZfyFhoO#+pwQzf;)+w*I zCuE-RB)khvU@*De)U@EO$|pYqr^bfe1$R=mTW{FIv}6Ob%Ni9U{yn!3o<9@ev1a{_ zZJv6P+Xb5^c}=xYV5~X7=fuEx?#|qOJ07zzRWtG1$b5F-%e?vTLzuNw(Sp{%-i6}t zj`xQ$uYAFIf+g!|=w+66Esr|~Qp*#jvh%pT<7VC>cDv~&16OLY@(Is7Z_XT7nH2MM zUy}Eqn+kJpcc|+W6)`LdusXIx$eh9B1iQsCudfGxPHhV<;%ZvL$o=m3DbC|DbAA_i zO32L7yE7-fr-`BCHecZ+yO$>=Y&#CJ2o!|0N!S)yEMTvkuXlGrU)AKk9~KhQQw+K1 ztT<_1bYuccCU;7LhPr~DgUoY@lk74KRSitdTNoIGD{gMixI2IU&du8`Z*R9Q+&yo` z``-Je3|c2brEC|h7P#}gO_t*z^KzNygLaDwc^ zt_NKHGPCr)NcQT?6)}fpcFkM8{6^-lw5iLoZarIDUijqYtk{cf#RBY^zbpSU9Mt;H zko}+Ghr0do_gr=7>bJ-@Z`nOxy2g3y<@|r*ukQ1$e_s>*(7sEoYTIhH*6jZb9Ci!! zrZ36b{%cCqlVs2R7q;38dt5hpeR{9{+p3y?tZl(wj9=^DuKdry_8xo;b;A0aas0P_ zKi=MICux6}*XBo(eMeM$Q%#+X_hb8($v=)qUjAdb!1eK8-7T@!9-rR*ab5k=O5}PS z|Mfr7sfjCJg$2Aa7u#@dIt#P?H}3xo%{8Dkw(~y(=HGlD_jlPprXT4K*Y$qbbn1KD zr9FzZAA^s5x8uBGr}gTdM5XT5+xG3#4j=xtb?w!4b5|a=Yu4}B<6HZ%o@;wYuBon= zZRz`x+BjxRH^pyhZ!^)}TmHzd^FIS8`(b&ZKc-jp{=`2x-*HWI#ayX(w-5d5 zxfR#HTDIK#8cUhf_O{Zw{2m`gC+@$vu5G62w4F<1?lROC{*M07aB%1R55e*`)_)87 z&(OC2$Jqz-+ly+fFX@=%hCkA8{UiN&`bYUA^(|F-d&`rTN8J14aG|d=aj|s9R$ITr zepk~!#CO^x^R*XR+ig{C-C}e{fgQB<>HB|%=KB8(4;I>g2-@Eyr~G%}KZc6xhwC~& z?Dn{7dYAq1touj(rF|d#v{T9cC)1<1ap@1Cd)J+>+g%l#U-L0DeAf34l@IS*A6%K0 zt$JN`*M=*JTNwVd{+RpW`f+)-e~J~CABG><&zDsv@T2*|=Oe$~H`k;;%uN!^U-?2# z?ZdT|<%_eUeBXF|QrNNaO3pgnyN@S%s2cv<_UUZiM}Gx&ZU9z(Aj{s z-<9RT#&5FzTN&!z_A&iuXilhqu-IP6K2h8L_Ugmt_3ZauuG@*-{!{Q_{z2{cf(|4--X-pU%hX8cdTid_R0xyxRC`?an+d(F%8sGx>g)&C5vUjG@I*z0c8fAG?8-_KX4 z`zQUQ`l0ik_B?fp6-~Df-2X7`kH!_7&~pA(JEfRR^Ido1L_g}ye{`SwN7O%#Qdw*7 zYpNHv-j-Nj^4H5(?bE%7B{$EmDOZqt{~`S!Z`FT>rU`$F{&txBXSkhwNVrC}>HJYS zrvD7BD?faGV7B&AzED(6_Q&^!Uz)UjWShAvGpee>E1x4G+fMz$((Tvws6IR!eRSIN zs$;W@bG{qDoF2Zbbmbv+={V*{go6|Ly%xFrPa!EKcUaAIEjsb;{QkU;G;Vw29Y_*Vyl}$-ly?5Vm)xqZTWBoru{@;%LQ2kwBQT&f{^CNq%j9tgy%0KQG`r+;HqiUY))wAat z|CBEO@ho?r?8jy8k7Au}`%b%mFzurL`mJ}vV=kM_UeD{guwt5Sw%^tr7v3c$@UN(U zuwwp){OxZ++fc-Z>9eknq%u9 zESbM${hQDwbvN(v)wfuQPVe0#P^VU-^+WN|eZe2oy?-41Xni0!!gKYDUC-LH#aq_x zkzeNfI9C46tm6%DY#bl$HFVHK0AEh7Z zXRP7AQp5eAmjA)MnTuc9i`psG`0c&+f=fnK2Exhr;8!o=rwXwIoBh?ah<<__9OYT+V zZqXOy%{zDgw^FKL^VK9i;#e2&d3ld}@}v9g6<&|tyIo1>V3KN3ovi*Pe%d%Gt{8Yr6i+ z_Wca+?NjA{n3@0M(*7Oy@7S&Sw-Z17`uyNp^THp;51ZE+{qX*<{+OJ`N3r=I2N9xlwA4=zoR4n(N9c~K7*eDZBrrPX`=-urFbIO$14vHjNmKSIvm4*s2BpV!XpY{o$zIU^@S;K39$ZY3~o&@m#mDU6noUp8W31Yw~Sem%o~2 zw``u~{|rr&>ksaf-(LPlSovH1hyM(3H9sat{c!$h z|8ViES@!M!l;i(MUE348*8ju&=#MPjnTxh}E&k}qTBy*&mVbt_o|dU!55)e#>IZhkJ`3uIE0u=G88>#mjzp`AfdOrBZe0+p}-4eC3@h zN-G`g4F5AUWz-*(ug|vsp=EwNUj7gN57o?v^~|R~)E!*^&#>bCqov2TFRt)@c&2}K zY(`aG^J=~JMcKdZ9`7#Q>+g3hcS|nQeQ*Bazm4ne#N53ZnHgU0!8rGK4(M*y{|wpl zZ$y7P^+ElO<41Lm59fRK>D0vS{ouR)pl|rYYs;%wR5(A{Q!ltD*t}_vLXG8$ACrAP zoc-JT^nQ14-XGIdk=eKMrvL3;bK7HjIRDZuQEM9H6aF(CbhCeONB;Kif1GQ7o4lOA z)&6L0^@Ha}^4rDAZ}Ihiw5=0*eNXf1R{fS+$Gi02mfU6jC#3RGy`{#GZS{gIt8)vN zS5(%xFYvBS7u#Q-nyKTRUb{TYe1ZJ=r|?RX@nVJdpQ zpW&cqecE2{Z_9p!KZyMoIVm6+S=9-87SiI(kbHI*ErT-b;SnSb$ z@Lu#*#x2pl&B-e=eAoS$^xoI~n#u1Um(IMrCjM*g3if0F88)W>5pw^w`|pnb3_16u z>JHcQev#+@VgBep!=c^hrGyIPb`?od!8JZIQ)O-*>kl!=mNAmIO z`z1_%e3fsuGx`x~w{gvn{72rhX}2AAT{Kypo9ub9^tHWc=B%S{Tl*F+xcky6_Sdrh z;^i?}R&^^gs~*Vb{JRMiSAMtblko*1U+1^KMwZ1

>GCZ@U%LK5A9u2hYx=Xza%U6{;1XCh#Tv+^FHmJBYrV+*V3yU?8o5tT=e(d{weAd~s{Y`eFxBqB;l>ejn&-`kQ z|GJ8L-;c6S&(7`dmOFoVR>B37xpA@Sp;xwiOxvG0E&F!?-}--Cosi;k|AQ&^Vs`R1 zCOlUtgRVVzhJEMG8+5AVm!Y4kmbw2!C?)lNYeO>8V z;otF|doD|fHb-7wXY$rLurJ-Pp?3D~kpBz^H`ad$tiLJ#&G1Tn&i;1$dbb0=&3}fb5fRVO{I`SMzv2 z?A!gZKjvfJg#QeJd(s!*ij2*wDsR>Kqy6B%m|uylmao%w@2fR|E7mTZQgw6Jr7f## zlOFqA-FJZTQ}~;w{~1`O{M}feS%3TTQTq>m`5Zs|oj-hk%lqN=k$Gkp?X>vCvc1`x z_DN;d^Obgd(HH-*|45zk#oVupD<95|^?L8U_2Rle=E>`PA7yUcD_CM(ed24hWh%q_ z{SQ{ve{ie6)&GxE_3?j(_In~fey*%nGlo#A)sBQi2R)26q{fEH$ zH>ZD7y;h$aFMIco>=l#sEI;)0JMN3r@l;eFnWwtz$L0^u5Avq-EnJcHYm50lp^YEf zRu(_Z=2YGLv2X6eAB%o|@%Gkr^RE2re%aJwO@qz)f1Jht8Q8x5yS4vA==@eY+aGs7 zlpp!ez`jqS#^6JKM=3u`rk(na{cC@$I;1c2$BgXpmKkkQr6kWUZepOu2iLZYeOnwEqwq|3g{+E$A}ei66@k+loKFw)TO&bcOq4*}mPyA09t4KdimC z=)2=J{XZHeiI3{}vZBg6w&XI$T=?U>bbp6Q<-*OoHcUDndig!;yZI}8a!a|Z4fr?i zGx*PNaNYe6LF=31H~d@wJEvloeaq%sKim(9$BWr1)v#UKIzBk%|7~1?DC(;($yKiYJ0zgyMxZlMswRPVQ&6BzK zNBZG>@z-`?)0g~cci45!^l#8<|4WHhXXmb)thT+DZ)Ns!h8oZfeE&F2e<%NO{?E|V zoMr#PXRY-&b}2aDK-= zrXRa*oo#R0r}9Vr${*+21@b~a@?@=Qs}IE~Z2aioUSqoY#jLkm_btz@Up_IjcFAk2 z%pH>sht#@zU*ED;hWX>Y1}+8-hQW6#GxIC|8~49i{&@ep;@<^1&cCzl)Af0rAFc2F zXEo`cH+S{x7hBfyyIuTJ?P)4oedM3UwPpN#-4FM2`?W>ybq}amKIM_NwRH4dr&rg` zCGfARf3QscgZup3+W$D+ANez6{Abw8*8iaTJpZ4d?blU5$RF`#FiOcGmzMe1jqyG@Iqc5Md zulC_{yZ-H7`0}3al|8WwGrfy`t#8lO*D;mWcHmcl)@i)^^WuMKwYTn5`tki+&gD9p z7xn!6q_PTsl!t#jfAqXa)<3}y|298*Y4W-@{-3~mu66C8^eJ_3Z|1VNm%G~5yj*ho zMV0Bg^^50vM{S=Uw0*mRym>|XKhEjDQ-7@g_9p*_+Q0c9CjGkpZBz5HS<{=||6yPC zN~&!B@|Vj!FD(05`k$dY-^N?_bjH~S7JmdeKVDtm^=4hZ$=l>Z!jailoL7^td^z!+ z!FDqEzGjxpzgz4dOxZR6@QV7(x5@{$|C9W&I(Nl<);~@^q96G>ADzdw@0v-(TJ`HS zy49ttGwdX;eOWK~((L=U@JW%cq(sD~e>3(hUlO0inO>!P!0z71^M9m1|7T!T`%(X! z|9;)AU4QDilK&n2QLTRTzF6J)!XNEN*H4{rOXSsU(u`O9A?jwO4puX%Q13iI>*KUCxY2&8{I@}Hq;Yeo7a`y=vAatc3! zKiqye{cvvTAI18|_q%s(@5(VbTYtRWZPWGeLv@lHKD4*mM6Y@k{PI`a+C^D*%2!gh zY}<4(r*y8(v0Kv?7UwTJ$N0Gow0&d#-;G=Ie`tX2Q1O0IBfaSMzboJ7ne6>A_uJg^ zwKHmjlb2=wlc<-y`^RbLM~_Wq{tWtuzxE&d_G|yQ#4Gw+th+OGwsiOAJ(5*&;IH`4 z&;(kJ&$~a{|A)#Pd%iucA9o+I=dn}zWBOxpZQ+87^hf50&ITV5m-1cTqf>ICwEI41 zMZed_^!7c*YcqB&*_D2~)K;!Ivvhg3RN3rmmGFYNgLd;d*e_47|B!wBkI1C+--LGk z-I@BIq1E@BwSDXRkK5`SI3Mu~o&0#9Mz{ZP|D)?yC;N6EnpEKxFQVQa__geVOPywJ z=7Qkm(KG*eTztXD_}TrB(Byv{k-y{X55}z7r}|_5gS6crc0W9SIKMTjcYAw{>qqfN z*VaDrmK6{9IQ4J)x2elpYAjP9+VlKa9eTCYBPV6cmD0{9TQ^<)GS}$evGQFz_Uzq! z`5l9e(C>eow!af^{AW1meE%P3!#$x3Oa87*{y2Ae%aRSgRmu-C)~w&7&3=SCUe^23 zMP`-{hCfOzKHmAY@Uq|X+=VI|PA{&{Jy^Wt0OObVKcd0^I4plB)gM%|G5zrTk^Zr@ z*$?L*ncp&BsB}e*{PI7ZKY~~OXnX8)!KU7rEXG_Ty@ur2#8>HcwE?!Cx>{Zc%r>Q~5rJNZ8Y%d`*v zhx5DarM~`WXss8n6R&Z*p8O%;nhx zgnqar-*b=ohxm$$<|7;aB!66gn0qfv#bVFvCA%y3Xqq>#{hBZIqs%)t`_Mh)$0#x)j#O6-x~i%bpH`~!Gs^&Yin#jm^b_rKD^BR@%+yJ z3?lhkPxJHM%%1hd{JNa{hjr5vAMO=C67_B4`d4PZa^EdB3CO&d`73QQSM2>P!#j6x z+=ywYR6kn(L*@S4$sgMPaj}Ee-J5)U&z5m({af`%_eCq>57)8vJ6-G9$MSL8{KwL% zS4!liGwgJ(tod>zp4D!GzG-f7*p)XWuY0*JyI)Vbx+YhDOZRS9hF{bFaZLs9B(J+! z|G|sDt)9Qm=ug&1_k;f#I_-J(lt0q8XS(}i{zJa4kM{c3h<{{y?{f9)4;$-8-Ta~z z?uWJ)MrK*nw|_l$;p=PGy<4uRUV6T5Z?osFd8fD+@E81@zyHDY_#c|$Z{>fBW!&GE z-njIKduHb2eCFdHtsmwe_*;G?PHEGRy&shigoo*_eDP~rFH4-*{DZT~mOeUb{^nPE z^A_Lqqtk+4U5jd)ZTt6F@85-Kn;B~I|1+?x|Ig5*|L>~(hp_oFe>^|zKQK4^fF0w9 z>jzis$~}J6e#F1)`>cOcAKvHBl2e-;)qQB6^rnp;-nBl`t&jL9x2wi--JPYgUQR2M zDs7uwZnu2ptve@|-;8glY=6u9pMhom-;MTZ?{8l|eE)-=zt|6Z_mARlB_GchsSrOr zpLfBBymi0&oA)W+s`1$6?ax`U{_sD=D{sEEZ~d_5Q|*dutN9%ze+7QTZmF%Uap#+1 z;{C0I`E~yf&Fg=pul~-iKd4j3SE2s+{IPhUKk`2&emH+**17(N@tdTsTxOgV+if8rm;bHv}tw3_vDR_n6qk7BLvRcn5gcH6YC*{6E0?Ry5>_-{%7 z8JgqwKUiG9HT_L&;_tLS_Pc)AFU^!Q_%Qp&_lNq2Rc_w$G zoxJP0?*0gNpMP}PhJ%$4R`Uz}nCw-Xb?w`wTBlqt>(y>M8GcOv#|aAOioYxOKUgC# z@=y7X)eq6q2kV(%%Cmjhs~`Mh<45jCz3(-1$SdL}ryWE`I$_<;oxL zN80=OU%yUW7L_UIx^!Ffn%Gy@e^uQ&GF?HA|33pO=oG)|e;4FG_}sr`9rJhQ{*2T0 zyq=H#+Phq`iGKK>LFk8i@a}7Sgdfjm`O)}Z>0(Xv!&v`-%Ug2&V$D9i_Pz4uWo~J# zy!X`{Q(OI@&3iXA+&-@JK%VCU+G@47p_<(bJ#{mUF|-v8qQ-7$Us@1p$=rrB>{v1hQ)D1K<0^q(R6!`|h6 zHr$WTxBqHezUqhk@jAghS4??TvZ8FSZC;WQb#(5(xvO55|BBnW@YOHZzQs%L9axjR zG?uiCdc^8WGtP*#qjsb-@Wz^Zq2{_`yc1W z-#`f22QL-c|ZH{-`~B_))95ZLLdUU!Pt3Bi`Y{AJ3(S zAMr+ee)zU|#SiQ7Yl|;$_4T^+Oym6?TdmhmnB;$`U;iVW4xVUr{m+mlz5mwkhw&Zz zxNGcw6t6b#$lWJ;`%lD2{f;lW$&b!{Z`rE-R``hiLf41qxqd9V>~vAL^sRdMhqudi zKB}H;q1k`RJOk@{9Wf}C%AAR3a_~X^< z$1(pIM88V^XW)GixCRk7RegM6=hSN-Oaz4_5Qu9obVcy%kkUGc&fTltVH@4jkp zI3{88I@UX2Bf~9FHC=yD!9H96hnoLe{s;ayZ$DPu-da<;_D|f#4f}r>KHA?hJ?h#1 zhvz$=2V8x4fvtPt?blWE?Ry-vAFZ{I-MRJRR(t;Y86W+&f4`NoE9Td>$*VJWt-Zj0 ztA5-1KhlQ(8Cco>w(U=`w9k?6lv;mqzj#Ibp<8t`SN=)-z<(H&W7>m0K0jnF8&xgs zvHt6ntABJCR}>$qDw|llGBR_Y`tA?Q_O6<%^zLDv?!v1k7W!LP8}OC?krRwdmQkQzx|*5{-gZ@KQ{Yb`KR_{@}cX=e%E!kNau6? zFh3gja+YY@YT0|O`W;_)uGPICvuW4nZCiG1-Fzkgjf0)#-@g41CdqGo{&wZ>?27x} z1b=j{3y5d;a~eB<-4Yo z-P7H>@x#B~%!hNWqxN0@%Kh6@XYF-gbF0=B?9TrgSV5=z+ko<%cYJR>-#@;Z?8yi3 zS^4)&GrIpr?&I}Cx6j6heYk%_IDJLtEw_z}UtZgFZSum`)9h3(l-g!HZo6ud{nnDF z^N}pqYmZ%5*SN7uF8xbqItMK5f2iey=2}3rH>_#^-E_CtTOkKKwt#@#RZ>U{Qt^{vz5dB5(i=dCy&|Iuvs z!l>-;;#IcOJJy%%6?UI~cvsnG^G^M*YXwU>(zah&w1Yu5|BtBsKaSVG1M3f})~Wol z{}6usuKR&`rXP+UoogO-WuJ77&yU#pHCc6%A7%DQ%=T(O8pZO>PT}Go+jTGH)IWUx z_O7+Gd|6zC>C$KO*JYYl`7K{j`CTxIp?39ezyAyeH^_eoTz}L1oAy=vT>Xye@$99# z(g*8=n~&Fv{uB5Sf8^J{r60W?E^k@(TlsEQRZnq6-1S3NZ}!Qwy@{>Zb^h=@jeWUN zm+pOfcP%#R+Nn<&3^vpMae_L^-|KGe{}8gj`JUO2&mX#v%;%_+sL}thyu&n}^`+E* zhNE}WkCqu%JU_O}bZzgom5YDm`+jI|*%MvPH+%7ahRm1OeqHa_y7khQ+t#a>w_X43 z@bcH{2!@*FAEf_r>is}w4l(Dg5wZq;m zAN4vP-Ys{%{zdVTuF$LK>bX|2<^h+#T36py4!a^G>#DkqpBXGlMP zqdN5OcAH}RL*kchWFK(WY5np4A-}Am|48n~{)fj8|F*UNog95->uOu)L%Zr*)^A(I4rL z=7;>-_H+G_Tp#yC+y7(z(cbtY-!@iUK7LIqb=vy+hv#`;*71GV-d*aLy*TpKF2n0v z?}pv>+p=}X)irOn%qTtmpJ7YX0me`351s#^a{cZ6kMaMwI9JvmH2$)lF`}aS+q?@l zfsc5Zk4EKpmpU)0a6a_QzO7{XN4=FF*@HexMcwr*{qXPkA*b6%>!h!j_?m~#KW3%3 zc7f+ZU;Bt%*T2k;UH~510`+u7?6cngP-*X|le%3~ac!T>kM9Tei$A(L`9QqzKG7^W zr7Je(rE4q7boKlNek8|j{;1zxW4R{7>f5|od*{C0V-$HW;?*pl-pa%k-o=@#GJh4Y zU)wKY|KR%kADZ@u{BKnKXZUvGV~9Ps5I^4^+sTj4_u4Vsemzf3Ub;s9!?*ih{jpcx zZ}qi%eW>i8;QS-|WH0m{+y1DYBeG&ut-r^H4ObK*^-a4izf58H5D)I7xqmwV>Y`QH zKMFsxzVV;d5BCrEA5K3UFJRvI-X`~BdBjK8*7+TFihAA4D%u_&s*}H5qxdjh?A3I3 zw~JrzvIl&WTU(i%p1kb#>se8ey;0vPbsjSOoPWsvgXe#S+v3OUKLp&q|3gi*y?INv z{;la=AJ?saD4XwpTYv(@bH`+hs>(Upw@oL+z+;fXT zO^noiGnd4^j_O@ES%H1)e};qM^$$AYx7Pm=GOBOeHUGG5{loP~;@kI&KHqm~kLE|| zhjEJ^Nw1ZX7s-^WUAK7EkKiL#`CT>k5BH|KUA=Va>H00(%%+z~9h;q7TFm++JZ$*1=#`@SmY6_&>uz&i@SA=YMGD zwSTlecAsUBeXl6?wx3;`3>4SKj-fzeftNCK(`0$FaA*0EdDky z^D+B}t^a1*cPy0S*l;=c(8fOwcJ@0z#)y5M_(1+gc+ug<>)&Q!ANNQ8NdGAQF#O2d%st9WYP@~~uJ|$Qz3at2 z<#H2C*VP}AepG9Jbf4shZ5x+nRrO6@_Bv`>W1;Nar+w>}zpy&8KV!@8O_Po_*!6Gv z&(Iua|6s}V{|p;a?6;@xu2HPDd;T!1rfAy_7MtL;^1YM)s7`j4)9Q}vyHSz|!$LCAeC+xpv{z(7L_TNq~ex>z(bbq+?@6>JUOmw|} zCm-H*Z1$SWX;JUnYrG%Mja>U?*_9gGN4D%im$%kxy?S-nZ(l@S@4ThUGk+~z`n7ih z`>p*S+|K{duJ8ZP@L)py7WZyDBhWIqsUK$_+%Htab)UaZ<&WW&KQ8xpW=CC0yqa5j zRyq8_pH%twKVkpmuW!-yyU<-e|8UjW)_-$XyqXqnw$WF0>DvYDYyLAF3-u-&Hq4xKG23Awh*&}sopnGe6cpO5J|6`@W3c&gDO%V?Mm^-YTnoFph8j!~YCz_YaxP zdfvImI%Sgos*24=%ftT3_2ufAY?tc!{_TOp{10C9e^`Y-n*T>Q{F~XO{|w1@i@!yF z`2Lo2L5kdM=rKYTBeS$3b#rpP9BS#-tz`0HQq z+P%9T{NdP29;xn(+@fn2qvCEh)TsVutUp+|&-C9B`wxNoz5f|L1k`#z{N7e)@EUZI z#`_~H?QH&-&-&%hocZhR{3HJvWTR?)ui8K4`e?dt?jyGkrT^L=#~Sr+>8nhgSPr;Sc(ceWxGp*1!4u@czxAdp6a`e5jlK zXxfj~wJ+t=uh_UQ_%S`=Yo2w z?UnD}KWF$W@pu1!h6iu*yW_XpztMecf0N1nruSia&OQDg%MVxSx7O+ZiQN8UdF|s^ z`;bdI>%P4HTX*}wJjVG4=1ERIB=lq1Yrlwn*Vp_weawGBrnvYtLD$vyPiC*Z$xyle zkC630zS581{|Iw`^Pc_9<0Jc<{fp}^%oF)=ZSSM^f`3%6Uw+i{pMiU;Jp2BhQtSId zS#@$BMYq^7emw9o47BnwZ|SP+ZPT}v*GXU1aa&!P-o4}T<_ZRT?i$aJj}O^z-TwAw z?QiG0o7?ZHT>QiR@&32&?nnA>CF?&-_uDZ2$Sw1xKgo}0eD#+*`r*0HwK#9leHZ^& zmRF}fjIQ;4AHw8XVZVHXYAi9fAp{ZH!0BNP-1nbrrD`<-G1=B z(@yi{>tz-9BQHF@WO8^_+^e!<$31ud=KeAF>yZz4ZmsKG^txP5)R*sZ+{G_*^J1NS zw=jG;-@A+dkML{Xzdid?W0N1$_u83e*Kg-rzvhq2e}=ZrALe(LbZh6g{FA(-^R@HQ zkGdM}s#$7tQYHsnobIKZ@`BxaQ=-7yGOxy?491@BEUJg=%v?ti3Xe zue~s7pMq4|-rNQHON2bNIFaP+xMe)bV`UkUX?cN_V ztLJ@sean}RSMN8!H8qdbdwkUFo>XpFKHu&56FDl^gwAQtl)JNg1N)YLw|}|6nY^g( z>U?pN{u|Lp_6yX+|JI0nU1ej>QRDg1@5IY{=2uO&eYn>4Phoc0*7=?v{@#i@`k4Kr z=(bk9m5VZ^)}C))cxl~MZ|>-q>y!fcLw_t?P=C-y=RZTj{hO!be|SEY@4dDBj|g|j zkIu)Z?Qc0BoTq+q&BGtY3u`R4?)}L1!w9rYVZp_>Jh#?9?f;`_C5ct+Om7X<8SxyI!~iM-Ie zt+(~tK2_{Le6ix!=R?Ya^_-GgZDTZ;yHUDo?igX8ZFOZ2j}gcJJe*Yi9M7 zU5pjomO0bGZqnbqCG&6Wclf)?PBr8H2KFvH)r#xi0yoEVE^}s|7q7|s(B8b( zj-}$&?*~(_AGB3F9DdbQzrXBtQjO%&ib>pe&4byUc5JcQ>?(44X~uP_T`xcUXg^ZL zH{)y-gPs0|96O2mN9E+Zk8QU)I=%VnwJkgLgjPR%t?la`zv07^=@a#K)-Sw~B`#J~ z`)z&dvcJn7`)=B|chlzGd;2)_)~$UzFEfDob^H$vz3$(9KdQe?`xplrAb|6O;|G0mg@6+$|aqIn#v&#;RGaPi!uQUJAcKXrx=B>5b{{0X7rhc%y zb^plO1+V5wUO6^Z=5uy=-#s?2)9Yhj^{&|7t6R#;?SFC27hUgQhVFlyzZcaXv@bp1 zWoHSRZ|MH9{c(P?onZ7I@gLpw4`TfvgiT+bHBV#DkIQMZw105B?`sddw6!{M$t$t- zk5uEg9!_1lzeD-M+kHFNyr|+mw{+zp#*b+|CCuwHwI3<3bbq*bXO~3Z+P7tv z`E4bsIk#f>-kEsgx%x* z!1uCIe6t8?Wp1cmCL!F4}UB=W?lZ z_FnCXiZ9x?Rjx1YeZ1kf@{ff!rn!%J!yP_sy|#NzWJ8VFW3SvS)3cXe+|Tcnb7^C& zyOptn>vHYfrDlu1zOAo((%HIl&f9Q7*)NRWw*L`vclz5tVNCFZVjAe6uqBKJyXB7jE7p zU*p5(yWVYG)=(2$w=3ZK`sv=)*Di+rdy{$TmDo1Vm3Ffxd4BWE&sngenstA5YVW(; zwd8 zP79l-zU{~HVzssQyD*GC+xg*((YsC4&e;p}3*3G&>)hstPcP*^VfcIQ zN9=zF_O!aI_1ym%9?bnE{>F3JAGRNrg&)Na-e-ODkFi4i+tlu(v-Zs|Uhsq6{lgJE zt`Bej?B`0j5OwH3!*MD0*4zghHh&bUGr#<$veY`;x9IU9y`-I&^&RX$=bq~SXE<0@ zXZWAtpzQIZ{ahLLoF@GJ``PXE<6p`tU)v{>y-)v&4RiH^iu-PtpIu7&nBTT1FnwXg zChknrT-7yO&I@HuzbCt|J7nuc6UXe*rKgiV-(je@epLU5uJ~L1kLLflW-j>8a4<~T z|HHY)kJSg`)Goc>C;vk;P3c+h!~2BV9X`C9oc>5QDLS7&`lVId!s6E`n^O13oJYp|R4gZDoJd)B{e`x*W-B>3~y9s4DJ;6FqAulq;p+v~aB+VR!s|6TG` zUL-5<-jD2qztkGVTmL9mul=Jp`SAB&@m+hOAIfswJ9paun%c*_xl6NxBO+c&eQz)I zoF32~%~j@dmPt#uDtoO=;WhH-8XTUo-kjpPkR5u z>if4jfBWnc<$3=dmr6gN&rsv^G5)YVYwQp12gi@w<{!PaZK8S89&4VW!w<9Tg=?&~ zT`1+Ym%a1D$kHXs_KckFhZgfPv8uS)t0M1kN3VYUf&cvdA1eBP#NI!S|09(BIQ{7U z&MN)JlJos_d_Vp!%`Etn@PWN^k7xOVweE2r#oP03+*kdGKYGi*r{u5tk^MJ!o$Fpz zvHz%j=I`?M(rnvezmI%tb9I-^tokN3Zyw{Bf4Am;h>E`@4Ia*zP^0~yp=siL{)+I2 z`A6a#zVGL`{$P2t$qbc`evJ?I^Zn8M*th=ie7=mRXA4)>r{4MT^8FvzNBjbDKl*;X zJMZZ0C!6=&d&{OvZ*_B10{D0Cf3VvAL-G9M?|*3Cf3rLHx90K#^&EBQ>KQB8gHSKa4h_RFPzd{@W3vQlf! zy7k8F+V%4*7|QlT_)``fGTQ2a)*_TO@u@tdC?p5Ik9|FEs_5j*K0(jTjj*lAqb z!nucc(W~pbwyj-!tK#&5Fe|^6xgRBKbXR44XZI1a)A>=Cw>_0)T2kfcHg}jK#Pu={xclR z_-Fs`-u&(B582<|_4%-z><|A(`ZwWgb-;Z62d2RLf$+ajR@eQ8YOClpOr%f~Taj<*-pP^}K{lW9{^58*^P0IgH zY`5S1z0r=fBL3+2dqP+0Oe&fW?=!u&C%gEg=pWIwnRU7szSIi)Ts7h6KmMSe_vqtV zdEbxDH)HPoI3Hq^85K5PY%5P-mblam_9yIbPJ)i{x816Lv;5nMEA^TG8N~l&U--|E z^;Z4xJ+A5B^e@;jE{(YN$Nr;vN7|M3i$1n)KO#MQt==mOg)X&ADrNhw*;vc&>)SQ= zSf}3B^?#?k&tN{ZKjp3Ux8ke&GwwIv%70tD_Q&nR^EbP`@B1QM-z)Y0aCJuBpTZ?S zgrbl5ru{yA;dJtC13_&c5us+Wz|{JvwxQ`9=M$%})Orn#x}8-@N}#`pUW!c1AzmKhnRY9r>eh&E2}& zQQ}8q#XTkOzf!t-E&DO? zdrBY9&0uu@7JhMm0@6Xm3{gZK_J|kE8;k@3$-@=*lykB-ppJ)3q`%%uN z?3HV7)_CvQap^1jwo41=+NgQ%*mGg+q7Q%eY3;tem3zNn^h+t<7wnC7XTI4pmg@K0 zbL!M*=|&&h#sB7G`A7Cc*X~P}o^LlRoBq$@Qk|Ud>S*=O^?%H>t3{9S*Yh?`bZNTu z@6kO=m3gPVudaW*_3}CgTl>f9J^OfSME-8J@qfJkhW;^o0qF*Q->CqI&#|FGBias83={6F?be+;>2d3lfJ z>X&hfJ1+f^eW>g2c4_gC;>7jYyL5Zct@s|N^l|U6OsB^V8+UJQ`c(ah@ho^V*K_}V z`wu?vZ*u-;NYSpp`S{^~hU5NR6;b~gj#(`~xVQd@oWV64@$wJfULX7QZtbJHS;vn= zmCat5{qiIG;ZGI2qpy6u@#RJS9??~9`s=mVNn7MZC|)uiKBDmyD=!xOQ&#+RUsMYkI_a*uK=O6LreUJL^ynT5GFSFO>oZKzh z($TfHOU$>IH^?v8zkT{2+4SGOb$9AF{M-LqJ|1*iBHur;?08`t`=u4ZkIRqA^H*$- zxc21Z`Sx7ZO}npsInP;Rzwg4DHT&e2cjxOU_D4+ne$>ut+If#%S4-B)zPa^@`?UkV z^?!z@!2b*f&+M1Ae=vRjrg!UqYu&CpKac&#=LdVOqd)3*)(OAb^}hG(dd64#`AhA^ z@19&;(d={e*vyCj83f%Q`*X+q=!)(axSct{zGZ8-_g(kUZF0)f&ON(m@_@g+K5_jI zo$qf2e+z@=a_zMKGaPiuDY57KKl;zmll-Ic zk*&Vp-YvdG-w*mP`*G^H%k@2`|3mcrp8X%f z{k!gS*rY$|Z?}>^D9`YR>GId9KY~BJ2c2x&tM}!+V8yck3=Vrg)^~lqFLwV$ovhTk zzIuu1;teDK>nrXS}&-gf-xwt0nl?;c~5vWXR|^m+am@3@q!vxm9*@$Blz9MhFc zKeqR*-Evkw_?pREu}hr#Yu}e#;0IkV;P#*4;HmjC_7A4kZ(iTEKP}}4`-A@sx5N(! z{a76Lart4*t^XNhvgY%D&2O*z-nppDx9r%b$37p~AyvPOpEkv|h0O!7{N~_RV{0KN>&w+xYm~kJAtK z`W^hj&-LT-A$cLwx5h`Tp0|{A>PClOv61APyW)kM)Q%7P7N(o6E?vC8?aPjQzwN1N z%d=N7l>cY=5OMy8p7_!EKeVs+)d^(%Q~nXX@DKZs;tzI*kF0-?-(JPvxvr+Z$|m?R zr-^2b`=hnl$E^C=9|o?7z32Y0ogZ!quLpPZZg!2j@ju8Pl(rXQ~EZ?%`S&yck~Ds^nBe$OA}HJQ1KDt7-9eBU-X zQ!K2G``VxMC8tZ1*S;#ZGyIslMsvlhOE0TJXWzTgE5dxl{z3o#4@K(7=KoN6f1qBf z?(D}uY#-*o<^8Zf@?+Y@$M;zxv+c~U{FBX&WB3@hfBDO6Rd>8s-}sSzjQbHkXV!G7 z*kw!Z$yf8r{W=|#>-6jT5yn!>HH#U3{AYMz{zq)_x0ydc8<1^2+W!&NtLEqV@%S3%xC+t{$QQj*2`PE&$pNH=T=s)tSI)q@W*~pMX~pXvh6ar*4}H^ zZ!@ueebzo)yE?as@x}fRk@tV-g&)oTp<~|hPoVyg`-k<;KLkIVf6!8=wCh9u;kE2X zuJ5zhyYHiY&t%vBBm1;In)m9L^mEUC-SM`@a!KYr*-JS#y33F%O#OOo_L_<=d&#U{q03g*uF9?x{jk3^?y<+#4?oz? z?Em0y|HESbWBETq&JX2}oNxFR&$vhaw?mEY$NfF;_o-Ht`&_k2Us+@RQM_qSg}eppL~?8*eKKdwLWPyOopn%D=sUsu2OXL@~)w3=~&&=hSzxq2(?@PRrVsE*Yk+bEmMZ}MK_l+N} zHoYvopLIDdOKQ)rro9W;5A9RCu>ZmQi~ku8+So8ZvgfNf{@C~YACa=xkKe6aUICho z{bjqvp5=`l!_kL-qjJq7E|o@<%-wGF_u85ovx!^gDNl~hKL5jW;oc8-=S|FClJ#o> zTl*j3`QVnxkM2j`yWiA5SXyWJsz+^&ocAH)ZEc?3 zJ?rX>_16x+Rm_u;F1Fkz?sUUg$y3WI{iks1Iwe2 z>BsaX|D8R4U?2O==Ext9mq%yLGnKR1b@5WZR0M~@#V?%l-kbJiC`am2aAjT)J-~2!MkO)#y?KS4+0qmKC@P5 zWiQ$6x!QB3&d1|gKl)@-UiIn9AD)o$aoef`qW>8l%!{)BBm6%0$JZ60wc;LsB0nxa zIG-W-;rhOHTlNM&2%DU_vSL+!=e29wiYl!A_eE}*Ta{r_J>}m`!97+!+b;cRw6V`# z8<+7K7#SfvKht{Hn*`v=oOGkLQA84l{(XZN%Iy9&A-RB`DahkX}J<_lH?AB|(a_Epm) z*ZSBz_K&f1!ggJ2*!4zCYU%lQQ+_*c-DuzIi!OaF-E_rdx5yP{Yi0SD!%L&^v>DZ^AZ=KJk|Hu20ul=EuAB7%2%I~o8eC#iLDs!Y= z`J4Z+CyUPOU3$|$c~Qo-??ovVF$- zwt0ph&%Qr$p6k$l;b%X5AK5eiI30dr%YObJEvHxWGyL(`{?V;@edLeq!@|5)&klR< zzPLW;{qwC?^d9e@bg#iK^+Ww1asJ2W+wC80uxkG!`abMO?Sp>7KhD=m-v+YJOtf;z$3u|mPU;6Ua{&-Ntz1Wx^{r>Tyul~tgS+r;W zho=YFH~eQfnDd|E!Tx<_{~4MNy_nBd!_5EY=aRZ}{~5Ud31rXO-{aqY{D@_pW<~eW zf2LR0C|$fBapjNqe+CZs!>?<#JujIgmL@OzQSW~-HGRjGue@&m8NzpJFN=J6Kt8I* zX7hf@Xn{+CA_X-t;pR+-G=Fb1SEM@;kw66Y#cFC^# zR{eh_uj~x=Ufo*Hd~ z{HDgYP;7B+zUtf?>=*1mME3vCzkhW84;{Yl56cf&y+7#BSHtqd`@`ypy&tEpZ!*c3 zim!3H^2hfh*Jr;S7cL*Ub-!ER#`wWU*=f@k#KphdW!AX-h1ls_wY96_W3#4DI+-oI zg8jzk{|pCj%Wu8&pW%c5O8aEz5Ag>+)~AZYk4f#RI`Lu3Jn4FN^^Sxe z2{!hBXXbxYtT9XM_%Z*Vuw+U7+ar^Y7*+&HN|roe{KWo8u=*eOe}<3A|2Td=?C-Yc z-EzLEPVVmlJK>t(kG$*q?H}@7-IH1Q;dRi5`A58CVeyO!r=j~kZ z@>kgOH7~_?iTPCN%`8n_anPjP;4VYWm-+{r|1)ga`JbU_mG^%J)+}G^gV+8u$SnOh zrRsErl&;z5f1?!H4BHKR@`seV)PA9~0{z`idVjzc09Tz4#xO z4Hs%0ANFb=k?QZtuQ7jc;>%xs=ZiLhkGONy4{xhto;p8r_n!UN^n3Ts&&^`|s^4t? z;LZJ8yARfX@Q=0srn%t?v1KZ48uabN#f|BsV7{bTyU zUHXS?xxY31aQs*ta%oTK${O42e^OUeF#TsZ9A|m)&9Q0wG(Mc~db5RJ!ja*6N$IA` zdwiG1{fK)JzpQzIZ~Ds2&o{hvAFy}-`2COD_qT7w^?zK;m(?GPSa9i&{GytS5BIxG z?s^}pWB2CX>wMsse&d$;?DuM{w*Sa2NZY&p%2r>i?+?E%U-ctD@}sh6`ug~|SGW2* zbIwh!Ro%LD1N#;G50UXdEG9p;|08<)Vfx|t#_ji+YqWpc*QkEb@6p+(^4d=Rl1=W) z8kcK((m%5MTr^$J_tI+a^N##I<_|fx>}7Yqw8#4q$D0~^w(eq+?N!siwZ7Y2@NHVz z2ZmbX55fQV&i-A!53~o^uEy}USmu0Y8|GDiTtA2(^5$>6$MAP)UQOic=zBk^4^)Y_ zJ^qm2y1rDtwR53o@}qdEAK8Zu9n;g-`#xT4>~%#y$Ky00vskX{1bvl`Haz9)b{3-w8|8e3T`_&c8AO73@==6TZS5@UbHO9Pe*G+vipZCXZ z{|h!jIx{k_e|kA<-{jhduYcF(N#|NU)Z0?|mU9a8sXEj8gPH#sGVk})e+bd%tUnkm zclmIg)Q{C4ZXfw4zWYb|hj$kFTl*P*_#coH+P>@ZT06V{s-^LT?#(@kAHsrkEmHOzx;dtxIdW7Cv9u(x<}6N!@Y?sjxWs&%9b+O zr2AnH!=GRO8Jf)gGaM}32O7s$uqpl69s1$;p||Xh`w!RmUAreg`4Ri!ztu;IE40J% z1%EjCmj2OvG?)3fSQUF`yzj2d4?nthtl8>o^!{+v)V}T6l^MUk++J(Nxq`Xj?+W_| zTkdbU{#N-v1FPu=@joIDbHCU#{_*L zEiAVEH+R*K)oveoZr$%r`FQmG{D&|1_!mBw7q)F(-@4Y6+getE`OW+v+WY^=%>KCf zAJ_KZb`{RwGIsr)|3~t-eU15sk8zV9ukVPz5odPQPT_UDK#l*y+tw#+>_zv!TmEoA zx9h`tp3LvNTF;6_-9k};O;-zQvk8tzN%GGDE-T%$?KST2-`z-mJ%HKpk?0-|e zxQ5|J_2cytH98+y;(6_OYRa$uiC&dC>4){9Td^INZB+R?_sK;4DEoB&*!O=NmnL2I zxVUcN$NW}>D{H>&wfuTpdb{_OT?~J|{AXyg{?BkQ_aA6#PPtCMCjH~T<`2K!etdlJ z{+8VF!=4|D56n~h$oss-)Hgo%L)p?tdxHC&PrUwNaRM83FXHa|ao z`3iRN-x)9JKluL@KKP$u)1LVo<-7iw{8)TU>i7YEp7?*)ru}CS`?A;WwAY8{$6qRM z$rs41=UbO+(yRN<#%_CZSZJSQ>0UEi(~?NtKaQ2vW{a~k7~VI|68|H7?)NvHzpG2< z_tzQ!c(}Okj+Ec(2UGt}`lJ2et-Am2AHk1u)t)_4{n-5D-nUr2R~~8`8}oY~$7f4% zM@R4LHE&nC{+0cz&RPbW)!%eC{GIwW|5o-xv8lgvv*!!!G5xJ`|Hrh5I>Xoh8MsPL zl={oRoW*gmH0)B^g@u#-uP*Dm=P$x5#4q5x{AJa=we0OFSKgWQ8V6rlxOLME=Ck{( zZ~al&^yBQuu-gwu*WQd1Vfa=2ZRf@P59aT(KO}$S((7+#AKQ=h3)duEvty5V zCBJRo{EyadTQ`0<-fCwpVdA>$Vx7c&UTvQpdoD~4w$s10!Fz4yq>4Ow_PcXSzy3Zi zkSS%s{+9iZ@acbiv;Qu&PvE~%?eVv7%l(ErrXTxjKRi2pFM)7+~2O<4i1cWQ_d(zkXKa!0<8kOEv zGG8cjR@t_H5+B9-*S)^=<-?tA?du}Emsj-1T#EN&%a-*!!hGUC!-IA5A4221<3EJm zzvb`!etw^chUbD1hd4x3IFIn z5_NCw>N|H|{W?-**PQm@=<>{KR;p9C&6W0Jd^UgUd(hpZb8nd+*ni{H_it7ok00k3 zt*N_iCw4#n-)$-XBX{|Zdw+;;`KS3aPEM>U{BU%K$!WG7m&3#Ir4CDM-20Jlwtj1F zjr59pS<~Xw_kCn*UfSUN+c4w)2mf5>Z@GUre3ieo{Aj)O7X4f0ZXfbnmjCT8@o$%M zn_K&UU+BlV>a%h7!UsQ^J*(CF=CM{G^~3Y&qUV+`h}P@6r?6|ot}9=!Zn$)V`T6^s zla4=NTl(AWkJRP;prQ0x>~9{{eiS||wqL4retX!oS?#?g^V_G3e{g?PeV*@jTy*?# z)30m0&-{M7<*$2?hfwC1TJ3u`O{Ip-| zrY^s~_ihh^{GR^|AASfjbu995yuq#PBKg##7?`@s&;g3{H!JU|_ zAcpk%gGTiaUc}!L{>QoVxBShz6JhZTHS8ZxAD+jaFLtL!?4x<7P5kN~YM&pz{Mfd0 zb;a(0i+dcGXUr1YR8#-(hE4j~SN9aR^yvm4ob{$KYrB->b>Fq?=2m7fez5-|eEuJ| z^KYlRgYny+AKicZ?G|3{$N9bW8LmGjvmc+&^<&n%=ZCh{*|$HM-~VNP=RTEvmr8y4 zq<7b|W`C$S?Q(gmu5@6(!$-Nf%Wl+XW_T|=oxZa2TZ1hBA5rsv+{GVn|KrO19bBXI zcgeF4byGix-TzkJ?KAm-eN&D0I^RdD=d-@vR`qQ9quuAlU)|H)f2qXnqkPBvJ^qVc zO#gVo#(K%iTV?YXytw5PCeF`#Z)FYR_w{eT{%2qh`gid^!}k0){%h+_nEz+k{QdCz zmRU5n`Q|IBnoo`n zvhIIG_y6P0|2Y32*Vd1>A3Q&LHvQqh?hpEh_iw(s^ihq>2l=)=j-`)Y&*yzzb#CQ? z*Kvv;^VUAP{Z`rG-~H=beVq^8lM;I^-frS+?S1(Rx3;q)uBbBn{{HRpe}?9D z{~5CCZ^wTV{;2+@vi5`dfqJf*$dAs4Zq+y0nb+ihXzwoVzPYFPVd$ijdjgmKh&~`b zOLy(^hT!8Dqhm7nX-59>f6U?D{#7>K?NZ|9+ri#{O_)#FKbZ8N;X~2+C`LAHN@H>~Vd>FZM@w=k-1LD=UNpe-y6yvGVM@{mbu|&3)J}#4lPA_C1FE zh_e^}!?!`8yo&>__2$MA~%a5A7GUQ?4n$ zx=+RTwkup1($0)mt;sSZ`D_554Vz; zCx3bE`rOjnn(K9+FueTF@L*H?hv@w7{U0LYJL);NzL%&my=cew$Kr?bvLBP~KYV|< zpRZ!p_hZ+?c0KQTa$(8G_^#6T;_(^(B&4n_UE%(4KL3pyx1%buT{mCSF`4S(efzG{ zx4#GM9qO~^|FGu&w)*ed{fYd-b;qOLA6VZ$kLTn1Blnq(e|Y|w|M*@0qgHp{9`CU+ zF3p#n-{s!6PwLJ~vu}3nS8XyAWpTj`^J%lmwt+|tB_wf%>0_4X~-Tdumi!|wcw?&<*kKKr!&pm{gr-?sl54*Kp% z{m;PaxbxB;&By;4y8bhWd|fYYesn+U{oAn_b|N2_H`Qd@O)1T1&b%&GF4g+Hp4^r8oi<4(7JnQct*t+1zRSKXJaEr%{q~a8vOAC8LM z*O{Y#Vt&BRD=9lWnBUjm^8d$o`tR!fnf^D#kJ@kCxBr9j!~YEJ|CDO{eni)QXnyqm ztz_z>wc&v~K9;wX-c|GGuh^9<{xIhk@7n&>J?@Y7`SQp|Q+sf9yIM#n*+{<01&*s4$@!I+$`j6MMyt+24wY~A{e}?v7 z-`m#O2=gDV_TTVIJ^bR9e%>GE9v`mtKKSc#KWq9e+4HS?>{q_>eB{3=E;7qOZvB4- z);a$fn%@111C3{8?Po8!FHocS;rfC4)_T@syH{7#ABd9gTK}hfRn|T8k0w#I^VU9^ zFYsgEXQvMfcD3F2yR_y_39s1s!`lKg1Mg%T-HFdK&RTU%LB6^s{y#(W_Wumo{yp&< z(mU-_Y>OZCH@>aYuJ{ca&MJST|JGH$`)lP_d*O=cmS_@UYDvRj0SvB<#Ir`#*%n#{c6wkh{llZB6n= z^`27M;3NN7cW%9svxj|M1#{4c8hPI1hkh#c2k*M3TVkvJ@M&B3!@bObxnBPnf-dWA zk>!uyQS0~Kf6}cl%y;V_Y>xjB_r81nhX`-|mVG=Q*$=Mg{loIZ^~3bgD}O?l|6q6e zSo-}4um2G{tE(md8Qyf(_^$i0@Au(*g8e5iZvoBP^U57QY}UC>`wopsHB_eZw&;qwoD zi`QiSXW*WHxHoy}k6+&p2c1s)r!Hr@>+;&Iohlz?=eI4`nW6KR!OrbJLldY&b+BGy z|APr~d_PuySnd5o{b7Iqe+G^^^@=q$uKw(=_8IN_k-NGg{E>0SF88+eHO@;b79Bnu zXXL%sIO@Z{=0%ymwl$Y7U^mK`m3p^teb&iR-W%-aX4U@@YyUQFBj~)Gd-VsS>NnNM zsvq0yespry^m|Odc|X`|Z$wyYOS> z#IL5gwg>Fm|1&h5u0MFtUTXh?_49atM1NQx`oncW-TC<(c2X5He*DU3d=Y1O#ZED< z;`k%27q^zTfBDZKmOW2v?*$tr-31@cb+65yRo`a9Ykzp++RYWykC<0Y+qh!SubMCh z`RJGb89tPizl~g0f6#hueNuIN!+!?;>5odMAGD}X|LuRPbWh^?Z422O*Z$Gl$J%@( z{=-_fDN2(cmBa&z`hVQNKd%4B$@%bo=Y8HS z@h$u0e*FFr{iwV%_;|TJ-`4YD_bb@`GaUNQATZhE(jVLMN9TEeY&!4oF@jS(*^kQqh!lU@_+$3(s6Ww{OKjPX z&KFF-q`UTy;lmm0!W}=%Z{4H6IHDr^P@Kf&JpJW6u9&J`D$zAMXMHz0pmguGQuqB= z)7q}wCsNL=(!gkW%u|8!;FAdqER!AhKBYeYo_jE%;zdB}!SC^pWcwJO zG&l$@E;*R0J%Qi0pi0Q9q4ou1n5K+as7mp9nW@J@i|W!C8*l8)-EU^V$-vO&7M8G} z<=7je$tMpejw_87~s+MBb;$;j_GqWxBdiQ!nA zYsB3Nt<%e=RakGEAk83Dp~5`D^293+2HsSSqY(-HP8_xec{sy@3__<&O>hiyWNVP( zk}XU ziq}8C9z3tS@Vx`qx)Yv3Ms9qe5rPx$w6T;jeEG|{faQ+o9Y6n-&$n5B|Ni>^S_Zvu z2PG|R`<3^vFWkAIs`qS9o+X3mU31nXX5KXxhxLk^_*s~k8LEs{^BApUVA`y&AM1)bvUnfQ4&dShR9Ncr4c8jXZtQK-^p75&7K(loL zo0yKHg|!3o)4&5NpBq1yzPf2~-~|H%ho^1f1wIBD29Xlx4KiQVRh~@pV3}9atXyzV z&*RpN(<_1;Za0K5K7GKi(QxriUIvrF8BT=>{y}`kC-Q3R68Nt$UkPApPq@9wJfUX- zA7ktuCXS%R?*tf~7@d|nGH{%Ucrs^lUtKfv7ZHXjj7HIC6`3q(7ca%nWCCq_ReOGC1-qFB9x=z4xR@ut;2|O7qUC9TvG~#HBk#7?#~DIJHcj=XP0> zL4-w^o8+;;sZ*!^d#!ngQ|1EmtV>Tr?=DK{++HlhD&3&{Ih{qo@q|j^lzEaxj1pZ= zBCRThxAt19FPC+&u=YHVyS}N+j6o)0iWqA_h|qyeJh?0rPxH?Z2v~nlsP45~CZ_{O z;uO74E&8$^>=V4^E_rvDp=?g#whcjE+ZaT3pG_=RN-bhcI<-J7C2-Oa#!0UnEm+IV z@49O-c(QEVv|9hn={;JTPLw^E@NKFyA;7@!xW;K!1>Cd z*7jzVEzz)^(E6;f#_rjWtUi2t-DtZwx!{+`zc@S?o>cmfJg4hB4RQ zwVpD;xs*l7!0{Z13L}qEPiocN{goA#iJyx8H@!^iQ|5riCRKtvbb+ zaHN2PMMloUCV-_ov*^7{rm)SVxf>TH-o4Gtv1-ZO=Zc|*J7>HJi0)$OJ~_qES-LIY z$x3zwH_17FMFUU%XDHpv%fO_`SJ2~duZYc2aNTo-J`UHPGnxwa%;Q+V#+G{SlA9Z! zbx(7Xbx%V<#RAswlyjHZ9N85k5}I}%W@2QRw8kza$7sR<0fyOqp6At9+DxA3SM0zQ z+i}ZZW=>(xoZ{nk?Dnk;j5o9u?6pfJ*wuUF+#F>YWf~%wm^Uc!d8YIw$5R?lQI~68xbySy7nZT}l^?Z0y>}g9K2T2F~*9mw`^i(Kw z(NT_l=VlSp;IAU&RxYqpk=c?_@TLQ^NEc)3vFenRA~(t6DK;tE3|>&bu;A%d)?(aoyv@?OE@?*b9^sz1 z;gTT?Ov*CttnIqr%Z-{2t1uZAg)21NwXk^*z;Y?-F1xmhkaBCd)(K_BSm%|uoLYI7 z2pCxP8C5c5yBD!cGO`FQ_vE`L_%GW0+ojwK+n3Uqo^9mkp+Q?#fuD~O~ zL)})=e~0Z0#x+O#BAz_p`SpOo_*DU$fAEzjpB*>}goOyj*Hp1|^Q>DqRbpjQa;p&8F=tcWBIV;okFhS2^Qro1SM2_(ilzu&X>sYY<%f zEaqKr8`HeG&ONIcEK?Itq@J31uYoz=Xt&$NO()iGoY;1qcaq`84aZp$CUEeC2yw4a z;Cynr$tRCN)z6W=;MD>-ZyuQyEDdWs7^4}uwte2nwQ-}$+W>>|Z5Iv(&!0O(^w#Q0 zM<0i>E^#=(6LI%;l4jbI+&ItUG0E#6I&djYZF4eYF7uG#T@}+*#Uvu!x1@>Tgi?d} z$?4W*0u{$R7@Fr8ZB&db;Jm{buwbLLqXLub=3Rw~9 zpO14cF#B$KkY_k2)y*;Zbi*kN^+`*kWt}!{WH3-+6uXy}7ASeALCm?~f$=K-DHD#o zD}BJ^kZ^?c=L*JUnj*$V5A@7arwDi|@T6N>sPLp`-0j_A!k`mmH7!Kd@nBx}ZkD%q z3d|G4ZqL2#n3EvHAfx+CMWc7SoYUiH_SF_~%ULC(8QvBIZurlj%V2U?A!$vk*QDiT zB21htLLPxlmMRZQUpjDI@adRjleh8Y?&PV8(aE_va_!jy{5zR)`;WRJC=2KY7TzhvXGDO z5+}p-H|5q-YZghiJ!PF(zyIKA9?MU6S}ckf6g^T^m{i-sZi$(xc7#e;rm5$NlqS!! zIL9BwASM~RZISUd&RdI%6PS)am9)`N=}F-$Y|>y10^QIN`}Qw!1r%ZXR$gSJAQz;5cK%aAYB)Tg1oKS8R{x zFnJtcpX9-iIz@nOl`&IDx&-^gdBuI4C(jueUhRnAbBcrcPiFz&Ic}}z+E?4cZ9YiP zUGhL^6GO2~ql4_X%wrFdwN4d;o?84o+x}FK^ERf>Tm87*mh-A4g0h9H-o*(Ek5spD zP1lljP?&IVN}F5E>Xmm|`Z5@s%vD1)b&hi_NS!e$gf(!QdVz97r3k~ve9f?e1&f5% z9DJ-Lkl&rFomViF2zdw3%B_mVm9mCTr@O!%26 z`Y(zh+f_+G(&OauyuA*u?-cF7$0B+{;R5r&vzEE@BJWR;F5126foM#+^@h6CS2GeG zdYLgOEMRI74cW-GSmlfSqbde=m3a&&Rk#?Yzxm9tCOI`+L9s?5XHL!14i@HyBPp6% z5>2N#nEp@#=5J88d1{_GYElPA@+%xk&8^mkrfn^}yZ^M1}Y zi95?HB{b*XOaB{jvUY(GgMg!<#SM1_A&*e$jZLu~uEBHKbi^6vJoIGXP?|XZG)MXK zynBpy=j2*i@Xnvk(!86yXMzS(*p{8a&4T9+tBFqS2$Y!G$#rCo9`(ZZ37l2 zL6MdQfk|!-@iQi{z3E9;mic&l7Q-a%=*}Z&dGxgo3eHM%+~F>?X|c87fq-+X=ZDE{ zVUW%|a3?8uzlM4fV`&(}gaT>4nZZp#Q6qatx8CGntbm;7vtCp{@l z5)TACSklZHHp*+wUD_CAe1nOTz2Ia_dQn^Isnr4uI(Y{+^)z02k|1NqFz16unAY-X zTt^)l9x$F}S#xS3-!naOfO;PdCyl6O9uNvoFLF1vBP zm{sp-h}lye$p{9%L_+~a~B*HwzHN!FZ&QIeK3IG zqVxn0H^sV-$GHkTc&ddM4_N3pGOGwWuuoNcFx&5OllV@S6u~!+to|Q0SyBW%Snen9 zt;xMJzLRr%!8!p^4?CYdd`Q(<9iap0g%l~!Ak(@F&fvCmVCDswl6SlaMt7D`yC zt4z0G;F%!VaI43GsXOz=u|;NVK`eaHy9Gz9F}%XSdgyL zII+Rc^5?cE?98g1TsM+f8I+E_lUOy4v+2fjh7X*wh4YLr7lxjTbY!U9J|o$1LWE{O z0p~q#1%^4(N)IbF?z}p2iolBq#=5hH_dm(sogk4H(-gAJaJNcpSQ>*+W5hOt0)}Om zp17qNoP5f;?_4VPj6Z*O@7eHJD`Nc_0}m}{Cdac3=5Ibf;n(a;@Mm{yQCXj?F#GA7?*UKeRu5zd2sSPFnro zmmiWJl@HI;zFO)pR1>rRqki8X*@ds{U5>F)?$} zkJWyYt{?tow=}hg@m&4E?e{-;)c;U<{x2jhx?f_*gdY7O#2XRfB1%t`A5w??kT!kZ@;w3yL@`} zw%N(cZoE7o-y{FQCI5%|{XfFp-oO-U#k4ewYy*@0Z zwtBfATWBsdES4^W2T+=*|L4>u3ht9X;)X?Q{dx2Y$yIvz2}en!V3FCR%}}z^^5*k9C_)F z?Zf@N71dt*uWkKXA9wBjg3Bef;%*nWoUUH$9dfla*6G!)<9?Ud=&sYxU$3XQZEIWP z(>^nXyFaFXIQ~ff;Ch*g{)hcX=QI7-|8V({eD+uTODnb?m}m3h-P(ut;uZP99*<_+ z$gY#Sp7X{u*6hhalW5>imN3rLJ#E<)Y42k3TnEG#a{=$m6du&(g%qrVplKuMf z@_Pqu_g-krSlXq)`Xm2?{$u@P+WxZff7E{jew2O`JO400>#O;K`&42pgb&s!eN5fo zXv4h7gnP}8^G7$mh${W`qyDf|;LgUaviTv|v3BRoKj-Nw_eNe}KK~__JN%>G?j=96 z5ATXwx;FdQx8;e8Ud&RPo?5!{)vsflSKe_=U7lH$HhcMD+qeG~uZ;KQUE10@NBHiY zJLh?~2%a@wk-#_mQTn0x9rs!PR6o*hy)RzF`dGisUht26t=Q~G=NW$dK0IrCOHJy- z(?4_9zF)NI+8^8XkrA(UrJng_UbcOMdu6(?cV=mBu|!2$?;YmkxF3=qXCId5-Xr>P z|B-mkeJUC8Z2MHQetmEMC;unxnvL)yTjPiKd%xc2iSf^Tq$_`@&T!A=qz~UaOm(?F zUHWuAZ1bg3?bF@dv#n28uXwm)^R6xhj(-w0B0u(iT>2+d{*Zs`eSv-I`wy>Y$*vdL zQ?-xbL*4#I&pw17e$U6@xBqd!C@;JJ6~5+&x;!zr_pM%^<(<3iW@l35yG2GT6=e8k zE`C*YZgu9u%rNm?(>AZTAN{f_ZE|vYY30gSR&$?TDGQrb+BZL&=X!bg_i&>nV%G|s zz6+l_%@eq1&58rE%a6u4|7Q^0$M)lQ{Ex_wvmY++w-f&3{b6{^X8xKtFK?}K^jW)qr{3LMx_xI`B!fw*f1|xr ziG9mF$sa%0{!#rgaY>E9$Mqdwn0}x6KF!N@ynnQRw0{f__~71L zdcR|z(Z(O;5AF+AoO*p|9^;2Y4*!%sZtr;b;oa<&-iv+&JBgPq55K;ZW&7ovSmrF)Owy?UgRb?eqO`nE^D3(ei=EMmw$EmFnCT_kYNqi!F|}NK?b0>x)gDV{rtWG~VfgG_ZM}PEkhFL7?3GJO zx-8Z{n3kC`$t|?cq|Y=hy(~4vm38J!rzQ^;=1X=4HKIRue_%d#{XXwM^FMk&+CNl3 zVw;)1G)t<#$xfrfIsSurcTP^}Sv%fn_uacc{AcJid21Z9ecP2Sr&m{>)?B(Z;@#S( zk&BYngfghie~^DHI==ba{4Fy78Mx|{j($-4eXRZA-ucHoZ6qIQ{F8{Saf;Q8E&L;x z|9JW3_O`T-Q5oO9OU=&ETshbKje&2;wzoV+n;6s&={Npokl1Hmu_&JT5C4zIkNl7R zGxR*T@O8a#Ohx#^<;`3FGf4kv;^+HObjsH|?%LCff6SM^GXMB;R%nl}?c3K53Jnq$ z=bxFTQowJLA%Cv#Zt~=FX&!s_t~+;W^BczGPgg%3St0mvcDdx8G7a{nHSs?bKTiHu z`?3Cy_WQp5T>F%2WIo=w_@!S!o~_3JLw?5|&PS~mw*GeAd(pJ*!XE$SnN>3`mu80< z--wfrS9&V?XQ$-if(0!0EOv%>{;2Nx@iq6u>PNcA56W5}3#_rZQm60y;J$$OhfBwg zPk!0C*7?Ed{nBc4YRq#V%`B-*x^mfZdXUuQh!fkL?i@O}f^k1b9al}SX^2 z{D}U@{g}O9&*W}&Wb?s#o;`}|f80JQoN2{7Wmn(km1><2&)NrFsXMsz#$ny7e_w7h zyEb9vq)I^#6UJx9geGq>Vp_BNAkVtqodFZLHocQfU|+757 z#wyL#F^$ep*|>AwtylLfdylR?63F15`N93M?Ds=+gCDP5erSEibbD44d-0n;m?MAe zWN)@{)jyo8f846IE33?o?}|;{tjZ5}=Xb9Aa#MN#_6zS!HNI-*s`Ne0{WDYW2J>&p z{|wD8_9^v0)a~CI*ZvO8+P~#LL!+HQP5$2nd!)-BbmxN_2mj0}>JP^meE2v0!~Vd_ zUxU~Ecz?K#>tjw;TjQf#zt=}5xjy`x9$YK;@AJX1Z&obZF25}PvCim$ywblb_M7(q z5juQm|A(UME&mzP?i$B_@NfIikXY;d=zHUXABPvb+^4qf@;-?Q^+&GXTl7rz1^y^C zA1S-OlD++pp#4#fFZ{d}UDCEr$Mjk14>>=o=Q>`w*m=R~Ob7X8`#+SGzX4sPbnEZT z{i**MZXP~1fAjT2^Beaw)MwoFe{^i-!(8P<_H1AHxqnFIcgV9o`Vijv{!jSoip2-( z#Jr_#z3llaW?epLpYrkPxA|&|Rc^h`2+j(By&`%QLtWRuBk~{o;(sU}@2!8(a*yT5 z%#X?k>NmG7U6g%))2oWjkKezITzjj+qbB`F`Nv7G54yd7B(v?l(7V?2-Ft)|>9@yO z?bv^@#`1x;^pBF$(=L74Yx>7}z4MyWV(S=xRQ_jR>-~4)Kf?$A`L~K6@xP5P{vA{y z{!sqde}-N=mWzKF?GMGb+Dm<@=d0L%xbO#mM80t3kI%=Y@(-^UiO#BG?>5!8&6DSR zacl0^qtllEXW-boZR7Gi_9=U=>*lF3e2Bk&{#z92M9Uxi|M<**g#WhrkstLzzr{W& zH~&%j!Qc6}+z;&%@0{%YBl^SZ) z-{i`5Xa4DOYOvp{|KRifhuZx=0{b7a{}JSWc>KtE`5)X5*!xWQ1?$ci{n-9(?%!p9 zit`^%{Mh`k|LD{YS8h!|CM|8>8GIwEV)Mf}{~0*`1e$GK@WbBWLfVEc^PMl6c;D{W z6Z7lpxt;49>Nx8UF8j~0Y5O1H@872WU230Q&3|k9!FcX{rWMZ*%oF=jUi{ngkKvD# zOMleIefZw7$8^K zzN_uCKf|Z^A6o8jcK&Bz+4Z9kv`XTS+z;M|vF-=u`SvH*3sjtcp#R5Iy{$%kU36wV zZ=HC>=_7F(R~G*0e$dSucIcP2JmZhJ?;hO;&G(sZzLdY^wtc&B#qlE-GYc!~P9Hv< zyqux#c>Tf4_ctv6BXsy%<=>_{$%@Ah`VZOPeE&fFt@wWiZvBH*><9mvKX^C&p+D2B z`=T}6CQ=hW9%@yeB^fKde7ut82>k z@!Z_pkMaS>yXyr0GaTCcN%)`OU-QGawzoaJVzS#-`bgUSknV8rw+_r7Hvi-G1?{e$ zdw)y)+tS}5FXgv9Z>y86$@%EsRHOUw=l(SP*8MUizRb}d-uKv;m#?olb^Vcg&wqx8 z(G^{sHKvc&i~dOV-29{JLdLJQiL2co{;n6xwtCyUC@wQOa3jN~{XcZ{-+)fk+4Tc- zNAQ1!zw`gG{@{CmU_MuU%6j3~`!_F{zx7}9hwt%QFa4AMqxMm~vrhSSn7q&))AYZy zR5tx6UB3VD>U_2z?MKRfm28as;qzmD(BAEvF3DYev-W^}_J4*4&+Tuh|Kl|OZU3L) zpjnOWN9hOW53RrX`&-qI-^(h_Km0cHw~hHhUFJh_qSyABWPG2sZg%qHTK*%C4;voT zKVr|HDbM%5G+yRE!=b|Uve6-z{y0B;&wF#(p6Daj&1P)?Z*b!Uot|a?!EJp{{0Fyw zmKwhwXFn|dmi=S;qj<3j??cJ;$Ngu>JYHYK2wK6!w(Z|(`wzbRZ|Q$~|954L@o%r_ z3jN1ss~^PkuD$AKhKI_ED|a?PI&tHk!BF?7rV6C%5%SY~i{ehn^pk)0`f+ zf8&)cYxN$Mu6S>{Qa*y=KZ6}V%OAcU$3My+xG!9%_JjLj_z{1p8tWg%$qTdW#r9M_ zjAyTCkNZ%6Sejq>mC(z1vim;1?=(TFc74{gwX0s4Z#tNnnw*}m^ZAaiOh(Ig27!v>2ki`hR6klT^h5f1oybS|ra$Qq z?v{sqINx2P`e?0m;Ds8`hy8*X+ly~x{o?O1$ql-7Ip9)huim$7&n^dCe!q6@yR~|` z&)O=IEABe5CqCZaXfIVKUg7?5f4`mTAC(_dKm0y8U*e1Pd662`kLkTO!jJ5EeiT1S zZ`<;_JKOth^?_OEHZQxG`A_qbuKM1M(Y@}jJNItwE#)_bOV~Jm%xO?MpJR&&qE# zS?l!b-m`nBe#h+Gux-brTW^jVb6Ynw$joo9k$iN%W1hx`{|t?PI3Lazs5pOYp6<0T z`+0waAF30*`2H(s(d6VKR(Ib><@GOoQKdJX_1e-^uYRptSeT!@JoB2_=DTsReiI#~ zcXAvM{_*)hoXL;)$MQlowm*6w>I+ryAFt!Pre9+f!+PFeN()Ioot2r!{tZ%98?)a-FzhkwJmQ~4qeLHpA+QLP#_l_s4 z{+s`;S?b>HEenKeY=4M;od4*4`#k7z_Nj|872A(XwY4t4f4d_2@U(w1pJTGEPT$P@_U+pA@4zI_ufwhwHh2gdeuDTfDxa|45zE_51~wO=7v7E_|7ty*D`g znrW@uvwMeqE}OiLp8fjw$%k9FG-W>ZxWOF$$oz15ckPp{asH6E zvT)&#{RjS;UnuqG{;~P6)!Ahi3zxp0#kMswbyeo{4e!FUt50X7-c+)1>gjtR%ii^m zttRz@`QiEP_7XP99~M6{Z>5t^uD!Sc1$RFw4BVV^GzU$Ek`>w6B zX185F+RT%=602jn;!F1HXP4KVY2y~E_KzRSgu z)!douzuHEJURkHG;d04q?daL3SMTn~&0V;2?!^>_``|Gq^@ICme@uSZezc#d;_$=y zNAlUT_^r*ACYoDe7?)-cyo>O@*4Y(=T<+o z?%()hzW;~m-S16anCJ_{Rn&bhUAH-RX?(2ba=*2_7y4XE1K4Uze`tOzf6TkTU+D9=X-G zI^1kl?xN&#ySmFBFdllbOKklk-E6-P?^>5-NnP8!=9O8MUGL(|F!QRu)vL2^^*%9^ zs++m&<+XtGmDMZm$NBTFojP@oQA}*Cx9A?hlO~jY}Ten>ukFIc1f=xTe`MeLcAwl2>mzXnvk!t!4lvswTYv1IhChGi zt?rCCp^Lm%S6nUq>zljfwE50$ZPhE5>})Y$pPPBB^xfL(>}4;vl~=Vbt}o6{PfktD zH+}l_>D?=T#NE0Vz5Ae%A;a{bAL<{Q-G2B!+}|&?|IqyotMr5Z5br~68d|0C=C z!}Y=yhn+gtna<~aVP`V$ppEtF>|3#Wiq2Lqy_qF+FM5r5^+UFE&rdQ;e&~MSeNVjX zR@-`~AKD*QKQceMxBAdM;p=;J;m;|KRP<*n0?$mw5wV`IKNtGued zZHxc5DA!f5!zVf}zv;gsbAxY@`o8U54*Xa6e<;)+;=gJ9c=;cZ{|w(Mex&}+vTu9- z!|)MzzXW$ZZ^iAvevc37efyN6D~x03KfK>m<6m!jbdG1@+8>Lw)i-aP_EO=a*!JY5 zKinQkzxd-+@_GaRUH%`|hrikVXK1RbJ2QR%CcpK+4gT2vu>4^DczN%;I)&&zjz3~Q z1UrA&-nd0J_~E>%>5Jrff83AxQT$M2%Uk}#)64ED&-?9k;p>hqy_$dGMP_|5zMExK z5$Bo1V4t=R)EG41|Dd%>{f|KNk^90niVx=BeDlNHoZHmQ&0JE#51>t0u*y?Yw3s;Tzt^X$hT+6qZ~$Vye}<;Qy36ux`?L5(|6P*jvSa^K^>^_e+mBAVO!7>B@-NtEKYmdo|H1F|!6@Eu z-H-Wseuy6nt6J8p_))wk-zIr^);$AXy;^UteBKw|rrqkBDRZz?f&W}SW8LZc58mDX z8Gcw^4*NUPRM-EYx%~#!{l}``N*~-OS1($-`C;4ck1~I>w_JNu$8X>DpJ9vg=RM-7 zza3+;888TVYY%e|&r@Zhb$_{^-d35B>bD!r8`|VYePmKX9oc%2LSJ z%7FiK{149$&X3jKGCmgjz<%I;YrLSH!H*{|tj^+R)Kou6NXKxpyj z6owzxFPJa=)2$KuarA@uQL}#>KW=|m|49CjzQo-ZQtDl6ZFE0sAKa((!M`J~#`KYQ z{Wb*`K(sl4;I{Zu7m zhRR$95sDXcF=8wit6JXnkNr>5$MVDda(|LOoIkX_`KCYa zt19>({#*R$_5C;h8Mq@WE+5?%m3plC;p+K<@%$SyE@XZGR`g0dU&d^u$(o5CjFUec zewcrxzt_(4!}r7TEpdz=ryu#x(D6@y-@h{-mN(ikKHQsp^q$^Fxkb9~^yN;we)!g$ zx+3dV?~$^w>E)MImo7QRt?j*fznH& zD*iJZozH70*vq(2^T&UN!*v25*|t7--_CrrJlxDqJYVL=bid2%O7gjvcjW8j=b9w1 zxp!06>Cv@E*7*j99x&AV&i{}({ms$e?Efy$-}?PHukg2{AC@29-(_d|VfvxB{Bb`R zKh!_E+Sj}Ghw#zsn_piKs~7y^@=y39M=krYd#WGuCe>K;O@6pH?sDYze)o^|oqOE* zdR5Og$}L{b5dWW{=}Fz``yagjGc?D4i1EL9`oZ_Zx6iZIDF2=HC%yaw+v9JyKD{}5e}v9me-wX+zvVvzgB@c{{skMy4~KpJ&e)pY^eesfkM$${ zo{L#^$`{xCiG6rq`0fis@9K}Nwf(!g_tG9G z#{K^p*uK^uv<1yI%gfk5=$j}1W8ufs55?bH{#bqd)|dT~(chN*X#bdY`OtJb^B;l5 z3oD)<6N~bjwNLkI{?m{A2X4y>KdhbK`t_}S)Q5B1A98+;H4f-M_T4wXaFrw948|Yr z{~6fs)Sa^b;5)y6|A*lJ47Z98*vqZ`!~Jplp?fTs_avA8mi=S#?Gkoxv z-*Eqf$KBvZ^AGHA{?D+v{^0(uD!Y{*y6kV3AN|k3vDQX^*^kAKda9oH*63$GvbBoM zcivI!|Jbaz^uxaIN9XD6yg4#XZk1n`ucrZx9hIV)BCYK_u<~~qxGEXmu&nWS;zCes7hPEX7ZXE zr3*R#B%^=SAGsFwpP}{rAM4_JyVa$SIJbK5o}u*bG{Yaz=>}E*8B*8(Q0qT-|A$Wf zTf-0QAKk7Hf2@A+J&SyAQQgsdd_OjtOKFC@sktsY{MeTLaFp)Di#EoO{>C5p^rJuM${y~; znO3H2r^rcvINllU`*}~S>D?3T@4!nSI6?F9#Xl7PGq6;D)IV~*S)Ri_ZGNkq?S-{< zlDF$l*$eLzS>L8plld_^_Szozhm#8zdq4K&D!W!^8C5a;;iiiH@mEd##k2NlSI1f% zdw;axzT2&enGF{GKg`(N z?K|wf)z^Ue!}ot2)xQn?GaNLu)BewJu-ralep9@l`Um-A`8SIXo&W7#e^@$GUhJ#2 zuKS^S@vZy0|0I0aH~+)7d++_O{yV<>kNd9a4_W3Jf7F}3IO|usr^Lu>D zO_l&(3VhKXG)_DHTlIg2riQxH^CW&SFaKlvLtMW9kMXj&x@-3Q|2Q(G>}EdP-#&}+ zTz56t3RDPuXHveOA&BpjaJV)HWQ}?Cp#G@+0^gHB5_QbEu zlJfph>^%Ds+l0*({f}+e_kJncUoT+Zxz%5|w%uoT$Vad9%l|W^?Y)yAC;Oj)ZP&kp z^&hl+-XUtRD@^RjzS$SJmXMLZi(D}#ZV@AP^Ob7Yy_z&*&H@p9F)_(N= z$Nl=_?+5WmWStM(7q(Bi&;6tMTbKPN{FqzJJ3hAUf4IUld#=;vN38GLx2&E$!}Q1%w)ra=s>1)sCjZv0KWI~*&VTFu zw~!0y+rQ-V{IEZ~v7-M_*?s<+jCqV7UPZl- z6Z$CLv1Rr74g zw#MoQx`n%9hGV`9;&L5jU`aha_wRh_GLx07-KQf&nT(SJo)#XQH z1K*VNOZz^u-diWDe(=}w4x7kKzSjQ?99N>2O=A3{|3_-|Zw}DGw;BH#Zpc5{-?z`| zkMa-p-;Vz-?EkQJPx8a_UAyv+5 zj_;HB`26_p{G0lR{^@*_k=lPazuV66!k*;h(cAc>J zuhzHgkMIT`vJ>65$7i#M|zb8lG?D<|#pLK0^=JKpv zd;j)zF5K3bS-7~N#`r%2+oFGmtYV|$L96q zc}4rT?DqH%^}Y28v*Wwuj5hr!T<~M}(VYD}e;hxCU4E#PEq0$>{K)t6Ss5SMn`&%7 zp83&!U?0ooomaMU+shvW-D>Q)RbDcxqEXMrhwFve3mWt{6Fk{uk91L74_ozp*X{j?uY*~aN165ToCX4XzA52 z`TP}4w-5iy{oea%_eb4ji)+^$e(+lKB7=R(KG6Pn^ZEyEQtp2QwjYZZ*~9;!{^p+_ z#%_P7l=@5l(fqqM_ic@7?#I(d>&4do;s17?`PHxKM^xMHAB&pbwx{w^ue z@BDU)cX?x-$%{CtkGp=DKbUfl^TXeB6Cc%cR~R2Hh<~|Szhh5y{bODG$dA2iTW(F8 z{kL^d=C|*=JP&6v$j^!6`#bGF!-Hv3)Bni4UR8IdJ?2B+;@^zxkG+le`tYrxCQ$f~ z&Xs?P?jPcR*Z$+bQ?dDXU{pMNb%3G zNAhwNvk%M@+IH!SuhxGC|BnTiw#=13dhumd?SF>O@Q7<`-o5*(_Ti6E#sT>?QtST+ zSO4RPxc|X%=Rd9GjdqO3AKb7{nSEdSR^7F4)9fT?KR#WP*8lKN>-vX3+AaLW<16me zcz14z*e7^$#lEIlclsY>)aY$EEV?B@eoFm^jOA}?|1+@M|GVT5`|q&V^;_m2dfy~3 zSD*2ox8nBUUvZ5e9zU*cx6%E`cKpzO*1Q_qY?C_U58E5}#OBv8i9fma$Dzkj7yo!a z>iAMUdq3Z@=7&2J_h0JU`>oc4`N8h{H^cvNR@(oOxnKHQKjzgw{*8YdE__#7^auQJo!9;G{m;-+72o!sfwe@RyMpihae1cX z59yunYdjyXm;51pXj)X>bldN3HigL#Y^|L>E`4Lbu<3x z{AXw?`_FKYdu#p=g|Oz2)kowQ{?1DMaQFF};>^eK(sfrp-TRUD_|d=C-_G`jGah~9 zxwc1i%@;{{vdx3dVyC3c!Z+~n4(6;lTwLW`|_K&*@eiR@5C;#F8A?>$XkFwv(Rrnu_ z(`^p8VB@^%^*r6S$BrNEw*Q^FBL0Wc?ZfxP`_BD#?><(%Z2{xQ`N#QhCjaA1UI;oJ z{h##TS!=)6v%RR_kh5QMkMfc~E+4*4e^k2RN9M!(os<3;?YM5L-+bZ6H1285ULSm~ zAD6C`KCn;qa^e-mc;4HucU^DMU2igRN%w9A{>%D5tjoWt{by)Ot2;Y?^ZYk=AN^;z z`Tk-3f%7;2GklwQZJ*AqinPNA_OsepKdfgw`uKcP_k;C9F%`3q%4vQmZ_!<2^78Vb zeJYo0xRaMx+-VT$F!p7+xYVM%D!T&MU(~+=T^{!2KSPtkkLmw73?JBc{j>T} z{@W`4Wz_vQ-AgNOKlXp?zxv?A(jR9ZyzhPgpnmI*r@H@C_iX;L{m?)45AH`+Zk6YM zA#)epb#xYauK?)|Ph!N`hDmqRx-AGYWJ$}f2Qp{@BbqloOXz#m-ikKEJy z@TYv9v_G#_rh@#+{U6HG-#GtgV7c^n=KhrZH*X)Czxn(j{)Yby4ErV~Z`~r# zS0nM^ed`|n^2hs|y&uR++^skq^ijY0kH)UJxoa}_$$Vtpe)vWBBetc>Vzb4{f1RB- zS#7h%N0yZu>>t)Ht>OD|bbXEX$MEB}{13yA#Ku3^-}uLM@eliB_F^UP*?u&Aet5pM zCh`%-qsh)6?swFrW-s@D_*OmUnpk!6vX|kzrif2>dUtk`k>1-s2YBwc&gb~2@u%oV z^AGrH#zY?Tks;$8Z0ZOyH^ ziOaL3mVUpvHdU;uwD;-TgWlDdOC0!@*FTtlpRbO!qW>Qk^KXMcx_>vVe{aM2K|Ap8 z0vp+f{C(=t^YXc5)!pS3eCdx73IDW`p?ho4!=MUTuGHQT%Y;{15rA zAI>*zwePa$6?eb(^yBJ^Zs!k+A`U*bwRUVZu?_zxyf^7`@b%oSj!Po!cD#QiEP8_x&;uZM;ANhOV z)~WuuP-FQ~>t05k+Vwr=%Oh*d8{dAFH4bif-14z(ZNaocUy-V*8`uxje<(VB!2ibe zZ{`0Pn&wqpe|Y^(=+X-9L-Y9T`E0n?{qgxx{NY-3WR2j5&yUU4mOqOB;kifu;csis zWg9*`>gSEF(2u`jBfos@zWERD1|KP(RogmkZa%|hod$cMf66udKaPK>I)30*|Izua z@PmydmmcDc9w;eFuG|7xGs$NWxHx31{M#6=bLE?f3pHML&4 zt*Lr?+ofx6-W6)|oF*JqUcsi``N!~)J!?gO+=u!e6W;7Y^K?I~+q$YE|EQeAN4c5R zi+(g8-lup`Kkf3?x!f@qze?zC<<*Y3RLWiX#?;pA*FEmr(kIlm?P}6oRKg&;{Rls3 zDonJZ`r+}ee`+;4KiWV1K3p&UM_0c0pXlpT=&ENAHIM5m%scjrQc@%Q0eMYUu##Xqi<(tneTsbZM9Ad z7sK;EtRFrfpD*$!`{DX_d0rdINAug)6@j8k;&+%20e&j~Y3VDvs_$NMq=@OjaS?gRU5KAhYCV87H4=jf|8 zrjPb>{8;U=|ALM8!})?4<(FU1I@h{BqiTuRu4xn3WLouZJ=?Z)dHn6@yEo&a&*v5j z`3f&;DE(*qL-Aw#!}2ye>5A)*><^vqn8);?{g^J(^P}~ge*z!tv;Am49L0WQKljZ$ zOXr^NsNr6nEmb>rdY4IsUTT--`Y)CFy2;{VML*B-zTGm3!SliT#(K#*l^@2B?2p8^ z@8_~p{_*&LJ$uPq{n#H{ANzCu&_5EV_+kBFTk+^!^GUQI9C z9`?G`v>oD^KY$_sd3wL;U9Ol zoXQX3533)Y%G}l7Vw3)OM~&;^TR+;~<#T5XoAo{h_2YOa2k*aJqq^!=R@naJ+~}j( zOU{-)tj=}zX86kg!{Yk4g#QdpsegF?Gc;}dcgCLgANT$yJB}LDkEQL0CF^tEv)2fA z`)#>UC-Cyu^`nt*OXmx}jFY*zWY5Jt@{gtOw}}hCp8P}G|I(J-(b}@shvO8awAH(O z&os#I-2b6qKIphSj-MaQ|8erJsqy?_{-OI@;Sb%9#)pM#^tb(;YE$`P*7ZZT{CocC zzS?!YEIW0>(4{ z84hmW|H1$M50&`0%>Nl!Hs*KCm-r|6@4UV2Kcm;Po*#+kzf~RkV{hie-u$EYIhI`7 z0~)F1`BCq7)%5JF^@sTR{xclfvhBn69-Tem554d6WmWB4xGrnfl3Cw!61HD%uotj@ z(6oQk_dmkskJea{ceOZ>52 zT4S+m|0UCzI$PFvSY7@jGyBLs?QI|Stz8^@>-EN{`xTRN`EKs9yWqxn>p#Q6v-d$` zX4MbG|A@4Ii~Zn#pw|51{R4aXkH^V=xO@I>{e$=>tGvm}E2}suJmkNJ>Scyw5ehXQy;z+j;l4&-Y*!FRds>=UHl($_kWy^|1+>; z|1kW|z>@oM{*m)7_1yO9_N{iBAD*|>2$%lW`V;;mG54W$eMh}WP2j3myF#ME`t|!w zrq&Dp$-G>X_^|qHbl~;#ou&Cgu1jC$*FWZ1wsEr8ZwH1?`ajHszj6O(Xz~XQ)cDE^ zRs4SJfABxU&HInHAD?|*@{ilafA(>Ik}mBrd{i$|qxIqIyw%0|4_506T|Uxl^Php& zM0!2j%^N?a9X@U+=gS{@wKUiH2&dk@w~I~Z&0z%JY8V9GYBqhpME!%^@mtIf)^}93 zH`!^{oskp1W}ngdpCP%P`;WtwwWjOa;^5s8^*@%(pxJmhO$ znpss+HE;GqS>dQ$(>y)bd&?MquZEr%R$PBj*iQ66!$HwKxgVV$rXTiikmq^+;rXHW z9NGIf%|AZB^;de!?MKs(=F4RTzqni>?7ipX-=6G8_1)9zg{Hks_}KiT?R9*==ZC59 zc0T6j&Qh~1KXB=2jf3@y}zaXkMre6_T%>@_X+%A{JUU(Ms|N^oYjZt z$Exdl_ep-3-?MdptDW%H?Hlu1Z&vvGeiYcb^FvCerY&BbCG0iN=)zh%>Fqr>fe%;bhJC8em%H=B+R184)vS1F z?g#w-;M4n4*Z)vo{^sn*&);IV{+;+o^~2@|?~nFNR`?&k9>0bC;D3guUwO?B@3Uvr z$y{1*qsV_~zhqnm`{977ALR$t=JXY@H=K31f19?3@%Q%s z46Naxp3;AYgZ%&G{xcjDw5k3W{*eE0|IOovrp3ws&|dVT`CHWAMSnO8AO5UU|Kt9n zyFPz;Y=xA_kM{I(_0D~I_iCJXu^m3L%e-QZ}X!`=wv~XW+4uS=PSz zKf@uueWDj^0>u{oXW+QjzV2Z4Bi_P4^$Q|jR&86nZr|#K`!;HRIJ^`C#^rQ5n^6@IAi2n?S zWIsH69kFkJbiQ;&yw~2nA6@si=B|79Ubjxs^TB)msQdf2FMI93EL%bD`hN!Yvbt08 zAN=iaE`N0X%?kU}c;-K-@-c zM*Gotfvx8|<{9N$dtBecyylfv-(;q2tF=@2wJw;e$FU^=G|l+n^7|X*|2QZ9_N+Vc zpF!TH^yBA;@(0e}V*WPu@;|LCImwUp?RF+F;>13lna@`zboyaG|Ggi|p&z!l{t3@t zd8?x8@=>YPKTcKemyfu0aP_w3i_b2+egF2h=BGZ4Ka+o}{ySa&!E-<88n4rFMt|q# zmA>cv@%@{?#vgM(D*OGgfB0VJN1w=#)JNw#ukGi*_So}>%RPnJA)Eb$Dx?qFi#`2l z-fAbMa%UsQ2un>-NrfYfBICulUdK;9WeZPx<<{Va4KaKD+)-`@{U9 z{Q+p`QtEb%WA~%^hxMDI{MsMp@_l}IZn}K?{DXC7*Z;}>2z+B>`>@x~wW<3?>@)u}Wc_FOp>F<1VESSE4^ICXr2hnbU~l@z5>=t> z{dd~CJ=q`PzwNxd*2XyVY}dp1bNzn?w!XR}_doc~zvcg}{_nCq>Azk6==_lX`2Abo zg*t8LhdXOTKQ4Csu)S~L2mOQ9{md0r-w)4I{&2s!COmsZ=C!kr>cwJze7YX=aq8YB zvs&leliG0UZQhsTn`eGy;6E$h62IyDAEEi-{~4NR)wupPxu5+{;OhQ_X#1XddY5d3 z7i7t)ip%rZcz%@QZ(SU+`G<4qqiff;$NW(C{_*P9_U0OMomuA=i+2Sd{>9$9bXs+7 zs$za@V3*7=T{KlvZ_Ke*QYu%0<1 z>OaFFTYHzSQsMnJjvu}s3e21*e^GzUpWtu0(w-k>_f1@Wr(&(?e8Kt*|5f#gr*#*w zzute;{)f8wAAxg+|8eN9{S$PlhV{4A%YP!<|4!8ZQ~Xf=)@}dGfAq^go(}qPYaZjS z=^lIkGqg@&_}I7q(O>UJO=`{hhqC8J`RZ<6-94ZA_6sq=+m6{;j6akAGq832I~M=J z_x-K#hyHK9AJyM7f7pLezJLFg^=)=anX~j}e|Z16zHN``wgB&IM068mvTlwgfkz?`qd^s;_um`{7_og{_wB5tq9|J3N!CY zzJ>?#Q{q2lTz^yfpMmA(-(@z^zXM<2-!i{(A74$`-(`PHAN>{f{_y-zeeXZ156^qP z_;c0>@A~j`%C`Ru0yU}+-it@azo@#m_L08Sor>2buRhnJ^4O8xNmjCq};yXIm|1^LOd^tP|AC=cFv zZHunkZ;z{APOs4aayQs#_q^kQm)EbIv3Kj+*0-CF7iL!GJU_F2XOqh;Nv1t{ONtna zBR{(Bd}JH<`-q(4Ra4#axT|`--&U^6`qg@G@tUmg^IMnh+kfS~>by^%er1Yy>)yR` z*;l1o`qa6P$_(7IuJ`>(e`G7}{NdUDN9zT%szB+cU?8zTOP9S>egPjhXF2n`d@9Gs%PK6y|gvC%X;?C=hIAj zowlEppT<}o`0;sY z0zV%2*l_uated{n52?4N9_p8l{JJ)A#l08e%Z^81D>-ugk7enS*Dsg8j=DCdRBh9h z=i3?NR6lHfbp42YQ@wBvwG3$bK|`Sl(S@|9Cy)kL~`SRQG;(-}Og#RrKp9 zv9;OxmApq=-lng-b+lM@>fg2ZW>+!az8`rf>TDs)0onYw+P^((Qva^|XIAn4TgTr8 zHOYmK-*?X!%761!Uc6%2WB*GgrTZrPTv}ryt9I$M>xX~ym%NbDo4V+QRoL`}FK*d& zFMatdZT6Cv+rrAOt=KTVfba1m|F>Iz`~HdjJI9Xw-${9aeHyRol>W~9&yX(bf8^Kp zgZta3OU1Y6=GG`yKKgy~{no{Qw=REiZJx??rQfbsA6|O!k#FOw!wZkr*5)rho|<2j zUZ>b;W=lgfAUevU|ZI}65=?}*Pf7pL)JMOpPf~iGK^aEc%>mAE3 zpSWI98}Q=R9D`R@Yq!mR6wg0b{m3lec^lWKDBmSm3 z%^JIF>-HEvKHqMq`{8@v9^ps27RU7W*gm??`RdxN_ibNK>u+(l;VoVAT1>2}g|m?7 z+b8e5l{Q9)&Py{?$-h}g z6?6GrFWZ-*F8H`Hl;s@u5yL zb3^R^>30NPoUy{^pEZjuOF8DN^eiz z_~Ci?*PnZwi)&XNT^Idg!;f{pM0S5{@5$HMverLt&&4gj&x^j^9u`%*_vmY{1(NqS z&40W8x6i*L^AvxyKU#mY|54w{54+=jto(5Lh&@M5as9HFyVzFUy58`|Uv=)WrAvRt zDPA(IF!_CBy~%G`XV2?jcD$)^UYI4eG492#b@T0Kd}641{BXbE>o|eyU-CtMWC!iJ zSmXZiY`EX`8vC`Gmrb`VUYuR!`oyYj>Z+IDH9z0J_wxI-53$icEjGn2Jse66w(q}f z`n&!=Lq`7X?#FWLKYstV;75DuNBc*|b}r4Rk_A&{$RulYN^#}hqsq6bRvZC7G z?EY~6k=p8y;RoZn<2^sT35zoC{o4FUeD_thz4?m|Kbn0zWXH$yuJ<`xa@}M0TrBzP zeB{>Czw_qv-u-=Q0sr60_PO(Km4B1`xV`hAa@}cpQPcdEd!`?!AO6oE^hfWb+|H#h z_sLz04*1~T_3YBSud(`X?9?uPU2AvirKzm2$JH%+^R8W!&3C%`i55rhJk zf_didU5_lD^{knA>=fe{`?ux4)9NnCZ_ht^^Pe>+^$F?y)2s>J@!?zRL-&um=Qb;r)#f|u*OF}+ zTlc0t$e8?k(~H0?sk!$Sv&|NLvfh{>`tOALJpNnt-}rt^UGXRO?^HXj*Ycco3R(X+ zKgu888yt7(%YO#`*Yf!v%Dd7o>zn#2z4rd-*1zt><;tpc-HWn+g?^b;oF%2+@kLiS z_EO%Oud;f#QW<`>ey#p~H8{rHxHmLEI4JwQ>AwrNnqq2~znxcBbFtuZqxGrSC7GvB z?+{~juV)HXa|PrqOE+xW<yogeR{t22WB#g>*Ve6;P-tZ3_4w|t&;yXHwv zj@r8Ay0~Y5_?B&ZJD9D%dH&t>pCMEJM)IS}e+vFi`=^#w&$Um@`(bVQ!O1_`{Vvzo zukD`C`=fKsvnwU1_v#fI;vM3yXj^r$jrmgm%I`G%)EZO+eY zu$%pNU48EPTlwGK{OEpozw7?i@FTC^cgtCQn1A>@-yiJ{-?r8-s)#;(|K>fm4_j`{ zz1F`(x4rp`K7ZzPVclS^Uf7@4e{1^T`c3KG z{}~eJN*E@uj_g5y}l=VvGn#?wc4;l+N&6Tx?Ot}Z8_-?lUT;N1>Q?e>@w-%TC=o5`q12&2Y#|0Y*F=K z35>B(<~(?JhrILF2_Nh}XdU(1s1Z8TYr?SxdDT0UnAXk_OSed3`ySAD)J?p21!G6x z{9W~B8;>>cOnc-R8rtWyYSHs&2_2DBMf{E`SGrdN1vSZ4lA8H zA|6)mdyre;hFwItVNYV@%ne133|05u%FTANg<&lS{V=J#TL+HXtx8N^3%7c%+PLFv4Tm7J=M!CaTB0WjD$9{%V4-=D<%(HE1R3Qvo;6|)uWl%_ zc&wB3WS=C#U~%U}s_`6?rDuATr>AF1TQ5o1-Ek`7XIp^t&-n%k#S_^7Zt|UNVEELA z*}^)hO#kV+kUxzl3naE(JM8{+!(9jYpg*F2ci3m~-+cTa|1I}#^FIndmanX^esKP6 z;L;aX=Ev>XqcY`W_VypEDsNZaoz0zk^=quue}>o(FJ|5P_uKEP>W6L17sX^trJjDd zYsvMhy4()-=`}1rK7I`Umh|J`hu@FyALJLiS0VoB_@VnkH!5}?*r&5;+vWF5FP7f5 z*gvu8SK9i_6&bhIuDNj9cG|+F*;Zu>mt@V#z4~nK%80v>cYV1TLVuk9xc*W6TkenP zkHnAb_ta_Z`MCb5e%CzhtADZ|-xs}maQVyYQtvyq-afwf%DR6C7e{8^a+{m?*=Xs! z=}R-eMfuIl6fv_ZZQVLYz)^sGLrvA+o%^%mZxuh(fAjT&`Q!ehy~)S#bN^>Zjc50L zAm6Z0{K}TUY}fP|e)whQ3j4>Z@w$6m$}#P&vbL2!Y$tQUWOu*FE2*dNCNIc{O6y;K zH~L1j3Io&p1LtqYf1CMtcAZe&#d_vC?wY`>HvAv)zYF<^AFsD= z`SEpjcCD57-Egn{SH2kis_*#Yxa8iwioEacd#{w#ru}E|x}v97^7Gz}hRX1R_Py~! z`_uEg_A&mr{h|D*_&%;53qKq_YUSVahxgH0D{IU5Ci(&|wrdxrSIX{Nm|dM%&Yk#R z&xU(;qrm+o;mP2hL` zasGE!-DUeN@(1g0KL1wrWA{h*;E(N~$$@BxYc-}Hv=45Nif><&`$u@m>*wCFe)4a1 z)jckM?H4$8?Uq{qvKx1PEDpG~u4Jyf&!$TyE7!bH%8+4t|K{uO!hcudH?2Q>{}%hh z^aJ)?xAq^t&s=w9K0jzzY2Pf-w%HHwI`K>Y&|G-!&3&Kj-fXuk>%VYsy9Bz=SNwPW zVeu-noqu~D>6Uw2bw_3eIW3Sg{4o15`w{!b`NB2KAE!Tb@6@-E{AmBMy{AN8zC!%y zud1j!ucX+!{+O17pvtwVrE|Z{t&}>MTb^#a_UIX5h7$WN z>2F0pHh+L~GSd;WwME`KTIcvXMl#x0lL3f*_VVDeYn z>Ef5a)?Sz2n5@1KXwC+upqWBmbX)v-WfRYmHmSW-gAb=#KA9O>6h;KXPl|rd^_Q zCP!r4Pue*7%`3+8Z~1?B?a!KjQ~Kb4200tn-!?U|*XyJ!yuZ!-QCBDNC-0)kXb_zHOe-hx13|nSXR0O_=og@IHeNWm{7p>GNinc~==X+W9ZI^y~$z64aOD1pioi2VktL$^xWUk)1W2gN$ zZrk3)KXK*(TmDD#Z%_UX_!IVb=05I!$LHDoSibW0e}>KdZ+@=Jj#K{O`QehU{^4&? z^{roft8%NVSD!4cwfETiQEyMq{3HLA`kmU3L|t2*|L}KgdBo<64>xbSqNkj|WxqN6 z?aJRi|4x|MOMW?j^Zuh{n?Kw;{H^gv>gqp=KQ?DCyYZr`rB3UE-q!rm?8@a9*G<+2 z|JM7d`~E9;wQRlXzjGhiX6CPWWth2Z>B2oTPq4RC@E@P2^&|c9e9k|fAIvtEK0Gh= zL;T1u`JO$h>tEjUipz*o`*3Y-=DLiiy&bb8A4JbJ4%xVM&-Tc~Y0N4blMmbOoFvI$ zoBeIc-&Oyt{@s$7vC01JUUw`7+I_{M*0OntN~Ac4^COQ-fRXrYCpoo%ZR~3+B}ymxBVY#_{jeI%m@kldX@;pT`;jWvKT^xO*S!4P;1hl; zZE1FHb*38E(j$V_+O4-7Y}bEV`*-a=`+v9VCI7hocKCPM&Tjsr@T2oZZe>K3t$kGa zC7!RM%>K=N-j`;#)Vi0wyv5#9QqTV*+wJn3lHU=Fy?xc)H?>9fY@g0;`u6D2BMYSV zsr}uuKeJwhHX|%l9{&@2N?C z{QhS4qj}3eEI(Rj|D*Jge5;-A)h&1R58pa6YklJ$|KerwS+kN3KJrzL+5qO4i9chbTberDV;i_Ti5EV#?$)MEZAQ!Nx{dTULP?BiOU zvg`c_3D z8ykzM%wh=LJ5%YC=*%Qz)q^hOp~6a2A_bju`y6^gJXWb3V@z70%TR0kTe84I4*YTQx7HuOD|W&)u2=U+KH_ix)wX|m#y_d+)1Duir<%?ARo2Yc_~T$M$`18&mG^>DGjE=G^W?;(ElW;0*vkYneoS6k6Z3bKo%$<#v3&{| z^VmN;KRj1E?yAXrz8CA$AIfvvEiGP}xia}-uDsJ_m6=;*v!kzm)eWlFEUitCxK^qg z9=?CymAq%W3b=Qbzdidq@TL7Gvz@;=U)~qpTJ3)`{K$O4w{_aD=W{;1yoGW3D>>86 zRy7miJlESz`lr+HC0^w>dDZEME4DuQ@bujFr7v$?JvZHKaklJHhMK7k_-|DpXY(mj za}&oa4jUnUb{oMHzxrGmYC}J~|91QD@_iz8XZK6hr`R*pgkJq){oCr_gS+A47rMl&sDX-f5BX;s`J1%{hxH`HbJ?!FI8%MUuOK)di$O`*a zyQH9@Fn+WDTg{L2zd8N5{-FPs^talNQ;#>;>He|ab!FWj?MEFZZ>1xn?cdH74*qa& zb7tk@igv%NTXf@nu9@&Ahg|&XYaDhtKPSKB_wO00&BZDc8w%$O|I?~?|JeM{ulD;oq+-0Zncu#P|62R^ z>h+1sZ`{0bTihd8S7B?Hbt%KQ^S7+O-TS-TK0BVXhV_H_Tjr1U0Y91_=HKjpxU~Ne zf0LB&y@?O+YRR+wVD{}gbnCLaXNj!5_pa;fuATO}l%sb2k8k1Himqp;y)Lg!zdDaQ zQJNwAN8s=5y36}n>=^!3{++&0{7?9`J(?f%zs-nctuy|?cj>s(uFW4aKD?Zpvn|VN z+SKZ`8S+`%c4YGw{KyWvc>9P|j@hiT9k*Ji&fOx&P_tL$n2=pG z)F=usP*$H1GF3sy^h&?ZhRAvI&)5rcH6z9S9)@jB6x3mH3-L^vRSg=udnu3_zuS}&jah>Gt?j8cxEuASpmvSt48>rK=XQ9KrJ0Bj`GO+z} zlE`JS((zj`)mh5ytE($xpn3062E%u!7cHLjg<*b9!XbwS3+wM=np3Mu#E3nIR0$sS*l$Z#x#*@6Fe^weI-&`GlQ zJS@*d->z;|UKX!Ev1xr1N9>6MT-!IVn80>?Mw9w6w-?*l3?^87;%2CsAj-(u*b?X< zxb#S;&SbWFBd3Us6F)_tiFCG3FKMxAXYF%oOkuBTb`X+2DV08VYE-1`lk%KtnX=6@ zC7O)YKFF(U#IW6*pusTdTVUq+tz~sKGQeyKeW5d((Gx8ofuxR%%ZZu$HxUqwQc>+uI^onVGE*kGXxExY!cI3i|(1!cMbGGNOm_^=8Dwol7yge;}BeuKg>5PXCfzvDk zRvC14EfA94$zvEjVPA;OyUxbeN|q!~tD^T$HB`ShEYJurXJaUe_ZLj-?Xq5S(c|^i zMRUEp+6$6@A2|DLqv{eCHVgIwCez!y*;0d?7JXIN;l|VC#$NoELE|Jlx5POm2L_>c zpQ>;2Z0Eb=aVB!+jE%{gEDqX=mrobT@(4Ws z8Q7p8G~d9(AxCT7b)Aq_M~UDqD{qM8a?Z(N_r*^YlJ-{G%xKwrb zOeL;$VJ>$<%uj2X>&_h6QKsWvbA{pjmQzXw+M=zq&ty!U;dUvf zL`al{>D45gjN?;#XL2T--NCSZ=d_#Mhu%w1nPmHM_pyhn&N3?`J$|b8%y<^WF-@&v z_R}N{MzOL0htMaYCr=(oY!XT5p5S32v)btIwSoiOre~(J&I&W~c)RmSQDmQy8+S@y zWapFTHjL*SL}I*~a)|fA;ZK58D5`XV#OkD4#!=QU-wPc6z)5+2z zcOH5lS+(M+Gpk;MK+l1xJg4{!8`#&r>3&-KC{f|qS(ToT59c1AcI4lSCF?F73*hmH zeDc}b%t5ibQ}%Ju+22z`JM!`;26i_r5!L>@+44pRTbhPIVhB_D{%K+E1w0d#+@)7) zBzKu;Y+T^O%E6a>j7jK#rc`Fm{a^&0JFubP|o+GF;SUs1E?b5%q=54yZ1Hs9~k*OPn97n$u$ zUGcYDh@lYFWPj`Y@$fhEAGaUs-%|fp{Bg^F2EKoKKV(gR)R;c%Fxe{|>#dzH@}oNR z!}1QDcP0M9nZM38uD{17nC$U%-_@y^Vu!5p~<5|z`3$|#{OxZ4*wZSL+gSVuI#Ef z$T{`2vYABB6nQnSx2c~_@XB42i{@Eoxq#``yKGOP2-(GxER|=Ohoxpb?Jn#Kx#b|F zm=t(IwaDS&3Z37aPw#Rqi)F!(9Ba?I=U7>u>jex9hjO ze_ObqM)bq+x4a+yLw`(M`0w0(o-g}b{wY&}!mh5MWTGxX%yFn+9lxNd3rsvp)zukEUvxY~2+oh&K0 z#S5|m&Mx`U3~J_t`(HKjjSjxF)z>)uvi_IfzqxNNn006Fx&<=vNA0`pEPq`7D16*r ztWNDm+wY_E1?^NT_8*yN`|*6|SF!fDv;H%5*Tk-gef>}IntInQ>FVrCQ{A{?HlKA@ zL+lT0@IVhk9aBy1-x>b||3qB-BmBYp+k#COx7_cGJ)^yu;7U%#@fe2-k)#86p(z`uXKXnmS~j~&C0pC7D` z8dpd^I)CVI-Tfo~I6kKCZJW5-bMb2dXVcA|YLnMz{#w?$I^yolyRkQ3Nx5xamL(Oo zG`YMmeZ|eY(Pwq?7$<+6{aF4;{VncC^ZWOi{)zu+dcEhLMor+yw7rk#%l%mQ>E5Ap z&yUDyebif8xazgl;u|l+rb&f1-MM$?)P`7?-C%{Hyuq-}OZq zx6+obzjJ)y5AQ>DvfHoP_*bvZns!hA!?EVY2P6PH$$eXS{6^g{mD>BJn<+GO`DrRRChT-)_-fL>sJtBblsq3y1ty5;3tgMV=m&O`}IfyVmTWRTj{M6CenR*@! zD!ZmdHx|9|Y5a3$j`_J1quq&z_0MZtPY79=rrIoYv_WqBr3In!Q$rF%B`0XTk1T6* zQn~$H$xFOW%tfg2dEK`LDZQuCzu)`oZip zuvw>qsp8MfqE+QbB4a0R+ABFxy5Mlrf_Dozt_7`G!^Sn?l%wL*cCFCqJWJ$?mdG?X zvM@2IPx}^mGPB@mrw+H>HPs~P4j#VucaBfq%_Hc@vT6dObj*nh-31=4ku!tUGs-`G z-*gXr5rgM?ROWvu(U2~3(4Ja>`Cy&5DjMa8Rwq4u~#STHr_l6{~SzhuCD$(8)@wB3gW9GyL!F4we@N|_OQwoaQB$iHNzEJvW$as(mRv?MVR3*%jv!4f67JcAwlj zXM$Y7Yi&KIDVd>7r$U+)?ya41|8xMywNEa0lDT>=?ul-DbXuk{^lD^hD!){vkK>(> zCKIhqimU`Urp0)vXmK37G<$|-(7Z^WCh5HU(HMa1M9in;Uo{bIy7-*7tmvB5$~CE_|BO3_ zNP|xJXU9EJ2CNN6earEh(tYG2Uz+h;p!eG_lz{2ksJP41aj%KZBs1lJ}zcAM*pYPY%BH$9}<&;=@ug*QQmPXe%U+{fzk~K}+d6@{`rGOM3{92)uGw!eKek#Qv?X-z zkNDaL{hd~4`=+n@k#yXD@8v(COMaa8ytZ!n#>=Mq?Ai0AcU;+WU-0&wEV1iWF&AE5 z_O4x?b?sB;thIA{Z$}@uV82|yb^afL>;JgIe+$)}n`Qsj|F_E@rh4`or7LP5&TiYh z;?|GD2XB4b|3G`a_?_D~Dyq&ul5gFToA){P!`!R?R#&GloU(hY3{$w@*(=(>12t^xm!8irkgF4Tedch!LH!%4EqQ3=Wm_< zw*9wjOx>w?COd&2{YT@5%+vjlwpm0@D8GGUh0#xw_2T#bxLv97wws;&pj)2z zHNRZ@%CnC+*DURi-?ia_()Q_5d%am0Hvca9&v4M@KSMVE4|V>d(frN(w=O>{J->aP z_O&e*f4m=d-;ND;|7hN76aVo0j%jDV=5tr9K79RFU;k>y%xa!(R=o>$G>2Wv7k=|= z+w7!Iw|_p67ruabei-gyA-S8st?JI*m)M^z-&@bOKfS-fj`M1rQcdiSmFw5H*GN9J zRZNOpzh3Cl>!2(8`T2S(U8g-Re32D99QcvH)AVmpb*%E?e{z>iee=V1Z@FCZdv(VF zdx!c5i{n4IyuZEuA7|mBKXxDU-`YR;&%m+0;h)NV--Q`bxlot?sCsH z{twm*#BOctFJ1Iw!b`L7x0be;hb8-6+w#{qY`)u-{QI6)7H(zurq5D;a990@5dMz% zjlT2W-hJ5LI!|!VkKolcHu2r_GxG}=ej1l84mwvXt>YvN45U( z{;vHZHtxv}<5_Y4-9=tI)acR<4YV zdikx~+>oKJ=Rd*P@=IC)TR6Rdb|HIt= zAJ^26tG`)p{kv@s_QyQUwu%|1B!kH?4dro5We^vsX_$LI4WZJZt2d~}}B2kEFC z`?p^GvchlW!@Wwc!#902@5)i#yyL^Y&Qs{yeZrRSo_^l~)sKU-YLwShI3KC2kl_f zEx^Cj{=uUE3?H21Z{PpNS^L1A@lVj-Wj5LmqUX2U*8$Z-{9e!D5`)=~;S5jq@*TlS>d^EFqaaQJ%EYHMMcl@(H1h6luVgJw2T)O|k ziv3&i-?YoWUHIYlL-zKkKXO-B6#Zv7Hjm>&+5Sf-O8C3uh4zRi=bPTC)BGUc^nOhk zZ?OOM+=aj0o7d)lU8^1baqpgQ<#9rnOJ~Qr`_B8o{Pg_~_4q$R<=+nfXJ}efC<1GyoVdE>O8z^V_IFwv2ojVmA$j>-3#Em{w?!A zLvy+PgXQ&Gr@xt3pX1*m&uyb#`Qdutg;IHu3iZQw3LlTnU+~BG$J~c+b=Oa-O+R2K zJnvp~^=s*!CbxfVF}uFCpYQCW$lPx~%6J3Qw`Mo+PqBY6Z~h0b{|vW&|KlqC=zr9n z^FKrCefAgEZ!K>y=`g+Du}|aUxotM859Twx{64)X^H)~I^oVR-Acas zD>I{7w{Bp+x_|reKLY;$xKu$$ZTv8P*x$c@OL?P>NS(y%Thk^lcrEq)aKB7s#oIdh zAJgMJqg^ljsg!M9y8T;m)?}w&rPFpU-nRE)=GS7r8M9vQ*u6!ZL00_D-2V)0U+NF~ z?9chna5L%ox4l0!KO8?49?zrq@cO$yoQr| zsQZWYHZF^QvCHzK*ye}x-~J7{lwnd_Db?A*e)9eX{y##WzwP|b&@}Z=&PV$L^KVW* zxGVH~mksBl*dN`G_;x>@oBU7UJ=+gUf5o-IDF@pU3<4*S#Mh1y6JL_{qmPq z(>wCdNKHLr6}aV+X>_*%&-z=R|8dU$9ry1@{Z_Ni-!gx4f8?6~(R(i$Q{g7x_RGF$ z-Snfs(vL>`sa~_a-A20b!}FlakDVV0uK&?yxyLp)?b|i0smnWb-<9MB9Xs@ot)rnf z@pozcK}LH}#p3s$p}~s(t?qB<3jPE4Kddg5e#`Ct(QM%Ze%YH9=YxKvul+G)kCoc= zkL>=2esN{S1NqGV3|VY{;2H}{juOz z@ABq4g&K>G?tj$eIQL!KQk|i18qae-%jo4Vv&D;l^j*62%=*NZxBi~n`a?E%{d(`Z z>&n_Mw}0v$uzmFUAMcI-3{CTI{yQozzrFPMp?u!sA68v{_-4M?I~##}23zH&5`V;t z_1#mxi41n{gHcD{X@PFSrz?Zn`Q@H`lGq{mBGtfSKsY^#NBHq zxbm*!vdrybQD!sCm&Iq7-zu?M+CGo*p>h2|v;Pb^C+zc7KsymVKSamdZ@n_lr2Amt zAH%+E$?mgeZ8kCUG-Io@+b`NbWc<(2BHDgL{Nwi9ZjIuN&Oc^FZ@Ij9QYgds^*=QJ zGyD-O{`T}gLsP`de@q|6-?~23XW!oVPbDTMQ%?Fv^M}hvlaI~gj~BZYv2A7XgWjZ1 zvDR)^w&aWG&DfMRt!(?PkTR=tGqYF5y{KwiE;e!1-yY^qRSjGW8Vn1Vs7En>>Ho)h z{67QBlD`}FKUlfX_G5nI{#1L;45=P_{*qez=pT)b=gVZ@|Ik&zeQ?{aqerimQHUrj%s%i7y|^b|wk{w?Bvgp4SdT9`T3vd|6_0N|$TuOFx=DJ9hp+_&u(VymMYJ&9?II)O-K< z@}n!Nvrfq>hi$)7(k~<{9&l-^hfagtB2Y8_Kf^(e{SOw@fAF<$t>gJ|^|!|l=L3^# zOh1+_PkeZLztA6(kBcgv-9GS7=W5QfeV6_iK5lLHxqRDi?^Ut$f!i*9^;Q2}d^r63 zubOXSzaGb$7s?jRe_)^JwVK?_ zMK!@cmOlI|_v+WV_YptpAH00?v^`++uK7M!>>tXHA&x5hKRH2d}Zh`vb0^XUH!E#3#?m@dE1 zPrJP3;G@_$Z|mPLzUs#PyY=a|=S7pf>LJ(GpPRqRm$z}-rLUZ84;b2KuK%Gn{cY`U zwYuw3{%;&VCO@2d_}_&%jjJ}Y%PWKr@8jRQ{iEOXk8<`MQA>8|H*K|NdTrLrBXw@> zBipEZ@4dHFcYOIh+t%OjYRTKQ>)yLBZRJ>dz;5;5efu9Y{%80Q82>}<{lR$Ne;4Gp zq&M0K>L1q44!TsLTO9c#_wnp_neB(q3uG_O>tFoitL~bd#LLmayTUK)nAXZYj{ETR z-a5t0iPvVm`gT;$!Akz7^*`Rn{~4O*)ZL2Tv}ga;?+5-fwC)q!`y;+Sdu@CLzt>0m zzAf*0E8aia+uy!Ef6Lpb-w&U+=dFFZZSUspfXn%7bn-0H1?NW}3w5)#c_4TFP)i2g=xv6xyRB|nYX5F>=4?gQ}{r;x=ccqE` zjrWh^4`=>ocv$}9l0O%J+b;FSb?!Z1-gj-gXL;?(hx0A^`((4CY6{gm6t3?H%wACu zcJ1z?O;_I5_%6$mGF!d;<#)3u8`!VbZ?FF&ApVcb^gjd3s~_AC-`{fo*7kwF>B*1S zmG@rU^6%X0-)?8N_2bp^hi%i3=*n2B&Hu<|&8?wZa--Dx=Aw&hP0z}Gj`{Fp*Pm^> z46Yu%w{!`w3)AyA@&6gvet}Bp{|x#5H`~5{+wz0)!~28g^SPJJK2mm%ZQIBC{*w1x zKh{4wdcLJJ`Y-p9z#6Md>)tNgw6EL$ddc2k=ji_of?4;ZF1`Oc*EsOT+$!k$NrtOKi!`>D)z^JhJ)LxrY-&`_WIbZ>3@{$#D63wF3Gx( z5!G|g=9*sayJeU19$(8#zN~8!t<4)8cO^e*-?mMcp3e8ZqP&e^`R}aygGT$a&)<;# zHu*w*s(=4|zBBQndotzQ^X#NuA5?Gq*xvq6Bl5@Q@LcA@*X@|Du6g_R={1!rr8m}@ z*uUBNCTHtu-6Z)9Syr)2HXYu>{5t-2@ju?5{~4Ml)E~6ppPp;}ZPDKadzc@T{s{ke zmfi2;oPUfTpUr+8TmG-o{@_k8}Psq3TvSYLjhyL|hucE77@o?qx&|ET*t*9-HiC*rem`_>lQ&0O`jh55<+ z8;AdKPW~NVcdvfSy8XX3YM6c$T$dNyBh26N?w@M(k8>Yu6c^U$Z}`x*nZMn}xBT(l z=zz|1J-_@r^>qJO+^PxNzH95d-5Qs-+~z(RdaA+ucUk>GBl~RoAL{XM86UpCx&2%B zhtwW>fj!xaGvyS2)RsPo_5LR;&;7^g;?}!GpATE*f)astg}%PN{E?{Icb&xxv!vo? zKGK$TlX$%AQc~N02Jd-4m|yqb=Ksh0`aeU{^nZuzH|s8coBHwc!{yBne?(`CHJkUp zt&x2^n_sNr@I$VOX`e3pT#|m|*P5IwyR}8=wfVOw-lzW=+PB6FWzGuQS}ZzkWo?>j zTPB0CeIox41^GWB&HuP`Kd8t3U0k1D-(JTP-zHVJbj^!-qF4SY{djfw!1Q>LulxC} zqhCkmAFeR9j?CTa@#U?n%0)fXw^g4H&l8C)yZ4{LD_+3#+SJu+8`w|UZ_5889R6+3 z-_8FSvhG%Y)B9om;Znvw+X}sS3I0wwnU6=G)bVcl5ug3xKSTVM59@pL_tciJTt0u{ z;fMQ$???P74%p(Yy*Kjf?bo_`=lyo=p5wMamjCT%&=tmi*X@5Wd%jeS)Nem>KFhAg8W=MT*nEV;ebBjfCU299M*b5qmn?UpC6N%p$3Mb_K%vgvK_ z*S!X^mTyb5xpy|$t^2!b|ATw}KQyPm$^ET)^moFatRLwQx&7P1#H7MjKHkfI=%3l9 zkLE}GTdnH07pEsLzxT)a!``K93Ldm_AnwS1a593@C z=hO79`fk#*!=4w{ZnggX+e4M%b^U{d^&h!-bJ=IvQCf z(nsqVU;96nXZi8#y!WoH8?HV0aBe+sZ|mn*w^+aao1eQjqw3h?#jn08hg{O@oii-7-6xH|()L!$bQA^Xfl1 zo&TZ1|3~EZ!}8vJCUs})ct4of^VEp0i~r&MPw0MUoW+(8Ve=oYm&}}IRkpPLVLZp; z$XDf2^A@IOuFgDPxgyqa`H!+o**kLeOjo?SabSVGSq;m7hNg_V8}?h;-*&F5I|w?A zRqX!!Tfv8|{xkI0*go!O|Izk{SJ?kYW#NZ^)!p_|uj(v5vaHpcx$sB*;qBl4Gu+%R zb#U6F`+-;9msInzmR9#>dwVb7I}NE(=KbAh|6tlayASD&^;z?|exx1Z@AwtBaCMFC z$Lo!Iyv4izus`N7iF~`Z^tG>fc($)`z{aazq*uS@DD{<(yJXVqr16#4?w5r6<@~k! zX$-&WA20tSz4<=_tMlKv^~vXdf*!8vwD*lpWA=%TmM6G z``eHI8CV@Zu=m$#)t%WV^uflSZx7?@J3rE1AH6SJlP=z5vR|;`(0RYjSM&BLKXfSh z8}>f*>YFWp{k?ZyS+BZn^M`xWOsysIx4zI@{@bdd=JJQ>|2Q}QPO8cLySY^U2LHi$ z&Qh_z%eTmjRP-O(r}M#Xb7lIHTNQeb5AoaoQTcVg+w^s(-}EIF@$TE3-)^dCT|V1? z-?rVC{xfX4_v>-QrMK@6*h>H7a{LdvDSeaugQ@>?KIpgY&zjF&k#^np$Nc5lR?`pP zm;J$gB>uzH3gx59hx3=IcWikb>-6Zb+tn>^vz`Clx+M48^SX(z-kYNj9JNL|ggg&f}Z@uN-^x((TNB2rIpZ;571UU@u;@#%kt4}SB1 zD1LwY@;?Ks!-wTZ?B(mu{1g6QyS<@KI?Mmz+xTPqh5sb&E%g#n75k)b zZdsdtXw%JXE8$lKSyd7H7{1p3(0Knxi2d97{|rr$+4cM-?{CdNEGN?J{9`5CntRM2 zuOEIdG+(U3Jm!jx&90hx3KycjJ@&s?8mk|7@!{1iw^wfYo9wt2&-`Nw6=i=*T2XUmCi`Yrk-?17v<^z;+0e|P0Sgq&4= zB;RG9x}Woh=^oJ!$9~;+{;1w(r~G59jr7CLn#juK?rWlc*t_lO_rIoJnp^YI+)S$V zPT4j2o4;b$EnjweRhC%cz1uboa;(e#GaNLp&wd;H?Q#7Nb>01kW`Fs+ZsEtP*AM^h zZ=3u|$|c}-*0d;>dCI%4rG0e2eZH-|`O$|txsP2xCcVmC6jNbybluInF^6pgy%^ug ze+az)L)rW-=mQ$9-+nUTrPyrowo?^twer6Ue(aJJ za$Mp0@MxQLGY5M!*sq{bgYSPt9seoZ{dc-f=)>%a@Q2Njkw5ky-!E7b_@9BJz4>kF zU%z|Nd(96`6TkTK*1pB7EBant(%*VAefi7cTQ8>--MahQd{^n-r)wrNRP6s_oevs8 zZF^Y%L*3(tcin%6t&4s&r2!yOk{1_tYPBuYb^Ozdins(0Shv{GIh#_G~}A{|Ur*eE;;KPX0&x zhwDct{u+hI$ z@#DYQ55%?!o?ROEBS*iZRI>7%wOC3SHJvxHnTdtTuxR`dFS>9 z=I2pJLlAub86Nr{dM{U#_haS*`R4oFdn{`oz3=$XAp9qGL51*vd*YW}>T|FE34PS- zci-c}mlbm9UANfwT+w^by6(ODwhfzm=e_&)>e@NUcS;O@1O78Kwf$!}$hrT)0{ag> z{cUypKaT(Qs&GEAU7n{VaaB}BLB;8Z(|eOY?wk6sUi|*c@3ZF3f9Nm%_HxPYwaS54 z)|$NjcG*l* zx;E=G_syIwFLl1`^?G)NdCuRx`yb4&-*WtqaQ5RR{~0#Ff9v_s*ZS~0(^|IUM`y7u ze|R@ZU+{gj#SN+JWxBnU1OuWytW>w{A z7Ops7`AF92w|lMFcmK;ZuIaTR1@|&O)UG?bj=^rn-!1hI?%V&+^nWAz+cb0k*7)P+ z+x7`|KWux;CNF*XLBG_^iql8_GjOVZH2u14>Z1P6S#k4oWtYuNEnOS`@}BJM$PeF^ zKdKcwd}P+Lsf%ApwXV4}ck&rV{x_EY8QA{)XE^A-|G`50&CfRe_W5^y9@7V_KL*?P zUHdYh<8F=P$7k~&tPOwExAEcW<(I$C=5OC-9(KL(-qF-m$JJVHo$pw;$MoUv-a78H zTIWM|PCE3@JAr?~e}?4qKh)O0E&g3pcO~lho5l~;56?6|c5hkqG4=crU+v@N@gG`W z?&p4E!?`}n0)x2#+5Dq8F=sAIGk9&X5a4G?4`F}P8ScFae+Dg z+sFS5O>uSC>p%F#_r=NoonN2vo+ZNhalLeASk(K&v)?nmn*U+OkK{*s6IWz?&M=7i z;UBzh+m=1E-r1(z^EGjrVVT z$KO$Z@_(d1)Ton+Ji0ih;=RX*_FkPm;g9FHip^7)r|qlt`;lH_V$QZ*war<=w=<)r zzH3}vG3mDRRlVFdUn_Oa%u8T+{+}Ts{)a;S9})k5T%I4CkL=%~|K{$4e5UzrQF5zS zSJb;)-KSLX?)*XFKaL;eS~kv&cln@W9u~ggzW4I8S`o2XyY8NAe0XE4q20V&=jUe^ zDKh*F{ucC~p(*R%b@^@KZ&TO&I~vbsC*!Std_G5(oZ-eF{R@8V`XtZ&vYvbKkLtDk zd_V37{%BkiTY2HdG&`y5alCwAoMpd$y|%YJb#bOx)YPwUw-hy$%6|y(|Dl@xw)Q^* z%ef!hAI3M|-}1c0B-=Or5#MyX%^z+bna6TXJdtf-qUgKTt2`H1B%S*9QYUZgi*=87wlVO} z{Lhdc|3j_+ZR_uVf7h+fzp?&s{9)Ll{afN&P0veY#Myr6?^16*ye*#h{=;vJ)h0i# z4L*MB?&Xuqf7H8txVKd5<@Bny--cYZ^i#Ju)X=uaL|8$>i(M&|8D+g5ZfnO z5$yLPaal%Gc-Nl7hrRm8zDcb=D&J-&e$98qmg_dP4>@$h?j5^#<$e@z*=6TzdmLB( z$Ua-V{$}Rs%$jQppoTbg+N0te&@~_+$OI+z;VR3w{(ns1165c>af} zH9jAg?r+?3U%X=5v17LbcYj#lESD|o|J}pANq^`kUnC*O-giqQ`!fU?*c!z zM|@1%U0A3#d+CpQC$Y1t=T=O*bF^lAmBoeY;ko(V;tI0c|A>_TexSel*sEKkyfIe)xT3`w=rUtKE?=%dgEk*J!qSNpHC4 z)h%o7-f5J^T79~!x8~i$xeazpejNPI!1nS#!@&SMwz^}_zuo)z{m^=@gCDm0eF*J8 zvR>kcv&YA|>sEd&fB5|H>DLxtl^^p<{ZPz#(CvS@WUW@j*La~DFMnNIxxOOUPdr>s z`I5=OC9kuWF;uqy5nlg~BmZ{|D6aN!KYri&pJB6q^B?it56>UITWQ|*m0$Xg-^Z#7 z<-@za_b6V<*?O`@{-O5dya1Ftz3KS?23bo3_sUD zoc>2R{oBzWn}28Dt3PPFNB2X2Q1iDHmujLP&1bJ@K4A6t-}HsIeq{YV7*+dieb`jKtlw^xn>l>KD{qxhL^q?!M_Cy)K8&KTv1&fxYwk(K`0)>rL1$AMY@+f1CNHr24LY+{N(l zi|_PH?k0O*zWHXWxAkk!yxb^HWrknT$LD{@3;!dk|M2{qwENrHJMH*?l~T{^RtPe^MuxtlO{Ov&ZwpyKD=UOcIAy7I9-Z)SI8@%kU> ze*aEgopylvSp9=J`#-qszrFt-*Te-s{xcl2-}Ju4M(Q5ZkNj2Y?WX@3$PqwMm%i~`a+4hEqqkJ z_2k1Bd*ss}u8#Z@wNJM@V7ANt4;$JeF0EVtXwN3^3H+)58JZ^6AC%pn>;FSFZ{Y)t z{|s6EJU_hj`D*;y1L9dLem}h4@y;gk!QAkOOG?}3hkw}fYtAmW{`#dAv;DX2-o7i> zI^@z?0BjN4cDO-Z-)Z0ogY{~7$7KHYrE@X?$9huZnKtt&vLzFS@2 zvTOMt(+M9R*Vs*dc>8YV>wLB)yHx5lt^WK|HIKg`_(S=hz{`anZ+vXOeXg&D-=t0@ za@H-6MFsr%{~4P4|1%tv*q?i$zR&;AeK`3c=a2Hjk8S$w=O0^J*cthM=)3Pe`AGUB6+5O$7ui!?q?X#2*uZa3ll%d|ZgyF;fAL{IX1do3MO};L>Q{y!ITl9zZt+Nl!=kD!Y z-}6uLkNZdAALa+XMSWYHT)rZ+PU!Mh)61pVf2)t0mt70K&fe(UYI z>FKLpUXE#~iTxc|cSc^SKDoZH&hGD={|p)W9Ooa*@4C;sHRRGC&qcOpw=GP2U7DV| zD%0v%_v#xb&E~GFs6HZP`fdKY45@2#ZLUSl>t7KWd*oGW#bSoP#{U_b#QrlJjM3fy zU}F4M_U?ZgKQ=#{xA0^5L;a)g8PpHQciv)edvEgB`q(eO`MK*~{!_eU6Z+tHZ^*%A zpuQ;V~UAFlNdyr^S(TQ?|CZRfI>n^FD_a##N|uul2U(DeNDe+E{izgz6n_umqK zsD9}7{kH!M{Pii>Z|(jwJeb$}k^Q&c@1y&aufDCxUQ+SsKf{qY`;8aYm+A`p^ao$5 zF??w4UC){E>)!VC#W!DuzyHs`$+o(5wYN=z{1W*OVf;U=x*vZ3BO?5d^ZFzHo8k|* zzd8OmzFl5upJhaa)#=C6J8II`|45Gbpx>1HNAYn!-;YP$@~uHH*B7t(adPWl=R>zj zC%u-_yimhk{&20`vyW?azSah5=lVJr*FWg1|4`E2VE_kU=9f6M!yftBUQ%nyCnAF)mT z9h+4zeEo5Lqm5)G+r$+=s{MW}{^;!eW3}DN5073)+)XcD`NKQz;+nN@OlqsVz3l}v zjqXL=&$hB#^zGLX2Fdv!;_H9tTx4*0C;p@Rx7k0YF8=Z8@wc@<>{tH~%IB{r ze=u$NlK9NR3cKTO*Pma0qkJ_wa@&q8-#pV-R%|rwj%HkY=lnyrN%viLG3@>=^+)+{ z>ps;Q!@uiG_uqQ{ZNkU;1M55fGw|85X8qIsJ7tgl!}VB@)8yM4m)gNdmOdv!?R}7 zcdNzbXRiND<{Za4_j2||~ z{b+v3EBwg5&t$FaAIB9x^bbf)?=bnzFJ7_U=Yq(ZS z*5wKf)$wnSe)#<@=|}AE(3*%3{BN>9Hb?#V{@d(Fb?Ak)|0HX|Kbm#xw{NW%jlBCK z+G+oFyW7cyi+-d&@fAKQUUk2H?bqOkZv{Lhz#RB4dg`Z4$+`y2g*bqC`& zFF(4!O@2##*Pqni1wW2I;_oxDzs3Kp;L0As%4I)-A5L%l^7=o6h`Qg?NKHt7Ckc{6j|B(NU;zz6NF7f`(sk=1K z;P2c;AN=3^e#|fShb5l<2VcF^oj-m*uBWWnC;h?nMXY(ywk_Lt>+a0STl(SBYql`X z$N4<9jn8&5{P_Lw+sucu(T~(yOycEVZ?6;jptkbSTW;@<%RASaEWh?up8H48_lT`q zKl1HQUF?!LJ@kr+t&;a%x63~5$*;uCHr`}qD%!&M<@wv!ANGIq`r-IHxW?^A{cpjV zg6sboGWOqG?$&Gl`Q4x7fi>VBq_?AbqJzt$~JUV7}~>6LL2k=~|vw>DY) zj`?t$@#I8ThT7c!46Ix~qJPWQ$p2mXC-lMk8{Uu4ciCt1-@J9)Mz{b|+fSz>Pea6V|yv1ZXn?~HjS zm-nRHk_o#$eY?8Phmv=%A5~wj*6%Upm9?>4zP)vwX{-a|{15*5E%h7ukJ)$av;6S> z?aSY8KMsFO`VqS9kMzgy9e;!$?`N&htC!BQI)C)9#*2+V%nyG5RlBZt{hei#S4PFX zo^|BH(PfAAHWog-QDdFEzM`l$voyMZzq`acmhXA=r8j$AtL-K(y7TH*>AuZtB5tTH zFJGD&9DiJFb$#|y?$xGO)HiLKH?F@6Y=VFL)SWN*;eSY~ZSF_sqyGf=eW+V+-fb$Ynr9RE@N?>liu8k*?nS@& zu6EDkefZ&7WheEQT~7HOGOKXc45n+_KlFc7+V-EJsl3AfvD@U|9u=#;fBU-s?*hF& zvXAm_*7>u?ME~%2{h>5Z?wa0OJH_Z9YhI*X`}HX|?6|w9s+?B(=hBlVtsl1VET$?6-!EWuUlj`3RbMxN2Zu`&@wZ84yN0B zOJm+IZ5^*|Y_F5=>UN)5`dsYijN7&s?p&haF$IQq2?6E(zgXHY52zJ`Ukp;GwD1t{~?^e#h%BGsUrPbsr}~Xhwa%b zZe4!V-rctRqnLeXozlnlK9l`?ujfg9xFa?_>>l3-+hxrU|1ty$`9qo z`+r2V-98jh5r23e*VYgGZ8qhr|CmfZWIlcSJ)=uDwu#G4@AdO{zb)a-kK5=Q<=ty` z=v6M80snLUf1LL}9R6ncWB+fzAL$3>x0fH^FZAzh{pP*j+xNtmulp17VSQ`b$Fk+i zD!TnHS&_3UqT>=a{a*nae{c`YZh@nh_iU8d9fZ1jtB*Zv6lbv|R z$8*h8I^XJ6_UnG$Eb$Mo1lFxoo3U+rg8Y{H57GKREbfDDg4p_p??>)$n~LCX`TrT( z>VDKe^q0?!7qf}2UH!*-`;Y05?8PcByMDOe@yC48%V|Nke;6O!X2srCvbtXE_4ROL zj#63o*xi$T%bGo!{U_b}!u&e^=Iwuc-+wIq!2XT#gZ-PO{BIU7s#yHk{ZM?fmHI*c zw)@h0|I|DaAN*%{W6$*?NS?jI_^6!Dk5uudb@`>d*@va{<}W!~z4S)KxpS9~ZlBg) zU%29hTjttShCiV0sqBA-gJoOS|4@Fm^5bdOYjv#akKb?IFI)dm(4%H?|So9*Y3N|_4Qlc#=Gsh^w#U7*!)#DvINd%n)W_mpa1XT z{0}kfd+lZGAFPSz+y7vdRr%q1o(lIP_u012XFB-d@I%>WD~lib@|}M8y(v7X&!6MR z?4xXlJ@#J9|Km4L>0@!WoWiDyUzT59_R)A>b85;gx#`jjwblO_SXcaKXrA~r|A#jF zn~x9tZ_YndzuBmU>4Vwi$LIO~1b)1?e)S*s_D6R6AN7k=L( z`uq|5JJx^YUVYMS;fmMB$CuxhuGKnsuHt=z-Tc3k{xdvyKfinaX7@M35A<)w2mhVA z$Nus8p8c%QnYHQM171>ASq^|Y#KGHRMXT9a)`_4Ci92dlAhItok zetCVH)Y&=Lj2&wjkNms-pW#D{eb4x<8EnBl6w#NAAP^_Bw?h!YgY`x*xh9 zn!VSrUOZDwPUuJGnyjibb@Cs>JN`3l-om-Zx%|=X`2s(dWZ&95uY2L%*6kfCTQBCW zo$s@0cLD#7{|pZ{-v3Y>-m(9|%KA;$x_@g|1b<6te_-BHnlDljf9$?!X>IbOsz2UW zYg|`W_&(ix@sIJt{LMe44+dsP@vhsE`*ro&us2y&(^h<&cB|^f%V{TzRbQ}g{dd^@ zL)rc#{5PI|m<4>k9cJ`nv7 zwz_u3-3q%CH=;IW+UaiYKRO{(Y{Q-H?Ww!=?VG{;IsG5U{{IZDCqMT85h{M{emq_- z`#%Hw+xiT7=6{#Oeu?~N5ZYrc-=+LPyz}jz(1*SGM}D1aek{uzm(TxWw!e|nJ&TVu zQQO<~zRY-2>iBe0ZPbl|PE> zY7#!mt^R1I-?{zQojSg2YigpG{V+ddcHsyALC(c5gMM6g`S5IO{i9m9?7MECPJ39z zRPj#OJ+GYMi~kSx>3?J=K01FR`5XTS`!`cp{X2Q9{^)s`8qo`-^<01CeoVSAbiVnI zwO(uW%2!eA8pV(3b2&cvn;&pRZ;z_kYSW4H5C3{Mt0r*i&71BOCbj-=rIs|v&yoM& zp8rFy{fPY!)%>^Oza46#uKtrc{PFw&`>o2E-{(uz2uvk2LOO->&?UYv;o7-2TB5`wubvUG>|S{}FM2 zVE^{z!Wz8``}p_2*;xN!+v^ARhr7?F?x~o@{?Px3_>ZNDm5=KM^d>(vek9#5RBfZg^S+mLzU<|E^T$42_sKQk?5YXuFU0?dF8{~ZdHp}bgQfE~u6_Qd|6%;i!aCL) zf1G!IsQrG-UL?z2c=98KD@q^2_AaS7?f>C?WiN#e zFS~c!cio+adaHQOQvX)_t^99SK0Gf_cXPhTR{fU!Ja!g;@^^oaL?|+m1nEz({x0xU3A6wsd zU)WCSe)j&Y#}DTVSFn9Qbj$r9f5%7nhu8eMf5b=ZxL9)IYp$7W(C?#BEc$XAAAMYX zvm)vAG42&Fw%t?Nc;NxViN90r1pghWv;RBaKG$FJ-{tvR%DevY{we&C{Nc3oHTx9# zwtey+a?f{~obUV3Ah>?^k@agT@{dH#iIQ8+*1i1Yr_4I>%TkqJ^SM1!`MdRO+(rLw zWN**gBE5m1@$XC<)eqUn>*e+dRXBfZ|IxbgkNOYUNAVqXf*1a2{qeq7qFLj;x*{kMrf z_CHvE)B91~&X3E#m4CS2vwq9|R(sJs-V4q@oLhcePNGdCr8{*0#dSI*I}5aHZiV`{VX+%Zl=&lWzRc{;~K`Ez6Jc*pGRw-=rn;^xkjzcIk_~q0ZJyQ(3Lk!LhkJXRN-^PCw{HMf!Yx0A< z)em;hRX%*5E!zEB@BZdsmyayBf_G53h|sGAp;e=TGvo7hxCf#AJuxllXA;*wVE( zGYf9r^4(~W#|pD=vAf4lgA{AT+@{~2zre)PVfK6!ojt^3F4-`sq7*T31B zAGSZ*-&tb0$9hf0cCQ^D{`J1-?9>0IorJRUd@qhhdA^f zvoK$+Gptek+q%c_H(%YEc#%5ce;5Cme0=|Q>Er$46_1X8>-@pEqCzj%_~3o6J?cyD zzYeH~4!g9+->!T4-)-Lmw`~7#W#e(St&cLcKE1v*dcFD~cZa_7feb5u^VKN-?Wohr z+`r}P@wZcdd;Zw|t?J4i_7BWQ?ezc7&G}>afP25R>&NKFp*_X2><4CT*>3Cqvtp#yE}P4HcGLaa{$k6k81L5j|FHkfks-f9{Q!S= zJ@5WZdzp&k-`;-gKTvhb&X_j*C@m|3Awf&EX`osP=#UI+g&G;DKSf8lhw@>r$ z;{EBp#%>>KkGKER`f>QM(2wY_t6SfCAGRv}&(QVd#@qA@f3!{C&0q97I41IT7XQOo z-g)_3BRSS5Y}j^Hzrozb_e1+P#ZCV~Lq`0M?c4t|Wa^7##cyA>`s1tbhwRM%PJd@3 zS(*E=p8t}l29>f0(tPFpb*kt}Ts8P7QPw>a{Bm1O2K5w)U_-A(2w7>IKbY}CR zdjh++b~}G$zH#H;E$=n2^_4Rx@n+RZT|Tto)jsLjvH$LFWB~QoAN}29lM23NeDgl> zy6g7*HkwO*eER(G>9zfdtA1#Q@1O2^c~AC(`JykS`rH0^F6d2@){T07c>DKRp?@Z4 zM!hk!S~AOSeRg`O+5DBakK0apAiv`NhnV;u7UpsP8JhN2I6q`Rq|aZ&V$b+bphowj z@B1U!r)A@hi`^I46aDCY$8;&)>uq1wdgc1vJ~U5s^Mw+9o)@>ycdpSb>9){ca&7sw zzLhKP-afnh<+W`OU98T8AHBJ%3=Pefml}``a}hpKVmd zn(dp}8`FMt=YH7wM^g4u#rfa7&t}Oo`AeRDc%#O7d*ttsSv6}P-e|DX{?E{)RDbZ$ zuKRz4)DPH;)$#tA{H@kr{-5X{kB{wtbk@q<_Wsc*e|Wy|5AEaWhi8?ymAZEa?D#Of z`_0z-{1x-PKiqGAuy>-VPuB zu>8PPriWc!_F+|x;eUqKK=lJJZTSA_dS{q=);Ovk&RoWR+}X8@@o}X1 zk>rDiUHyN!NHD+5|Dk*TkBs9Z{vX=M56idKGyO6CZTFwyuziiz2mU_gAB)#VzuGUL z^kd>9f2oRp{M@hOgf6OxZ`&vMA$NMai9AoHo$kjT!GpZ}*4C~+yY^^lrQLkdma1xo zy6~6rph}p#|A%^eN1b8Zze{mOTmJ6X*=Kq4N3i4H>Guz8H~yg?^utuOZMN?H-gh-= zyJX6y-Rim%lO^t1QWGb|`Q^qRQ}yO&R!gkTB*x zmA^Cgr=LF0`Qy;~=9*XEcDX*e)iybI<^9_)%fFi~I`-(dU)#HgH|y0m z37ykOV^zMDe2dh5>$@kfF z)!DxM#}zMGQFZ+2#UIi~cky@plk92x2Epfn5sADT6lxZ{f%=1fTlyI@{xk5E*57>g`Qg;% z2m4!%vo=+%_q$Xh`QiAHsQX=)56#p5Xn1*x_S%|?$I&1CdzOCGTb;V}rMOk-{O-IQ zlZYi!roBf`u-~!&5Xt|;a(>u36!_~?&e+ZX-VeK<}bmib6kN|C#H z-=28Br4MiK<;?l&a#i|}W@^K;*s-gRL9-2IOO zwDab~$M1iHZhxEl-8Z{FA z!Tg_rmFdI#L+|kXl(MRez}WM``Vv(;u16ovo9oaoqP~c53@8hFSFV=bM@(*&o8HkZJR!k z!SO!>tMh+`rYC;_|1&hT|GQ()P@fae`$zBMK8}OGwS6C2_c#6r-70?mM}&Uv${)_h zFVwhq$A6sOA?^BMi`Cg{J0E#li|@Voro`9U|8mJ$tIIxHckZ3~uGG@O-qHTSD*F$m z?A`Sb7VqEiZ|iTl4EarMGe7v{ciDev+kdQI=5CGuV+ESho2wx z^PFFNGh0se!?o_p;x*Y(_g1cmc&S;r$T_pLv|!dB1wPPTpZbHB|7rebXe$19WS04x z_DB9V{Z2n_J%8x&qa{C#A6NT#u|IseciZeo_k~{HGn(!E@ms?@_K*JG=C12~08PD~{y8D5C`+tU1%aU662l>rQ zuIJWNu6`M(d-+f9vLE*YKVI9vW?S>a)z)wQqpsLkKeP^Ba4=z3+}!0q;{BVfcdzcg z>-EWj|HAwqn#=#lCOg{D<#v>TN$bxBSuH_BNCAz4Zdpk6O?7m`uC$ z$9(;ZeVRKi{gbXptWkgX`>gsuA=&c7yMoTHs_2fry6oZ~n_Kg`#pbWNAAfXxL__iZ z51#9PSa~0n|KJ;cm2;}?D)gI@4MgI@%&it|6{TEVfV`>_a%Q!5B<1p z;X{9^%;g8_jB=w69Y1EL^igeP^17GbxBcqeaLs`4tTIw)PL^@Ba{ezw7?S z@INAtANT(e={|Cw+s61~b?o2e(vSYmKX$(T+OD%;q_u^AQY(Pnj; z@qBSV>W|h5fB1PZJnYgQ=iJhTH}7U#zpb}^@%0=BtNI^0;(x@AkNs!(5VHQ3`vdi3 zYuO*_Za)yuU3W%P=k-TBeQ-+qsO^1)xc@1~n~r*Adr{JLK-%l<=mW_Wp2 zjbKI7^Eb2RE?}SjpW&eV{s+%})BkbS{#K6tcd}0R?{x9U*BT$@3s&quTF?2%cgY|B z{)gKZF1=H6>R)bg*oWiYYtuj2_atB0Qv4|TZglvj>m@JOZq3#(wfu5?!=66v><$Mz z?!WE(AMB79-=D(&Lv#DF{vVoR8$X;rJYU4L|CZH%hDTH4CGEsr*Z4o=mVVD$6a4X7 z_eZH(^@H;?tkOH)*%+5UlozeYKm6T__3yLpbxYs2&DNW}>eh);$^6^{*8D$o{xkd$ zH$GbbA8y@p9z1wu# zr5fwyyl)@$^F+Utli6@-`S#0U_XDSWj=cKhO3V?)U;e*y>kr=DFZ!P$+x~|4Blb5- zAO2^!8SVTd{G<8f`opvDx5i7>890A5-4pomc0c=%`M&+mKPKlt@NeI~k7?7+i|HTV zx34Qb&+n|a{?XkqfA-8-TP9~-%G$NZOlsaN#?PQLQ|ABUOZX`MM`-uAq>uV*Yd81m+wb@xVMw{c;lbU+;uO)rMAs8&6f4L z`DJgg*QEap!CR-v#&PP{Z+5UV{5x^~gPr@O>eKXpXoVlW|3h=%;t$Uc$Jt!B&xrMY zq`LlCe#?bd=5jI@zv?sparvm${%EF+>4X0a!lv`KRzE!bpMg8;pXi60TVmTQmuCBy z=RRD&A?w?8f$Z(;(-^P+XE<1B|6u2?`+vmbkIrXR-1tLjKGQz28jh_WOa4iJxcj_k z^6`5bAN4!;REoF1{o}eW`_`8_xerJCW-oV6e3&ckeQ}TV@|W8SZns_i>V4Pg+v5u*c{HrXlEojmzl?ZrHnAFDquf2gYFGgnj`cHen*?VC4S4EN{vIZFU5D{=_#eUM ze|*6o<^Krve=GSo{mt1&=WlMT&+uo8{c%11$J7tse*6BI%zxOn{Lns?kE+w==B~}K zI&--~`Pj8x-`l=QuT?u8ymfyQuW{JLywocX1+vAimvt^{s44$#^Pk~hk)2Tu|9^(2 z%{4Cn8JbqVs^_UOe5@~86MJP(ZuUpvhoajy+qZ4;zTf^&Z^LDi*Y>PGs*g_2lvCMr zQQyWe`{900wW*J~*D^;;dzQ~H-RtePqd#o-))wXq`ycF#{}8*k`X9IG${O(x|7L!$ zvTu~<+{bqE(b-^!3;KW3_St@n@B9<|*uuox`fq*g_1rzS53^C}i{`^Wmn z>&LVI3_{@nAC~M=@A{*w-ux%^aXsH3v+jdcrBAna*?2CFdv!K*aa_frSn0!7%a&aK zaQAiDw+Wf&7(b=|5%m7YXM1h`gUR+xb&CJa)oJ~mE&TZV;q!t&-Ut6!{$TfB?MEi^ zd^VmBFhABtxmk0b) z>_Ah^C3cn-=Kr`^*Z*fY7-#yvZ6Dvq`aYZ3)vwL!)P7i*N}uQdkyhVUexG;kKEW6J zST5TH^Y$%Yar0H3_y@D84^M3Q&mehram?uzuYa*b9ox7dHtJPW+oT6_YyUH_?)%Ts z^y|m$f83nuAJPx(sy|R+$55mGLw`+0`_Zy0)AN0kAGvqcBtF&`%KTOpvTNVkHT|4F z?2fx!`Vy!U-K$GtUGYe#*N($bC@5U|DnYGN5&Hr4vPOoYeIkUKgz%P(9XU_??>a~ zo&U5le3$$IwE_ws-RG=$_WE!Xd+XMWxL>ai)fry?I-e)|S9r@FquwOlEw}FqeLD4@ zA)xJj$iK^CWeM`8KkEN+cYmDi_Mf5YXx9D@;V=J5|HyiOEb4yOy89tNH09RX?r&db zCux7!I=zSaLM+3XUUDPoi?_jy?1^&F;rjwBhLJf zkNqmBBXLjYhy91yu0L4rGyW5<(fn9)UocPC)Lz!{BlppHIvsX~ zkx%A(`TuZR%J3ukA4lbX26oPh_J5p=59)j5*|+{@xcSx2u15XG(#I0}R9}9J3O~Z~ zqx?}nTg9jQhsElocFy8;`+TU*c+>SS@&cK^_}g^yb=Ivdc|BL+W&WyLF7rP0Rjn$|3r1bLtT3(-1h(QcaNH{ki5k5)>3%^ z=f8WkZok}o`k?~jhv|O=-v8tN&+y3phw{JXkMkcc+x8XqgW z&u`6dOrrhy zf8;yQKC-Fd~|A;t0{GX%ZqPyyZYc`d{fR_y)C(X(X*|& zy=9fd40YH3dQl#A^n82TNBPeDlHF^48+0n{AAEnKy{i78^*+s-)Q{nB)gQ}pfBW^} z`?2-BKaw9#e{(eZLFD@y;m7eU;nSiRukA@(S#kUgFWc;T=2v!VyTz9u5u7c(pX22` zxygsh-H%P%^=}5_ss9WI?e|I69skcD{+}U>pSeQ%+uI)xAI0B1|1I~*m-`aiyZ>qa zuy)#U?dw_PuuUJ9Z7hAr9kFoJhx)#G(zVe?12fCS%iRw}tx3D*ztQBE`fdB|UoP-Z z-jkj9aIN{Vf9jWQq}QB(WAC`CIeR=<;`)vK;vR_NjSjlazT;g%=R_wLa z%T%|1`Bkze>XhfLn>+d%>@NIgXlnSQ{I{)6uwwgzW%GY?|GQLY{CBC1;iLM7e>@-K zkG@SmX2&xBz^5PbkHwanEt;qN!M8=+Dy-e)V`P+LVs-KI*|sqiRn_O;th7mgG<(In z)Qt=qALuv#XV}QM@;6gW>5s#Y&)+gW@=xaP&KlK^$p>%oAJo4&>+-?r;c>!0`af(A zkrT@ot~eg{u|>+}nwV7IOu3bhwL2F4XK0!2KkvW~ZNJUCKKxr$6QA&rq4ND3$q)Cx zh5p$5JG9351N+^1Ay!lJ0SHY{?fo z&0_VwTerlQ>2*;5;i#XNbH8fp-8y9PxYp?iQ+?z8EgFB@?DP6L>n_G`PVbOEe#`yM z+mE*cKe`{vtv@b}C zkhXurb`|FD*8jK-|1+@b{Am8iwfeX55C3m5ANt>%7C*qx>iuXw-;dvq>K3p3@)r#H}|*Izw@{9n^LdV2!9MdB(?S3 zO#bfD*y&c8$R`S+*p?~30l zf9O3EXeXq^9>)*d-*T^;^2XVB+Kc`%Twva5I-e&h>PLR$o{K`a4wmE#N|#*QF&s>_zzEm!NU+jcvE`N%%q7yJ2aOg}b1YHzO-{INawN9TvxhyODO+GH>K zWAO3a_J!uHU+bm**kAhNz2e3X?W4c;w=J9WaF2ccBYXaX%U=Cz>nv0`yUe%Rw53m_ z{Y(J=>iS&ymj4VJ*%l+7X{`wsI_FLCf%WUta=#zPqYO@b5K0NJL?YxEag|_V$o14M-WdBBgPzUCI=l(7457{%-UEeQe zy1sG$=Jw7SPqX2jxv26L0lcHO0n@!g74wRpb6dkPr5@&&=WqRf zsQ$+N!{Z0v-y}Y=zZw3m_oM%@`(5*e>)3BsEdRFsgMG&y@yds@;~VQG=G{O1`A&uY z;a^LSbuT`;KGW@c&_}s`!M4Xo>UVsYI=%C9e6+vB%@@BaSnhA$f2jUO{L%fJ^xu|$ zl>gT7cTr98huhzl{wU0R5dL?{KkgsJ2ifC4d_Pv}9DeobwvR4$s@J6sMU=eGRlDJ$ zZ~pq9@O07@AK^#UU9Laebk&6IdW~bQ-PG#E=M&dQy}IP8 zdT(-hYWfNVcJcoVtPB1#G;OKa{EzGFZ=Z_t$9XG1T)X_>eN&z657|1I8nYkee0yx7 zSG>F>{_Wc0N3!lQALYB+CNEgFa{Z6?@T=SN$(~sP5-PN}9cTA1NNB73(AMQ>4;QYvSRR)th ze?@sjt+@O4D>~~+tM7}yn077fS9{N_TH`}f<=yj-gq8i%-E>ixLH@ViABDedf69Lw z*WHw}{<~y)IEZ$JeNmiUyJ(I|Hz)_$6S+mA;-kEFU+@XS1kE`m+RBv zsEt?NuUWeLQr^FlYi==A@&6IE{>MG>qw_y5{olEN82+xe(f@Go_ygbVZ&vF!zx&5l zVdk4BFMmIC*Y_4Xg`*$k?8Sd9kDl)G;qJ7JtFm@|oz^qy#kXIk+omQj%a|snZTjSM zmQ?Qz_80U2h%*1Z4fxqbC>tvAf{k_{Y$0F5BL96~#W)d*frTmP(&h_ZAlu`(`!u%IQ^3jI;kUuq^-2 z(7fzVFz9rhzujBxZ;BtCziDm%ahD&F>fNb7bRTQWcQ!w~92xoJ_>o!Hd*0jFK7MPs zNBGfC)7iQDNA1);@*Zv5yZC_5W~z5XNlKQxcOjsC#>t?R@4L%r4y z^g4fQ@9lklP@m)MBgc>2;oV2d?DQ_zBcy zcbdx!*FQA5c-d#;m6EysksE(hK4kx3*Ic<=zcX)3v}^R+`XjTZ+>^PiI^VDDcGMnk z->fUl=j$I#xc{Mqy%RLFy?+zazmsy>f2Xajv&oppd)+4WQGCljjZ6D9|1%ugyzi3Q z$7LHUANhJ8mNVE?&-mk*ZN8hoaMrACThrIv@K0QM>u}XApKan3ju~7ggJs{Z)r;Gyy!_7~YHB^7 zC3{jt#l#Zb;KTEDFO{rbEB@Qv=jz&glhfI2l>;43UtG&Q02%>o|0Aw_EdE1ieZM@T zokqrg29CFNEEV?O5~b!IKNQdH|H$K?;_J^9-AAmRx0jw5tk`@sPPvx<(6#OQh5Ai@ z6f>7qlwEfJt-5sE>`K*ptE($l>^GvcYS-kq@AwwN9%t=`F!%cafeg0AFuXj_#qU}{IYykR>gk?f2n<)3fF5wSNT4S z8Kq|A?%Q{*nFgeW%p@x3bkAo%nnHGjP5!&8$&eoE0bW zWAS5I^W)}H>Mb>%#mj!I`h2+fhxd_rav!#SYg`eVIqUHKTQ72#y_%?bC4GMfbN)Z> zQqX+kNB2Ks?8oI<-|5uxMAxwVNc_Nc`j~Nr`k~JiJM9!J7Ds&a?_cua{vVZnDj(T9 z_CJ*RuyyJdwabU3Sf@>0nE5N@LgbA6_wT;)dOZ)Rcyqwsdw;I{59{_v`frPWGyb^! z&8PpH|HT^SkI|3S4@dX6*^Ap5IDRyEZ6_oZwl?gw+HSo+QeL+0`U}Pf_8;8pe^?fSmJo#gJ9AI-@AN&X-4t4}WB?t;mo47-FWs`bYW^wZ{kMnYKrNc=~T;!mKq?TesgZ^;OC~ z>&AAubjH%nW(@M;|IWmJi0SW%-~9cLX#OMjKcf0EKhhtrm;W*Qap?O#oqd{rl(${Y zv(a9ZCCBk`Q^txK!-xICCzt=2^x0|mhdXuRAErdvZLeMYIzA_s&F#vUpTglgw{K^7 z^Pl0tPWcb9_Mi>#Ni~5VjIMkCP?bNB-@2bc{Mhw6i3@sv()QVW^zZoN`uOUHMOnYT z?Oo&i@IKGWZv{7^E8P7zUo6Si=AOU#=da$Y|5mQpdq*(yf&7yCt?qwhZ+~?EHUo5; z$dB*e{AYg)yYR>O^~acyY?e*sXvGBf**^W@_9h(K!ks!$~SF!z1KMQ zV_bLXD%Eu}s~=16RX_Co*OKi4`*yOZU&`TKbN=J2ln z3~4Pt{xdw_4L1@nGYX8q4~>O9k6wwp~^Hcaq_!`GfO+#Qh()AJ+e&{k-Qt!-KW^lzvn{TrXR3 z{E^<$N3!^bVUEdTJWx$wjBun%!FKYVujQNQ@dGG6n5J(u(T2tL&3{}FWCzwjgZu0M&7-it+Ld<(a_wt0>7!+6fjUuJU`-h3&abg@KQ);dV%*R<6u?q_}G zd{eU4fccC4WB)(0-#^wr*8d~?pW*2KP2U#()_?J<{4M)O{Ri(hfAD`KethYVzUy&3 zJ1^}Cf4F)+SH#beU9V?VC<+-q;K4P!b9^+SG-^_j)H`J8= zXJGO8&(Pde>iUnLvAI3}H|KWe= zp1{?&YCEPMlr!FX{g379%zYdm4o1Y5tlq2l=-j(dtCsLXw=OV0<7cfuSY&5b;r)-B z`%(Q5?Irj5_L#5yvH4h>($&dF>{<73UAE=J-}{Gt|7T!$d7tY|ovhWPP8-f76uvuLeVP`yX-Xf84*<{AW0r@WsD*K5vPAqtG9g zAD$m($6wh~tTuaP#l72)_`7OCAFqvf^%XVcp6ho1&^_VJ7fa&#ZuqW_$eiW7r2NvW z^D9rU$ui$|>0s*8n+>(^AHDx!{rb`I&ixPO?VA5)IscoDi(cN}BDVWO(DMgzD?k3M zQ<2;w%{F;OMNmFl);#M=`^5h<91<3NW2d+K!jnqfEjw!xO0;F`9=v+@eOKz7IO!b> zKPLa$+!y?J-ms?rhyO?AS6|z9y;s$6G-_AJm z{)dQM?|)ohvp*a@r2RP7{YdxoKT`j$?)}fuQk383{IG5Qhq!o#`a`j*J-?PNTdjY1 zuO06^({|69Z~sK8U7LLPY+}K!kOXmNLvFI0ap_MhAztsjaH*EcMFz)%1w56kgy_;9`bkLi+%{m0C{-Adg% zZ*}?FnC$G@%KAjHxyj3pFn%kW{-1%pye161Y@lrZCZ76D%bN9%OWQYc|F%8&@#8*K zkA3{UT=H!FOwlz?lOI}qaE`fCanVFIdgYg8ACJ_@@A#;Zn|Si#ogX0#HK5&z_Wv20 zoBw!%&R6+hfAoBFoXQWb`SSbt{`h_@uJO8Dve)>y;2-Oc@lF2}Js<9sKXhx|;@s6S zKa!7LlX^GtQLXf0Inj+*Oir)0_uFwP<-(%^^2~#%Lldom3OkI+3|d+Zz-{s=lKzQc=-|Wu&C7I9bdU^ z<$bqLkNPOGE3E(T*0nKN;vX}mQukcAd5G~t1Zy9I$}h&r4vaxNR0^tOn)m~+q+hte z6u{^Jx*q$p@>Gt9sWJ?O7fK|L71*~3x1P>rE>2-QZL>l^IhW)1bgrGeb8m0%nReos zr+WVBxqAYbBvX<&le`+36W9*@y|Y15)lJk>WmRi{)2arB>t#jUG7kjU7#PHK92gkb zA|@~}lt?fzFc@$CbX@&9vwbJC`ua~_L>N{pYla0e#yN8BeB1N!oB_wVf=UUMSqv=e zWKKw|o^ZIRVF_E%atQ|hDH9J!ylr;iICpk?;kgT<#eEDajqjWMjOSUrU|7Ijx#@|} zo5JnVeGizPXBiLe8|um`t6{>*dUxiL)=1{xk`QVdumzD?|5?$cl}J{6u>vyS5+ zcjL}bX=WLrRXdegChnMfWD>*C^1#-l9oCx+_$m&bc-~WZUNR@SIE69Dz290|@|YuI zg(eOa0apfPYXim(YqyIB*d@=aFhqPiozlwacxwRzbG0b<#0CQk$z$H7 z$NW5>tIyG}(Q05=t-NSzaKjPd11GgOWFA{ccQRYtZ9Ddqg~9UK#ACACi|2(FpFC&W z^SCTJuk3wVOX>JE}=kV~cGhbfr^JLn#}y7;Pr8Gd%glGx>m8gPg+Du7;U<%nV%5c_h}k6)m0Wt&t-Zv}QrO(*YjC z9kEX%^a>{#7!~*YN@vfj`*!VE*)|4A2UZ5l(AgHUR!)03tfHnIyI_dHf=5Ln!_ zU@_C51zL^?5i7I|Kc_av8GmN7YIfj{NUeDr^MLvLl=N~1o{2j*PSe_%uy97hsv``_ zu1b?6J#Kke9^;g5yj9#L8OE?uVPb=yn}cxjoQIyb%w>ujCm0{A5@P)F-hun69pf?M zp42I|50-keJYkX$IL`2JL!J9AhKKpa`L~7V>!co#S#KfT9^msifduk&~Y6Xjc|LJoNIG7K| zYOyjuoxvgzaq~t>gL%St5e%~yD?7}`B^Lor7z^-!1t*^@=l4}~P;HD6(zDQ4Iz5@*o zIlTv_#xNMpy>C5Z&IAt$h6!fPTN2V`(j}ifc`}P3Sfyt-_qH<&Z_d#R^is~1$qQ9= zPPR~2pFDwG=aN#Z+bu>Nvj$Fe*$Cb;G2CXGbn%vXu(uz3WF!dEVwH|6cd4l`XgWf$!4T5v$Je{%e@#K>aSeb1L z7#R2(N|+i%Q%V$83pgBjdElfi$1z9i#$%xy4zw{eFq~E_R{O?R-ytJlxSd(z7So>v zJPbPm7#ISW#FIiM&t1U!jbReQFQLMJA`H)`K1q7#SoUPwRhAF#35ExcOT4h(wS&Q> zthad~ zv9PJgymfA}GGHx`NLFlMk(p5B=ODU#Y9r5qC&{Y97PrJ1w>{y}c;RlDzvqkxbC|nB zXf)Rmu^6T$4Gt;-j>?4!kDHqe926KuZ<^;?+*#_+@WDf!rFqN8@6*{OB@ZezXnVXr zZ07L7fI(*R90s-`e~WW`R)r3ndz+@PKUER3c+&G=-g8NdUl*7sMJgZcJtc6kF-}6% zQX+p3n}Y>AOM{0B7X#0wjVH_0bt-M|yi5C*RJGxGgK>ia!=*RjnU(=ZH#UCnIB0pS z^2<5n$vu-Y7}C?YmON3=TFj6jrsK{a{8^~-{8I;pGzOjzoC*nY9tU4pvJ^Nz_%6a= zym@1~>WohjU)6W2?-o97crs0O#x#`)Y!^V4od&2Td@zT>^8m}_#v2TtwhbyDG?-jG znYA?z2Ja9G$<=Z^&1S(+D&4@s+|!W3IOcGdvy=A7$&*ur0%(znwq;#zMaqeiP7Xe1{$|F z8STkC_Vffx2x+$!fhuTdJ&mA*I#hEKtKKjOE!bGF)Tzlar!DwXbAzLW2m^_|{MmkA@4hC}q)Yipog3Y}^AnsN)Z7t0p)i3( z(BsA-!#lUx<&RZq{(EWjxXg%Axgmq`QuKMjBNI2(zT0v9)R}g63wtZ09{HJ5Rz3OX zz;WT!f@w}nbJw1*_G3KE;2HCdAzd=zlwE29gG6BCK?!T;;>Ls0jnCIPo?xDz$IIZQ zyG&Wp;_i0qDGZgzJaZUsSW7&N&(6`^@_=z($n2(^+%pB@>?-pg9{;|JcYn1CPf8a< zUjr8d!^s2x8Tg+&@NpcM2tHT%zHk5ZFLrGW;-_mSh^sKw1xX|@6;JR`W|-65+{eq{ zaqaR7w~P})>>5SOEByBav%F)zT0BQ4ot;5&#uBcWmAAN>4zoJuyk%)}(A~O$^Q6{; zqTU9^q%ww8PmHyiJQ${_hNMhj^H7+4;GKg~gIw;j&^rOG$BH~&L{7J@%5#%^`MrMM z>;{R8yK29rKIxw!c!FW-^B+@;-o-6eR!9k8p2it5K{;o2ZbF6D%>z7qT$3s!j}_eb z-P^!w_L-?+#ox!G&U-mjBQ&3wG4QybIikrTP~fP*rJKk1$K`mENq>**ivEo=<~(Nn zu)phZTabgY;jXEy9(s&^zZ+F}cRcuWU?ua^(iDr-1O_?XXGtM$o`OG@q@<^Xh%wu| zy~E8oskzvIYmEbQlw`=%lmxeb{ELLvxvvP~P)SJfg`!#cH!hnY#a$5|6VhNwkxxG@=kEHlw`GOU2k8{J(AM;%0Y!mWCFv}?K9_}inwv|{0W2To+pfR zCLgqrnZU`+@PM&bdO=vKV#J29H%C2qn=C9&oa1-mcj9EIzxj-vmBXUQQHw*c_ng1I z)(RFe3+btot*16H_$wud_T(@dE1WofW_Ed#RqyW0Wd-5|=ZrKMB2v>oUFEE8^WDQF zm~3n;Gmq&Q_na>b&l-eJP0q+&o-M}M$24h%j**`^!>O{b*3%i@Niu{rh1Rwf{AbX( zz0>ow0E0ymqe>sY@=69~2VRW>Ob!PRN~EgKy>WtN;@l+-4ys-oenxsSu;fqL?V(k0 zaKn;>2T#ll`yZXZE3=K=gyCsh*wb*!bCxl4>PymfY&lqj93)kk818HNOx*B9+fc4+ zRb>AKt&R5Ywl6%u#8M_1s**0rZ1bV;r-1)<6Gp}DGXpIBr}kZ7)=a9)f4~#NKXG5u z?uyqZPB2TE2RX(Fc-S-{_=94yYiJHiv=tT*thf@&^FXsGQr}(yyeU9-= z+XIQsh6x?gI=drM^vb3_PAg;Z;68bRIm|uwcSOHo*?#|-<=T&LKXc9L5pa0cAa&-P zx8X^PlkW-`PJXqJnS9bxWs)bz8=bO_T!nMKFbGZvX>d5zB+ilZ+hhR3=}U|M1_d2aF46 z@Gv(xvQ#zS`1Lz|;gf_5Ol$hOUM=TVU_8js#LOn^c<@x88=uk6TOJC$D)awsV7|Tm z=by6-woA)H-ITK$7`PTMIB=3dOrd~V$Ss=vSx$q(#6wfqpQ>9NI9DxDIKkcG-~(gE z!v+dG>h{l>Kgn=0XfVE4K2fYJp2N6vLo}BGW8M;0MmK*hQFpEd3>LmxZx7cVIMvXq z=*(Bv#KCTFkoZ}f;gvH(29q9^JMj3p&R+C9@56lZ%dc`Mcvn)I;#Q30j zyW|t$B-q$LDNMYQ&cf`#gZ=I__BBCP(?iN!6^vO` z9c>wH85gjAN`_0&BK9POrf6ds_wj2w+9{v7qEP~&+_&F%Sx37Mu~@=7P4-f z2W&StgOcQfPrKXer>mqqUal+Dv|O>_-~zT? zRsyn-oQw?#AyX9u94sspn)scp0@%Og|7T!3U4PK(KSRp<9~#%+?tOfpvF_TZ{hfQH zKkR>S_x+Li{C^~xkIdTMa{tk`ZTqx8d~aVK>mB~#+u|iZCdKpp@b%zS8KST3;`vwb;PU)?HO_{i5e{G*MX#E*87@as`=bJxC_)p9Lrds~gm zwQ|p;F)zP`Es#GuZ|TE$;TN~!TGwUE$z0#ER`{sc_40jE*GzTg12$gzmWtv0*1 zd|{S&*vF`>Z`Y!FjBdo9Uw*bwbnly>s539EOcEF_)#yH|tveid)kL2!`_{FY4`c0Y zRVXMI<>X6b<6eX+qX|Q(lNU>ZQ8b%D&Nh`g?72J2ps!#T3O4j z=L{pm#x*Zyoo_ddb$0s5H*w_)t9z3x3zui@vfG?ky7bkqw1w%~2g{RJWPZQsSe{y$ zZByi2m|M(Kd{E1#Xwj~z3}Rp9xqi&|xcEh1;MF~u>s$6R9~Hds`*3b?;Ki@py7ET? zGs~4u2Tymp@-Fw$rmK%Hua}65x0`Q!=vK@`)`NXPLJi`sALe(}cxJDQuP8qpr*M7E z9^<9kwmEEZ{3B3ENY1=8&@-= z+>VTki@AOCR#a?k?A_BmV$-%=`*gmz)b>oL=pM5VF1vS%C`|b@_zU+15&VJ2( zJM!@)?%B~Rl;&|Ru6WuT8|lFE{hr82zv+u!>{Gq6$8`P6d$J!@W=*k^z5aenuG?kT z>&q@K>$)A@?Rqs&ck#xnDwo#jDKrOpOFqw*T-K$%Q20dG0ltei%8&L7>uvcb{o!~= zjl(>ti|cLNAE{q7iS-VRWj=hXW`kq>`fT6C+yaxeQpc)iXXKlDKW6RSvR%O4{T7?Z#04+oH1~hl-?3(o;kvt7Wmf7P@4m!(-FCdDzr}5vZ`ygUtIxLi+6U~q zB=_yCsdcq?=dumiyHj_0^n7Gz=yv|Fzq7`#)bZhZzSsK{t>h+FKjO{EsB&AlT2|iS za%ghgI82PW`TB+Pr3@%HV*Y1N%eA{I7D+>2Z~ z_1u*{iwVYidX8uFp)=#qU)lA2FLJbS?k= zm$k}A#jW&~vbUGU277g#%MJFuT)H!Nvd_N#mlLm){LFvzHZOI@=B{7oygMFy&){xw zU+^RT$UUuj->x6}Cwm<~VwMlv^U!PhiI>;DQ`vfH%jtwX zQ(mXizDwsAQg_a>IGDw7#HCU~CF9%~r&Wulr6pfr4e(Uoz^uNNA@@iBp?_*0>eiMn z__6))uJ^55PYX-kdvQ)Zt1q|QmMz*W~P^B zmzOSgE%`p}+jfH(Z!TApyaRk6zPH*0K6<@-H9uEHe*8z``gF|^1Od4e{6g(zf-Ebu_V4VPUEB4=G5gC$`9|pZ`rap>Gbi%hj+29 zUlMcwUd8Ss;dM$Ea<=aEU6)&O^h58LD6iY`d+)4c{PVc}VEX>_`Ud+=?SDk>f7|$X zzJ2OnFI(pa{fFzNUYN-VZR?Moe5CoX_^sm&d(cBktXxp%Nj{~>un%H2k|AGNgY1=#3KbX9qaUXl#Wza>H^*6O2h`&{Rr266f zLHo}8qB%A8k7pmdCVu$-t!XEhzM3_y{~_yjpKIrrct5@`cq8K{|L?g$9eR(;J?%H40c9!r|WtD82+}aiMwWJ5>;cC&s-57e*LSq zerFuhjt}mgU+-o|?YdCoy!JIe+u{dvgWW#7Nk3ZV%|Bbb^U3AXo%wq2x2(){uy@*@ zH2;TU{2#&MNBMsQ!ynE+mfvxUz2&QH`6K%`%jQeiY5u!0yP+O$G*E<+GCz+duMx1i*Zv1D=l@W7|3}#Wo8I4^ecUzae`oHKtnmL< z@#DPyvAfa_k4|r?(S9^H@11>D9LtqInveFzA2F}$Z?ExOn{9sm*7TZ5#*>0a~0aY7&dZGSXh z@U?m1jTgVz+f7#g&5ypCpQ_@TzU*aGig@_<>p>G-%D!#z)}G+BZ~^~X`vm^xc&>jI z|8CcB7C&(Q7XOjUf9!tve>8r){P20EKW^8T?TY1l?svg-z2ME8@i*%;@3#6%JrAAw z;6n11b;^b>KEKLd`82R4+hg;yAMp!Cyx2AMTJyq~ zSD#{>m)w3WwMXny=C7DvOSWw{e0kx1%wg3x(>^cVbn3zh5$0bD|1&fP|7UnGUw)(b z+q)m%f14D4>+@kN@uR-+57`_3@UE>f{h|9PHt9cu-^H!3^Mzl_sbAhx{P?c7_oc0} z`T-wh>{z7YTS_|C*!(oPZM|OBwYr|$SGF;Lul^s`^ZyJipzD&W|8V|iX!=%n_WmaM z!}+2&Yvle;+h_dBesk^p<9qe}Ki-{sZ~h0R{|t}pkDM((9B207*!)NQg0g{oIMeML zx7_Ec*mrSHdhzOm58Xa4yq50vVNaM;?hf`h`~S#H{LjEr^`D_ByzWH&2j8D|RyB-2 zav%EN-26Dbxu*2P=R^M)cydkT#eOUfn*X@}kBZdMzWzt~EU%-|yZ-pDJHGx^lzG!S z8~2aS|AbDgefCjxc7@TqYtKWxZOi^yI@l)v<5>QmfyL=RLz8FSrTZT|{9Eg#>dgKa zfB1ehzvDlHWR2?MyV;NKAB+CaAYak%_)-7ReSu6lt&et%b`u{~&*rVW?EZoIkIiM1 zYK1Z`bW(u2H+5_cfQ@{m0Hl_baZ)U$pW5SoZ3++tn}KyMOC5XZ{Lr7PL_>`fgRT z=}+LcEBSAi*~zyyui z4xV){fbT!UKMw2v3@qON8Jhg+uI&HdS)G<+l@-~6 zn=Vga{>c50_Z%dAZ0)n-e`vnWGE*!S(S zoBUDi_hEq_{}}?LRPRl!eB`ame(2=FiYXQKzPtK;uBd!ek(#$OwRl;^=UWVH^#|+h zAIz2i;Q#(+`9H4nzm00xe#k!Jm4A?b@IOP}wfXG%_8Il;FQuOBIye0Tm%iv9-`yXk zZGN;`bL(IK@ckd&uG{&bBd;d$Z|jPfz0-P1{xfVpn$Om{{$<&=1@fEje<*nWN3i-I zXZ&yF{|pD!{=|N~{H^kb^TYSe@)C98+56c{_P51pUb3lt6dN4=V~*KK*e>%e=WVg-+gYBOZ@O=CqVCtTd#8j?{}g@uWVwym zYsFb1ZVX2t)jRVq_5TdbsrE_tZ>oO-l>;4de+>SvU-RX@ScUze{Vad7KT02z=ltrM z^`F6Mt_{%4xc7a8knw${8|sQR#7^RnzR z@dmk%{~6eW>JHrB@cxbV?|>SHOZ$`OO1u5YF8}ENu)ed#_oM%V?d^XY>(^w=u+v-&Wc@!{jQnnMu&aaV`sg6_w}{+-K&pWS~B|(Z}pPQZ%*&NE@SwYU@Fg9vD)LJ ziM`OPdy*g7rawF{Y`0v#?dxB6w<|eY`~^H0-BDe(S+0H6o!hR7tL-M2>XhA@72#iS zO?`!%w|5b>5fYuV}E{Td3h;o@7lFT&%V8KG5U7o?ZYb9 z!^MR+EfH|cm)f%Ftnn_DK)0Sx##)Oe9T4E<7pN#dA}4(9t37YUa;IxHrjKmJ-o@HS zT(EIm7Wd-Ty~*{Qw@s%lUz%kQ9e*SC$Pv!mbql&O&hs@~V7Wd|^rPF{g)i=@U$HS? zf1@J*Nb#$%dvYInpY^Y}^`h$Sxt z#l~P_&s{P9$jLP)AMWSNzNgrKc$ew@jz12+foa2zvYqH+|{j@O|sSI ziSE9t?;~b**2F3E!ik24WaoU_BOh-^A2j)H?6Ki+K2N^Rr!#K^mHJe|CO#=w6czP0 z7FZzUuz)wV{&1brhj|<8AHElPVRgN2>tC%0x3a3__AkAkaqHOJ5%Y<#vu=Gm z_duV=6N5XBGP7l+^R5LssPFvH->tL9dRfJx&xhp{w|tnky>?~hEb}hCl2|W(zE{7d z^-PTey1P)P?Kis zt&e<_11@~+=2U6Z?s&ZSvZ-}-^zHEK$_I0WDwNaOGL{Aexg;_8Kdcvy`cZZG=yf~U z>$!i_SG_WyzC<(FP3eg3SdZF6Oev!r6%wyN%WxqVh|kGa^Vdy&z1P8Mg&d^l<2 zv1wrfdqwr3d72+{kA7X=3hKMed+&3}RJT58>xXasD_)07)wM2;wtcpK?S1EEw_{Fb zF3Aj=xcA!9l`%SHWo9Qn-}M#oieO0MSemA>(c_GzmS8vIaaD&Czfa%((Y|5!S)y_);9_wBTb zsp(sGZQt3k#rVh@hMzX-k7m@EviGWfxZa`kq4eCEU%cD9RW5E7`Ed2vj%jBrewj$U zj^@>ijnujLZoXgN%k^S6ql!5?y&Kpo@(;=Be4P5_R(gw#{v&%{ zW$Wn|CbdGxo(Eqm&0K48I(y}#%O<^Mr#=TxPY6cJAIaO+#Ut3&Vj=cZ?Rg z^WC_$+wa-kLvLeJ-r4Xl&%467^^rbz#q2{;{om}nYb@9Oh!6X%+WVj3TYSfsUT2pp zb%$3)M!$+meSc%Cb#?aqO<7i@ch~;yJwElx-J3DD??&&L;49O^@cE&>C_kTikAGW@ zgN@R!3SI*9xo^H2z<=t*xvwP28zu8qbKfAQH zzVK;f*tD|CcVlb{oRwFwzYPB)SpKaLbjeG$KIk&*l^@u@t@|hwC-X!7@qMY+0u_@a z|2T@b3LpKoulvD$p`>v^S*2zlq)&8SnWxbFGkKSN{amffp$>wa$i&+ukF&;5uW z)BZDf?CN0t<@2ARdG-DW)AoPxn}4JHf%}{5AG5!Oe-Ll|$5a#jvHxiGdxp3_x*v}1 zToaQyUvQt?!3TfSW3KHf|IfhjZs(d;*UhHK^KIR0eef3Vv5iaD?p?NMzu=?pu;0GF zBYYg}J?lj34|4x!c+gk%{g1%w!}5}Q93R&6o_#DY@b8$}eYvgcZ%gipeH3jk`l4Pq zzfSV#<6h^8?c4JO|1%_H-V$&76RxsP%$`@}(!Hy1bfgdOnj`dM&Yi3^j6Xs55zhP1 zaL{o7gL(4X^;_+;>O1Cd?QgNM{CN3M`{DT_Kim)g6aQiQXm51X#*2S6AJo>n?AZUG zp>^7=xbC%?x{s{)i)I(TsuSIu{bcvn4?90>J#ul;y&qlYq6_#B-T$F?{>}E^=5@#G zKLq#RGJhcdX5mWr;t$gg-D+FYOdQx*x9ix_kBN*S}BH zD6Yw}PulA1_vv@&#%(*dUo`c;`BhfwnJ0t2x{doodC?zMpKkkqEN`=Mewh9HthwK{ zwK_HKi(W;^KE2kz`nKw`$*ZHIv&yb*_FwjD+9dbV+lQSrKwBevODu`rLJ39nalEBY@6_1 z)w@RoPU{5P6zZrYiA#~y1Kq=pVS|ZeIJ}F`j0jH?~4~uZ!PhbZ;{jK5Bjk6 zc3@Xd|8ZXbaQz>j)(_R+0zbr$eVZrrcZ!`FXbEmlyyzaq zeK_>;`mA*ui`Q0U>34>OahEf{*kILA zH~T-s!HfSHK6vu~P@DfI{Rxz{>}GfKd9{I_{UQ*`G{0o@6sR5hxe&n{S%t~ zSift!octxzT;<4*CUK%WF8)z`__V1~uf}%4>Bk;=rP-3EveAbx-)g8#|0AmZkMrbz z29{4hRR1%uv(%lxzw!8Cd$H(0CV!{z)BDl?E!$pZkLt1?(?j=e`=P$>kJrc8XRVL= z+oj68_BgJ{zO}BCy;wNqezZU4@7~W*dS0R?=I`RI^0Kdf>FxbceYEM)pC79|E^PJq z`nsOGLP@@G#Cxs!;co>ouO{7ny-`KnY*$Fwr?`bX`|h8v zEYe`^+p&4~mL2mhy}I`7*1KDg2|Ks$Xo|?wect5EbKyKs*CtmTqt&iDMOu4`nEIHR zG?=qPK1}cY!m-7ByRBohBbw|EPN(ez8>6>VE8{S($Gq2VYFvyM23i$mM&l zu06V$zh=p->*t#){=EHgVin`RzWE+#bgy#U57S4o^5NG@y!-inB;_i{T-q|9QUCC2U$xu8(|qF& zy?Y$Ff7*-hi=&pDfWjlfW;r2h$pibqhzm5BI{cksaTlvBNt*%<{$LELdv)E{g zE&QPW@Y=?e*>bWM>$F~cuDBik;o0VoN$+_1LodDG8tu=WX%;6qNuSfRSZ&_tKhE2C z?cX}jo_7UXk8TUoA9VK%)_8ZvcAQ*$OT9T?ZQkE4yI22M z!T`GGfaO2KK{3#o7F*N*2>BnFFJou@NB6_|WBFb2BC0=*iXS~M|2od*qgv~_KZ<)l zEbrfw`mopd@IDzU`OYn;ufd-Hz>=?lm2`6qMb%UZKD%pdFj z@m~AS&{R@?&>pl<_=l$aTmKL1zqMR7>F@o=@I!Q2#j(fV>VDL&$rk^mH}T_TpUZX% z_kKkG6O;Y^jc@tlS5jN##6BFUlfT~I{&u;T)s%l)+xG0)wtMr3w+ALLYyab8`_Ir+ z2D<(^wf~3K`#0M^7(dAG*izqcpTo}VR*lx~zl&<57yoFB^>+Eey6ng7M@<#}u^)ST zpLeX=npvZnx$=kn+d7kfhpy?pD>lAv>}p-^(2ZLc@ZVbh!y@}1$MOFREKC1R z+W%n6Kdm1xKkz>+Z{5f7C+SDzBl(sn_e1-IGviD?s%>8LCuRPTfBF|}%-8?WJ|g&| z{m88)ai+WXe+XOq$iAa@<-xpLPkZKxO%C3^cl(D=S<{Lc)c;6>N*U1RnBad`|1*5> zTF>(*|HtPKhY!cg+3EcWy1GyO<$nf|J<+Rv96s{PZ25<@*N5fwuh&>F{$YN&@JIcT zDA)Z>?@QKmz1+p#$F{kgJ8RdPeHyWT(VaCGzv`xEF@i1{W#IvZ{(pvp_WQH?e`ue7 zoA`nGao^e{HN`*FKWLqQ?0f#rEg$^b_QckfF3ge&ecrWp zt(~0TK9gEzx82^lzU$w9U70Z1@xr@YRfg&R_&`NiVf{h7{mJKlXqwzPj%1nmx`BmtX(txkpv@ z&Bhxs6>7d-+Sb+eA~%m~u)jV3N4)qS$I|}{EK@)k;K8JSRzGfjuz$F|^&kHq{~zA$ zhnYXrKXRM-k^P9(zWpC3hkR7;*<-pez9Rn!|JsV#0hj+N-Kp4p*lOPPNB&zguf^Z8 z^1NvOP~qaT%QfapUzG_l6xx5ttN$ao`X6WP@7Vf-Mql>##mm)+SNMPH{&DuB_|f~q zHI^T(UPpaQ@7(hHedGIw_eHXHvA6%>UU0J_`N%92JE<$T4_><`_hW*MZ*iXK>w}M1 z2eU2OB{z3jgaZ4c?|%fN|8ZXa&%pBg@522LcFx~&|INma(MPP}kIUbj|DfyNrRo0} zHh-JB`o|${UG2wvuOII3i4*@2-;lij>K?-tnercIMaj3{y0-W6PMtrd#resP+&-Mn z*3LI?OW3z@2J_ng49zp_AI!PGLH|wq@8W-_;`si~S@&gr!#?pJuOF_q{?E{7$9S=n z`@Uete2yge1;0+_#d_(5Bq=o&(LAoFZd_w zqua_=8D&ywjq78wqW&#i^y8ehKb;?wKQbTqXS4OAde@)8<(?m^P9GMZ7+q0) z_?Ftthy4N>LO<%mukEoloBnv_A5VkYwBU8O9hbinh`MoNGef;V{I>On~r#O zc|TZxz+Ui9jr))258uBv{#g9s`NRB9-DR;!-;dlA|M0%|Yb^I+pUpQGyRV7!OwY|t znKidXJ9qtQ{%>vb1-hPwHQ1$J|6~1#pYydI@7u6NDL z@(pGu*L;=L4{W;i?bIvrpQ+n-Hfbj3PrV?g!F2R_%bxJ%Kk7X`rv7JWt8q?#m@nYF z;&z6e>Yj_G+WGvi%cWdC-p;y^DRt)B$>mwWnbVD(^H&^AFE+mEuTvPaCdg_fLtf`Y zdyyZrbffb5DzcBrDYQFW(%<8_?)0LJTYam=x);2xbN@DP`)bd`+=5wU6PMSny8ZUn z)Q4@SF25D1U6k?X#dn6}quIqa7o!j6+gv{P@hFd>v&v*&l{MWWg>GHvl}}u9+qg(G zJySqKb&3G5zQB*-BX)wA|h6Tl^g*yWfgOUV3*(r|ov&rLP*g z>({Jfva)P@#<7Is)zN|j6FBwu=s(JqW%l|Q`@X&OwmowZ|3RFTCl2 z7bl(aP4>UEb>`a3epm8KW_*>k{i+>pJ#+a+@84>>`c!u{oM8NW??1!Awe}x8_y169 ze>?xTcunk&tG|8!n0#n|EBJAeeb21#hu20Qnx}DnkMnZ9K1K_6>wty{nP=wr_Cw@Y=EFPV6+j{H*VaEGBT{oj%OAAHaMP_IAo{)ff#NBwV1 zAM;Dx`tkmo%a7{E{oO$|Mi-ZV+_A0s(fZCk^2;J#Rhb{`elPyA{99DtTK@L;?`*u6 z$N4UK74TY4xBEbO*u{J7SJyQ#eAoYB&Hj&b_iu)}L;sZjPWj{cTe-sf@O`$L;*W9n zABFBeV&7aR_K|I6?ZUGU?{go2X#8+~N7*jE&8uGZKa5nrVyYYOFv;3dr{q6_@D`?M zZU3zqQhQG`)Lp5&E&svkKf@2@@I(I@ekiR!n%}dZ-$wJ{TKgma8Tj_6YWH{F=dsT| z^6}hW{jNIIAOCJ2G<^M!?(%)!8r9!Uf08ck(>?j&)$0TIv_JCgO%*?6 z&v5%kw8K>!^EJ1A6d%6zuGjo_ooZIp^3E1HjVoa<*ZgOA$ocQwho|S}m-6lY_JIGK zf5ZI`zWzVd`@8EOJd+o#J21K8mwZS4#^vq%RDOKQ=gZW7^iwD4V|Ztc{fDyrj_vWA zznP%r2d3`|e&xCTwQsG1ynp>c&}}|G`)}}n z6W#oGa!vJbl?r+1ABvCfcSZFd(dYkh`JsDj>D>5$3x8Z6+H+?9lihpiPkr{n*HZIV z{FC2vX=`l43-Rw}x~%;j_1kowoAuT&j=Ot~@wfa(&@@Bfe+HJs59xmd(~s4Q)+jIe z;q^HDL;stJ=EwFk)!j}$YV~gBqx~IrrrSQKF7N(h{OI**>H7jP6}cw!JO8PgNlE-j z+m&;xws+lrw#J2#w<|hxzH%I}H?)6nW&RJ<^?!s{e|zwQ_qW%dh`+1t)wJ+`&$MaUy$6Qk1z5C<2-K&4hci(h2?t)x_ulto z|KY#lhZl#*S%W!Dm(E?bW1r?0?;lUEm~5RFz<*6P{vSv5 ze+HJ+_J17hr5|fD?D#iapZ?8!^5dww+xI`rc>TRj<3B@dyZqt9k5=pnN&e5!(iY3~ z*=xr~@1qZ&|AH_KyuGs&<`}_~}_O~ZLod34;$IRcM z71D?2%hhT8;6DCO{)2h@KcOGXW3Rp~xzAE#f6c}?f90(jms;+_xAsjfTzUV;@#yJC z>SQ)u-O9Fm+lNm-tls(RS?F)&)SJNkSo-}Bb^AZUAOABvc;?-IgYi!|Z~L3@{|ww! zmrmL9tM{$vDXN$j-y~))xcKip|0Cl2Cr7&O{&Cd0Mt1vdsXKMrmmgK^u`@0C*rN3F zfZf;s49#ooA54q?;Cuc?|AX*1k3S}Vi~f+__K%?^?PLDYPCL2yKgu7r?OquhQRDI> zo4wh_bM24rN75Dbao0?%PkaAoV5n&K{ovMIz2>Fbxyyn78Cn~2v)6W?{&p#SJHuc7 zAHn|_STz1KG@1Ws{>PF2@O@9z`t}-g{qC*nZ|te5{;2-8U%h{;=bzAr^E)s7vB;OL z=njm0^L2JT*R6{3gLZnq^P8Xj$Te$YiSnDQ-d_4;@5h5zY@99Fe{RXxC;ZXBXX}v* zd*mPP7pT}AwbQ$EZAm}xi(h@aSKT?7n{WH}$c2}`V#KFUI-Z}JZ+qvHj*s%TrtFno z$NEk&p4qhN?^DRqa|`7Omh8=rM&b3bF&7W>RgTe~exwW~ApO7{BQ|EuhOMZtZ`-uCa?E_=$Kb>QnhRHrrnaCw|j z`{6i+*@wiUwytZee6(XruA4vat?cdBF5kQHVtdr1Xjwhps88pvy?S@uZ`w9j- z=SjqAq?9t`x7%nxs+V{zHc$3Lf4k{>ksnQed~!2^o zB85%cckG^Vn7QL^!mNkijtcK;Nb(e!XxSHW&r)Q=}WWrsWrP_N&WC`=Bg}Bn^|?U zS7Zm?Hk+MVUoX4$^v#=h?i?*Fnf7^$;I5;J0UUppIPka%=5x zmaOOhQSP<>+Sk2)&$I1se)+5Ivwhe8z4Mc-f0|wJRJ)RPyH$lDb?@3M;ryApTRz@B zuGslDPEzQhYLK(rhfSSgt_n@9&P*Ii4W?iBbN!fqSlvfX<-@bR4?nNXG>iJzXS!ED z;8Ne(g;`E+m$$Ape*5j)e}?=^`PVd~P6uxNob#@wNJ4FbasuPGI>C=|ix*_w)4u%2 z@R9U?2JRQ)R=M+bKk~K}588FXBzkX_=c9{nOLLRB(=TtiJvS;kd&ZU<`g)1e{&`nk zx|*7?go`o$@I9H2{M}pES|6?xxllS^_(ylduCC)A7iw&m-p`J@wz}Fnx9z;&r7t)1 z^4G6hmL#TKouN9-&B8^!i{ad?ORw%d%J%Ng?$CStbQ*Ve=E6zZ9cry-f}KtDR;vUZ zRoYT??A3bdm2QlUB8;2YSGXUYC+aH~FZ?6s2ZNBoR=UcdE~_YxyHaERaQo#)AH_R# zN@|_nyIfGY`edH0mTh*}_U)4nd)4}ZJo;tdj>h#V+vn=GtQBc=(0)I z)5Tf$6t9<7pD_8W?sDPF>$B0}SM}e;sBZ0>yX=N@VP$n?!4nm+YZp$NtaCcnQ?Y<6 zysUhhc(|DPwe4o%+a{mRF3&7y+4enbyMeX4?cSq%PE0y%W2CLJ(W6@>Z_T<@2O1dH zuleD9v`%)@g|GSo73oK;#O6NAHM31WT>k4^_sYz&IK_+4CPgf|T4KHWc80>$EjLq} zG8XUKFzM0NCv72--fj;V%>6&C?=sC*j{3+p_2J&?;0rb8vMu+Nu6_9{AF$_=2}h~- z)ypO)OV@hcy63H{W$PZ4dijmquMDwIU#DHVbJ9}q0;{jG%e5NERW~xK_$ID=U3G7D zW&ZM)Rdy2-OIK#?n)>(7YSy*AZSzw_u9r`rZoJDmKR2Ievaj}R%dV|EJ)UphxoxhTbcb56YFkLg6W3A& zhRoM;!XN$Hb4q0WkH|@VWNTgaW4ZtIun)%;uF1I7H#7N>t)BGGHELJ3Nbd&u`^%5@ zYv1WC-E;ZLj_rN#IM>c|H1X?Vm=)hzQ$4y@&Q*& z-t$Mq#kwbFRWQa_1iXw|r%m`DORqdxMMp6AuBWxXqIWbRYivSHtJ zpMAS7tY7zb(blVP*KE~Lzx?j`<)=HkZvB$XRZE#?-s>Rj@nL=E*Wb3qffx2zuFtNj zZ!>vv^^a%ml8j$%OVb~|Rg2!8&GFT@=-Ih@876bBykDR8R@uA1x;lOLu5H^QeJ&sA zVVt}C<*vTa?0DU zy|=M_xVJiP%ZF<-O7{!i&DdqPa8>jRDWkj55!=?UJ)fL!6BRZ?KVMhp?BCChS$gZ% zu069a=c6)1>dl*Rugn7?Grvs_|GqugIX^cuJzw|g)uTs`T)chzAnP8EPiIX&pB51~ zDrJ1yWa5#obFY?wRzIH?_z@m)Wsfa*_4B>`i{oN4Zyjr2pILs(ZsU?yVv{P(!>p#w zT;Z5som`o#VHgv2*HPrd2{VtbI{_@;?LwskJ4m>=-r-tOdwbL(>b?mKY^4FAKp-0bdk?Juk7xqlGJt7dJQ*>5p1N--@{~6d$)gM&& z&+uUS{B8DsgufrHm#)v4-&QB_chNuNisncA`)s%$+ZuzeUT~XnpS`4>|JDur_AR`^ z-XHGmU*x$o%TDp4-rGOf`aKPk19!%T$qF6%cTSe;pXC92_5TbHrr!VHr~gBJ{qgu8 z`u%S?f6M&w`#5#}k@YvrAH46}pOO4Q{IK~vgGti-x7b5|6h7W9D}6|gZO_NL#ub^@ z_bE5~dWs_g=Pv%PQ>|#hI;k#I`NODZOu!5Ec?%Jc-PzMZKkjHx*c}< zD0^?>is%fhtKa&z^{>8}X|?Ct%jK`ktD;_s@vgWXdB@kZ>)tKtDIGC~Viq|_Fj2!Gh5rW z#Q3~KstPl2;?yS|L7H-zkwO+zC6`QmqL}RBz-?_UAQ&Qaq=8W-fNg)r9@9tm?61TB z$$ZRfT>B&bh*ivLw&e?cbbIZ)Xu8%atye7~|I1!K?&#N+QIg72lXo^h@!_+#L+4aiSnP1*A zd-tY-rsqpuXV26%>9tb#)ZePz+IQ-bS@@25rU@QwLasWKDg%$IOqz7SrQ@uk7H8r{ z$D9>G5y=gl77Q2I_ILa-e&o;oT0BnfV}ASB^V}6pN1|uH%6NWcTij{9N8^ zGgsceb?XKE5lm;tit;Xwm8R_BDTE3m@*S?wM;K?Zx7m00 zKX*lS%tzg?ZIf5MzJ9HWZF2pZyEk7{iT2!@w|&js+b^$uH;#U3Wi~s%a7EUxo)gom zN_&K3@1ErmwCQ5(o9L}9;_;+hQdLlL2J7!5D_DBUPdwhskXzAzXrIQ1x|MpJkM8rn zz9)8V;nod%XM22<>tB&EYe`tu(%yB;m&Kk=TzUJ}-Q(FSUT(h@)!KU4MQQiO-CK8W zuTQ4X8m0{#cJ-oR=Hi*_TS81 zb31dpK$f^v(w(hu#EcJ@>u~2Qc^r_xtqz$?-2%P_;`VqxWPtUNzNR zcirnhLyJ_zmm{0^@1B>*U?+HC|AX1{Klrb#fAGxK{~N=ukEwg&H^$CCrm0i1m(5@L z?h>i#9sLKp^*ajc&Zi%r_)0la8`M2}_5Q)Ddum(%GjL|lmjB`2x56e;>5c&ZuKf>Q z&;Ox%{*UndZ^}Phe;e0?|D9T+`EY$hyrfO`NAvI>^{f8~ept71bxcH!>yP~9KfJ@Q zm)3X5-;}zyan+rd;_`fdT$fjPA2y#gZT7cp-IyAWZMkle>%Y8Mx`6-kU*Ufo_Wv1J zO6)&4d*}C<%Sl{#Zt^I zD6{_I3Oh-=c4b+yOZNQtw?6NedVJ(_LtSwF!IkzOeByto$G<)N;rq8mKc@Z;t8hOY zFS}3okNwAe%@5v6Ke*mys+;V8$xi-{&D9#i)HRv&h1+H?&bk#{(f0Xxobu$Obz+-# zUU7Z8yiNRo@Y!4$@j9j61MCm@|A@^6AKx!z6S?lk{bN-(w{7v}_J5l%Trur;@HJEZjS)wAd#B%8_w={w6oZ=w zgl-(3FKzI#h;{loA#l@GQ5GqC*OzqH0idqGB2 zTla$a$RE{S5v6nEJvVOM_fhm}U-LtMz87ZURZBK|nf%q#TvvB>bo&KL8 z)xN7v=hD{vX8BEQ(;xLJKUmhB_^_AXZU3dUHc`K#v*K^hIyXB#zy6Wt+DDgi*OuJ; zs;M&T(k<_)>GQHX(-XMoZ`%GxMEKi`zdd^xfBXC~`N93DSNQNg(ZB2L1YX6n*qKJX zx+i?W#@%l71K+UIN1|e{Sh?QYm|AStxN5HU(R(VFCx>3XtKKK;;nSG+rsVhR0RH*^ zxN0&#et+}x8$Y@o_U^p2 zHM)AQb>Q~xSH8RspLXcmt!(X%&J(v_F|b*%MbY9Ob+>&xAsHx;cNTEFMPS1 z?!D{dyoJ>(vld5u)SbH|YTsVjJ-4PUl$)`uXG3vjW`k#T7`R}s* zNw)0Y+CE%=^YzhL`9r_#-@1Ni5Bj0{vFdinhjq=5+^_BtUU~bKSrM<+|vFtrH4=QOg^~WJBjgg_@Voq_w;`lfAoL2|5$wc_Kz9&?0)z^-p}^O z^Ml=L)4A)G@=bm8pMf#^R(|Im;}s{DzcP%u8SkFF=B1grZ)!Emx9Qq~O^+lM%6b~A zroYYq9Z{3^cYA%t{Eqsq>Tg-sX3kUovHPRbdxza0^Hwg&sLF3QnfagLq40#TcbGHs3j+4H)tKUiXWS9oh-c@cT93m;iGL^ zH|^f7`JX|!O+3H+o2>pD1)jfs_WAE`#ed_Q{J7`-=Jsxres1sI&NcGa_p$%+tc^b` zrQcb1{XxILhU&#jd6TX^$+Wt=`n{Rd+6}r4vq~pP9$t5VfBGNh$LED=j6Ys)`KMfA z^|j=@=nv(ic5)x5w|!;Xwsm(jZ~dWLPun}Tc%R>N&)IhFfz4O9+_s*-`EK;tT!ZdY z^CT6^dmC(Te>?tn<3Edk_vE*z_x@+de1FS4`cjSi$Mi=mc2e6uvMpWkLMpv=OLOi+ zd7g}W{1OhQ}{9Q!}miUD#9H%U(d1ee&ju0GVY$N`_Zy(0k5mJ&Cjg; z$bIa3RqWiiXXdBo3*K|K*`)7|ni*`ALUmfIfhdblaJ z=(~?+P5z_Rt81+uy}V>SH!bOtcYf~j=tzdBzwQ4S9xRaGa{Q0*{zv|u|8(n4-_yMC zNBwcW*dLRR<%eqH1Fr8;UGU@3jUV2}rr(piTAH%$@*l_a%5=8HVyhqgXW)GmCVogQ zL$BmWU&xM4^WI&0!2Dz`|BC5d zek329lx4->^aYS_UqTTXR>Vat*$M6GCSLJ`{o9w zb4!0$*Ikm@{>J{{{U7SB&$~=2?bGDBYf^Xq__(AZrXtxjUcmVy(|hl$TUMski>+O8 zZOKKKs&AW1W*N?L{C4}N`ZUil?Tm*EKTjX97qwHV(Ym_i`aj7ZpO4k^m%c3deO4(q z|G+=Z>ov-2yqCPP5Wf{NYkB+nFR#B}*88?{=jF2}H}6n3KCFLw3v=6!dEPUlGCeb8 zHd_mMTkqb|_r_m(?%kOi7*r+|9i7>r2-$1^*-|)*^WHs2or*h)dp_+vp)mEuZ4bT$ zA7leJls#xDpJ)AJ<45gBz4vCeIg{Nc9!E1XxquIGRA zCwzU>5B2b?ZySE=tzK0T#Qo!s{mQJeC(NRM-_5pt;-Y=jq-aTz0xLgLjmyXThyF7N z+^*RDaDUI9&wZ&C0&{PGwT{&Zm>IXQpPReP*yaFZUy?hN8dQI@tAU>lOBUz*9Y^r?|*yNSpMzW zXI6jEevj^>Ua8yP(*G{lQ~S`rvtDSAZt25Xq2JN_KBhO9#&cv_`Cgm)XyseK?Te#4 z>mSIopRG;JEK=HGrL{;RasO4Ny$nzGC*QyIpW$)-8{OXy72Mxu{he33R^H*q`VaSV ztKxaHqQ19(IWLmQE#CRZc=ZqG*ezE~eB&h7tk-@2g||Ft3y({9Sr->v=oto$Z52B^(mE*-6MW33Fwt2GREGePq*tgL^R{^S04 za{dHVR>dM=`*G;r%%X%GLwsl%`WYo1sO5Z-8=T23hV!&p$`w?&YkzZ#)cP+cG z%Ch>ldv)yT6Tq88is+%;&23eDojlN4eRL<(Yp> zKO)EXv8=b?dz5{bjp5o?Rs0<#t7i%>p7z}L+LzU{eS;1?%Xl)&GEY}uapt0li^4r_ zxlUl3w*S%I>Z4WXmOru=&APR|bC2ViyWaJyU(Zvxs=u{1KIDpesPO$)R=r;?$Hm9_ zoZ`7I9(>m1gqN>M`||~S?MLgy_No1N@AqL{@8kLoIkS(`x*x7US=hZn!^OyJ8u zBxm?BzQM+Q$&bSa{;6F5Bmd|u+dErlr;qlnTjzQozNd6C|IzuWl}lb%xt^W3eA%6K zkM?J5+SOFs^J0@mu>iK;o-;7 zrM>$)SH9XdT|7=^x0#&TN88q;?`C{Ft0FbYt4;bfuid9B%#X7mXE>Stv43P+d}!CU z+3SDghh8a>t#{kM?W##`x!b;7m%h%G585>O+@>ofd;N|@=$E{HZF%TURP0TE)}wn5 zxtkPmDmyI@uStEJFJ9yFQEvHTez}VIN9;^K=54p!ecdMb(b@dqkLn#?S8rO?_B!<1 z*V(hr`UmXzbW!-+?wzxORxuVaFfhNW@A=QL{rX$&kKex~{++!)g}*aS=aOlB=ziA9wXbiLPTjZkks-^ztjzPPymhu+xZ~Ha@^s<^ zX8&(~{~4M({#~ixXy0g`ncw!GVe|VQlYE{Z-ru(U2v-rH~*IMBmcMVA3sN2+IqixzUWq4<&ckM zD;H%%<+p8Fx%7gK_2LSDx2D4`mmYQj&X)QXh|ZZSj78^Y*u#kM>;f-mparDR7Z~N=Z z^_D)Wu6A6T^=()6kLB^(FX_Bn*CRHqs-equ$E4#vvraJEfAa!0{p@e_d;MMTCI9B< zqqF?ma_TeBH&?wsu)bUH)jo*}dpvHB_Sx&NZqf(NT>d6%tDx_Fi; z?p)cPMfW@y>*I zzfgwxq?`AT7H3vXtJ*g+wdmUQ3I4f-PkxpQdJ9__uy6f$WB)ewWA|_G{?_*K`&-A~ zuGv!d9d$Z?-2XGQ6xz71$gpCY{jgRdy2Ag^FZ1Rt(ze#FmlpP2J9qE2hUfjHfdaowfA&HkO=XYx_JzfS5$-t|NC z}OYNA~O@kwy%kdzFXt=@ya!^>EY4J{~20leX>5bukW&FtOc|B zH`Bj+|1)IT-_VZuad`c|v->nJ{?T6br{ss_qx?pD0X@@x?icekqW3dYtb6Xi=gPy& zN!Q-$P4>E`wCl32>V~UpozCXCtaxcsG=)K4ze4=jKE)q}kM+4K{xcj3Kkof8ys5@_ zLB;;VR|S@S}f;r_?^w{L%YRJ{JC`~$QK==(?c&M(#LZ_a+O{itsF zWB!I3L-96~=-hJukIOrvj~T_CuY5FPt=r}mnIF!7WItAJRwjI>vheKsJDjr~N!x14 z+Ay7eV-M-2gDxVLv;TPf$oZSaM{l{e*u;P2epui6Pq1Rr_oxrs+8>?H+@ttt_g?A1 zecsWsRhK^9x*WXas*Ua9S5b8fxwMa$CX1ANmM5zoopV55?ML}hsr4;;oEQFBet7d- z>$nf+HZQGcKK!de&|hu#>Wt}~^-J$@q<6l}OWC@+OS)mZfT!?Do~(~s*dkOJeno$~ z`FHz&h79>18s^7qv%mHIUHZp)@sG)m%62aNk$kw^e4hM=XVV}0O5Jz)&^Kk4Uw2~i z!WSP7r{~HTR(==S!LM_@e0tDH#qeHbhRfIY=hxqIK2pDty?OtGc~EEV*colF$NR7Lk^}_Bxf89qJGdLS^Hyzh9GuyPOrh)mj{_WY{-v3=(|6tL5ndomF ze^>nRUG!u1qwEU%!}C~uoe%#LzgVNV^zg&;+`5l5r%ibAOLptlMJMvU>iYeAR>rTg z-Tbpoo$s2mZxRhUf16c*qtpJuJl{uidH*w{e)+NIKZErChr*K&#mP^O%a^s$ zYfJmsd}D6ymd%AKv$`|amtEZZW|Q`blE2e97{k9w|K0bWA=Cbcdi*h4|Hu2^Y=1OY z`H;P!X+J~7?1#%1)~>5acm0?)xA0-S=-DOrvck?U&7QX1tm&DozvIQyXz3N#EEV@% zPKF{%3Ga;o=)PPn|6@O8o`0q}mqp z%}o33cWs(M%+cJe>*j78kBv7naEl*Z-#Ouj(DR3&%QlXk{BZ7bSZv+^mrdFl-ZpU!1a&i$J~$156gD!)A*78I9}|J z?MJJ~J1<{8@@J`-9ra<|UeVr167Sw5UNZr8hq_L^ve(U9JL!dMiGc2bNrp0o4Mm@` zzHblzoE1L#d~I!UvFX#hSDzlaZY~}kZhW$^xUkUX+^5r~XEvQyJu}JJ_1OzE1THy+4J^qAQX()V6;tzGb#Ed#Sg2(;w|p zyXms$CtQBX{Zt}p?-k`;3_rFta6wu#+P3 zX}Za@s#(rO%>kR#EHc;m;trazx+eq^7(N51tB_tqV_ z(=2=Z#@4k`?~Vs=5f|8e)MDH2?sJhcJFRC3@Z>tjUoFk``yF+4{d!a1yEg#YK0&KK*@G+TMBZJnwy6lBxISXOyQ3!>d2pAB!K=ADMDN z`A6qR_GABSx(`*kSh2Szf1KaF$NACJ59it$9Qk@5eq8q|{6E8uUmOKirEe*2iD^Bl~Fg?2KvB znSVSVc1!;-@hytZ2yx8$wY2wk^r00SrljyY$j$t%RCi5YvOe)Y!!7ZL*WcRzwtsDv z*Z490Vd=NEwGVT>BQNIII4@cM?R&fFe9r8!S$(}LkLs>{x@_sgxAozd)0j{0Zq%20 zcIe%?U((ep9BlqGJeEJ^FI!XfWB*6{$IFkr_2MmmWPeP)L(cHW?8j%5c49>v(FFN4{5yGhi>t=O~Q^VzPhD zil`_){7vZ9tZv&QA2I`DJm>m7`X0EG;pcjn9~(a!AC43GXx?aJ`H}h9F80PhS&DLo zAM&~%?XC9yu)gc)mb%ytAJyB}m~z)D$BLaky8qkKxfdG?Hq1C6!EpM+{JuTW5A209 z%T9;d@YxMrqw1~RP%kSLw zf4c3W(qzR6Ov^StIxkq!9eJh3^O0?S_(%SZuhln8ZNra9-CJ3`D!XdlQkzAYUuE4x z_sx3r>pw%ltiWfU^`$o3+Z?4k5B8)mFZ?+F=>E?C41y-}xijRfezZQW7k?qgb~)Vb zgIn{Xda)nP@fU3*7b`7NoA}UHJ^YHkPL1W#V-t^xlu6l$m8}*DRCc)+wSl4ku)XL% zwF=|Ia+)9FTj~@lt{=AJ|9Evz*gW$O>lz=eEqDKLy=QIi?b5B=yT0(+{krY-QAF&F zcl6OUQE^Ln$`2?N?r2E<5q<2Q?g#x&lX{5?qN+ua1AcPvP2D>1p?iS7iMdbaNu1V? z?H{d=TZy;sF?{Ub5oi7}zikhH=HIyw_VZUvKRWB%?u8ZJez%{_wLdInx4OQ3*^jyt zM}*`zWqjLawn)Y0li3OeMjOMA*^ly@O5}wr!Vm5jsL6k*&r@;hn63G-domx6cbmRt zcKkTCN3Njh^3f{0$@S^We{@|Fo#oEiFW_{K;ND(?HnN*~+h-cl2}zM|OYW8Z|PM7hb2_S$>y-@5rD-%R0{yEayJYgz9u@hvUf zr1)$W!?PO8NA&^~_Hmd01U~f54*hVw<4g69_g`wu5C7_Y*1hJX_(h9~e3P}|zO!xz zUd~&j(6KHjZ|yrlZ>>{;b0e68kKVKY*xvugb;Xav$LrKT>i5>fuZeN5TwSsH=sv}( zd)y!C27QuPxUK7Tz_qQ_tM~eSdt@D5T_IFGd!>4>?t?x}p<6238KkVDRBv`X3(KXKB86apX^uH5Z21nFkh`b&W9P@9{|v7CKbEatk|`D^cS-d>LrealU;0gd z>@$~VMCBCTjEH(^C-LE1?~>?MKR0`dcE!Z{+kW{zJzPPy{fPhVk_jYo$$9RqP9$!GR->HB9bBI?}YuJsgv_>jF+6lg{XZfeK zZuP@l{v&5sRVc^wJ(8U4Yjt<&t(Q`JrkHtxR{N7y3sjItHqGY}B59`P+ANkwY#~lysdUfiTv~Jw7 zYcJpGuYISmJK{;x%e1~n54;#(&)>%Ww)o@nZ_oZN_@`KRdH)u<%}cZIi`l8iW{AgW zT`AFJ_xW&aqIN~{Av^gGW}ULr+Lt)8PMvkDBLA?IX|t8KZ;`H0^vu*L`3v~|GaSGF zA)xx<@%;8r zKe|02<&rUCX_4!AAz~yCEzD%F|y?)WM&mqO>=`gZLZTVHnd>dGnJ`Gwp|E_kqX zJaSgv!S=l0PU*+{N7Fml|u#m95GI^I5-_VQsP+X>}Pqa{pVpC9e-yw%?D<$UY*{|o|q@~b~wcE0lL+7|!z z?QuFFA6(ozyZ2w+`lS_Kv5tG}m%rLPQ~vywT$301YWEIB&M;m?{f0jO63_Ub|10h-*@$k^zlf26I;L6hky09Y`fWnsznC^TR_7K9M&`a(oE!E` zdWD?z4871ig<0Ayo*~R@J{~{H-w`jo_2!@8kLt(n>HTPYR6W}}Te@HH$L%Axu6KRC z`DV=|mkpa|M_hkzD(mI#E$~!VciziIZAKT5t=_SfA+o-Ezew5#7J1Hhc8Wh9J%5gYqP9suT899{X_m})%R9Y?YnAU&0=3p55L9wZ+@w5>!}AZ zktemIV-h+SPApi(c7(34||yRcvo9y;wbkF;(#PkWU(bubUc2muV%K56 z+W5Hr`}bFKYxGaeO}RYVBRNo}Kx-Ng!}lK>KN|jF{LjF(;E%({6+dGCuKM-iN8UcI z>*5FEZ=XALJjPBS`r^ZH*SOsUa|6G8+kZqr>5fjIN&=I7`+Si)EBlEjvt=+cw zqx127CO;;wyZN%LN_82i_nW=X@LFz3t)K(1Js!CBe0yO>1^ zI%ZC2u+s0A7kculrOdNUtLIw!>xI#) zlO}~HW-XajoyGB@HP7uR1M>^{8?(Q?`n&Ky=3>W9C^zw8e&?_Jwi@q; z5~aM=hj+Pmm&`2D7rS{heeJE-7rX9mJv!%E>eh`rn_^U^U7r?kR+!_=h8^suq^_;6 zUjOP=-`169zFSROTVK8Mm3a_H%l7hLtyi{BpB~ha)TVD%-MM6%qfFp2+1aQ2EWWul z$S3~0Q~x2r{#N!sF7?O%8QT9d2-j!%i@l69_*j3~zco(whv~!UxAKRjqRnGP4PE2e^?xjn|?p-@4!1>4YKLabz-{m!` zzxn>SfX>;=n5T1b?SF>U_g(SACHdWTh9Bn46Z!D>+|J5Je|dg5MMviRi7ial+rRAP z=X*cc58i5h+dKD(r|Gd)mcTt?=C{r+TgG^@?pXY``N!*TsDEqxu>P(2Z;RJe=I#4* zD(sFQ`IR?$#gFF0v;4cZlx_T2*1Y0Jan!aw7ru5^u37H6<*nc2Qxjdv4Psj6X*=L~yj#j=?c2FB=GCpG2exhX(0zO9 z*{@HJ0`qfa44gJ`EaeAFuD+C;wyTNB+ab70IrF zTVC8h>({%v%g?Jf)F`U{o6mZEs&U4OZEv&r7OpZ&Wc^dIlf1I^J%47L#w8(nu^-_vAHq9y^GkfAICI`5Ua_%!q#Jc$Q%7Fz zzDs&K?{wcO^@TEgyf631`D6Hz^Bwa{f2@4$>;60X*nUBq_(%D&75hUY5;y(3{!zcj zwEAuFk@C0sNz=Z4I~;svtM<*O+c)=dD2wxp?kPAT7{dID|F-x)F4o`P|L%U5XU*H6 zabF~(PHpogVJ8x6f8d||w9MS;fxc|#&X&d|y<8f<{Z%=0$vv02 z5Ci=gn;7&D{b%^0q5szML;W}VAO9JCb63b8-DTST=H+jnis^^$>Hb*xuzNq(i>SVh z^=m7<13tW4y8QIwifQjI#YQ`|J$rm8PD-l9s^((n!ix{O*tSK_b$`1+M*L0f@0dT? zf7|Rc>>2-E-oN?U?&THbkIo}ksbn#J z$$uOFyYAnm_-)Hu_ow@_{i*q=*ZPQ;`&;-A^My5>Kg{zN{|I*a@Z^?xgN<$Wk{dUp zUtO;XonvnGZME*S*2TB)WtCr&T=wr=S7^l2d4Jb4sQj@0&%ny^cX=Jlzmxm7#mCblfMF*n*M%hm)Ocxuf+GsPWPL1FIV_T__j$|yEymtdt6$()iBlk zm+RIRP0nrQ^E@4F&VTd&yRuI2-_iM7_z&mbDt=(Mdg+V#Vs*l=ZnbTG@YX;2qRD%q zOtbG*+ZtC_#0T~LXK=ZiFz?*2Tb~`Tmu8CxKlSI1S$cKp(Y;r<2rIC&ew_U1d9R(# zpU5B6KW={H`X_4t5&iD{0yfDXosUpz=fZcm2Q1_M4Z#x%k`r-${F}J@Lyj;#v1+XWR2e{K>kQS7W^J)vRrs^B-2P zT)H_{`iS_q;8*kHe&K`||sIo#$N{ zQx?TkUR$4A%(~dV#Gph0X@%3YR$kuE7RQ|+$eBSj>`N!js4VQDa zM04L4`Efpa&-JgTXUod_wsG?sU5j=4Rn~OsoW|R?4?jKbWxI5S@%{WQ&;N)>f1CYx zeI4(=Q~MeBng7`C@sa&-|Bdqp%=h!|V|}@+zV(Z(aKObqhV{!U{M{zW=7(R_dHa5k zb1`4%gM6+`v+GhVy-#ju+SMJqQpy|j?F*Cs&F|m-GyGj@pC;cRzg7Ls*$?eUrR=&t zO#XKK!}|6=@*l6eUD>0*Fnjv1XZ;VSUSD|Gt#i$*vfx*C(mSj5MJk$o_RaR4%X+nM zk$t)B*S@*6S&R?oGwidjas4~`cs z)XD7a`t3LEz1M!vTvKj)D~0W15umx*?-IavDnJw zb$^0?Y<=|mz4VXkp&Krl{1x`wdp-Z2+0?Zrv3lIOR)f5_--)>;}JL^9Ks9yNy^TYFZ zSdGvR?MJ!7P9ON+&i(M{_(T8W(X!!(=E;8){kQeOUAxmqcg2`hsjXZS5gYrX=;fF7 zvK8z7=Y966STapE{M?~?k(uAk8stPj=s)g1;(zn}sR#e_MP({Fkjhc{_p<$E$iR%|2F$~=0C%x@}u&iFRhLr ztiP#r{OGRwgSLFf4@=4Qu6(iWpYgO~zLBPLi+wk5zhdM5#5qxP+KgLECVQr5d{(Sn zHg&mz*nZGWEBTmlW``tfWZ>i6I-+HM=;m7UNRTbe*A9;^$e(=-e zz3_|MCe?SNEbD7W}IH6D}tHF*>WNcFW|O zaktOsmNQJcmvN@Zl%f3o*7CQ(ADh2j{JWq|weE7gTxqU;?2pYKlfy1=?H7L)rPiJJ zaIai;ue9T(Ke~$#CogEw3@M?dd8x`Eg=NWy7o!;|h z*@c?K^oRZ;*>!T8Kc=Ugn52Dv{@G-0l=g1PnwMe`va(kjEYE7k zUoFvfI~;LQKd+=V+H3c=D<*q`y>{)oyj9jbVAC|sH}Bu%=R9kBb?Mu+jG*l7)0N3P z+Y&abEXr6gNr5+BrsDUp`$B)*SNu5sNWS||@ZmRL`{Lvo%(ZAC)QmMyT zWY)Bu>tgR`P5F{oqm-6N5uZyq940|i`9UR9<+ z+|m!9k9Hjn-g?=7yUu$1jiwc`v$A>P{XZ>laxW~HUf8j?gZY^}OU3ge@q9Jq5AJu$ z@$Hpz@2+8fw4Sfx`k`C>6SJ#K?hF1158r&f#QR}pZGLfb(YC6&t!p0L`gHBlwM2=o zXOfRAMGx5C|90;0COfOTd;b}>sQ1)ozQ1K1d&$Q2WBT#1ebO7QZ?$DVd@HX+nie9~Y=JT1i8m!-U#D56Xzt#Sz{*CVk z|9_lcAKe$rxL&{6{>^`ekKFM;=6`s8=-WNf3va&e))%Yj`hI*?>z!NEyK>)Lj=Z|% zclG|xd)CuV`|sH?Nu&Ou(vCK_S=UZ`Zok}MRG)PI_Tz7p{x;a;f0TbD{^tCnUg<;o z)PAh~Px5FWA_GY)d zgQb^T@HO#c{B*z7CVl-6^<#AsAHTP)x8MB7_rcwIr;q0CrfhxMhnHu^N$uHk#l-t+ z(8W7_7XTQ;vg&E zX`jFUmh;j2jrNB@PTKe=RxJN!_rvs~XVV|2H}BCFZ~dZOTX*@m)!p@7x_aq*KbD9E zzOHJU$?hs_HXUMpw@;m<4+ng`AqZK&o6XSnqsK2%R&%pBd$MQ$(d-iWy z-n}KC?Z@|H@w_#T9~MVnDLv0wQGLv8a)tMisA=tM?;I7I{m7d8x5fH3U(fSqSRHLW zbu1wE<%Gkvtlwsv-fca>eo~6R(=?v<^|d(FOUpb;bIk*`Ud?$nNg_|F@879kyl&Sb zb>AsZlkRx5XIoovzLAY^j$n++wf8zk47KjR^Zwnn-=hCVMEmi&<-gtPE?a$Td}QAx zwY}ZO@S*ow_rrA}7i(mf@roY`%S~Sz?Y<-xj>w!U!Ly%pYjtsVz|c>8Z|wcY%Z=G3!OHgDZ?!lU9-v4hNpt9jR6 zzdfzfUHPqj&u@o`e=e4N(pmH{W|GTFBX=Wvz0FEHaqkJxr;jrZ1RQ`we3(*Eo9%U18~l8J9izU~m4;PLLF z%fhY;%>Nl4=G1u0vE{d(eEeQ&{b99h?H%V2Z+vvN=7-GdgNJ)Fegyw^{>{3z;DNRdjn)4F6@=F+lhnM=!OMM}=|l$|k);roAvoB7{-|1Pr6_3z$gf1v+n^~3Z|JA<0= z>pAbfYRhv~6huVNFqvz)z}G$Iy6N7$-(DY{x7pY(x4pU~{GY-{x%Eq5d@H^e=~!4P zGd+-v;o{%b`;+J23jZemWA%zZLVs7)3H_M<;Q5iaW`~Z4h{=ci)Itt9sqKKLZXTG^V{XfIa z-;ch(b^lm=;6DRX9rF+6h#$QllONe_+b91azqv$XQTE^XKr!=MTQ8c62S>g9Rogaw zac0%J-D_UmntONN)Wt8N{8q2d3g5P^%*eyRZt>r}_7A4r-**0wuz%c#u-4!4HHy<8 z-l}oiuz%kN_YP1|oqlYd@JH!PrZcZ+<@R^h$gj?h@~OJk`gd{S-_=X*#oW4gVEVN0 z0Y?jywPH80Ki?^#k|HByWF;fF_{O&dEXYgOH9#xaWmZWxP2-;2dqjw>Dn&!L=j#FW z3vyE!f1UmC{2xc*e+E|j9~*zG+^xGkkNNNPJ-+MyF!Udu{?GWbX+-IH&cl!6g)8)Z z`Xb-&iGS$dW^VPyUckon;nQtx(YIxty+3?C^t!61D9eiJ)vhY(RSX}_-*EoNIsbR^ ze};ozHieJQGuEAu=Sut0zp_HudD}<+UejFXxDW4}YqTHcS_kf&b=l1~`KX=F)snef zyxuW8x9{q9ySQ-Qj;p1L$t&+fMV?JxniX!gKra8I^*6=8EA9B}u7t~PEbsrvSFzjc z@A9ON)7$?rXMgBAc8RzC$gJmGHnD%_+!NaP;dF+$)U=t8B5cCNHZHmIqdeqd{(F}dLZ(s=I<_6zdc_5TP-fBXBNp{eMH|ABedf7kqD_kW0McCVx2Gia-aSO{l1(p@&d2KwujHEZ?8$)$M#XbBjrQi z##-LqS2vHZ)!o~B_vxBjsZ0O-S|n}7aOv;t{SP|gKLq>Vy8kWjYJJv!hKBgfZhIfE zZ#Ij+eRkoiiP`tWF75ID$feH{dpGl*!u*HrjbD4yjvu`*u<){JHm|$O-G{aE0lEHh zmrHq>onphIH}Aash~ek*Zw7xG?6dCw&`>{mdVb@7hE451peJ808}zFUvJno93pJ>${FO}jLUza{YO zzm@$>^zX|549Wi)ZgIum%zyl!;ZSb=!TU0^9y>O>e+cb8Hv7>_o0xf;JG||AVk+cA zKh(b4z4*sDd4c%dFRikEDHVz<7{tX+11@yqtNpxZ`b|1&i8)*al> zzDN7x^KbM1&R@TW^~3(7=DX6S-Ba55F}~R*sYZRx>p)MviHj z=3mjTH)d9y{HWeyBAb5P+)iT0j?Eu=mmluu%D(k(?jwKMJ1@(zbj)eC;)-Fol+F?4_Tm-oVVeG?zP4L&&QdaHivTIHir zc^g+m$G!~PE~WbP_0hFacV0~Ya5HOzf4T(wrTJU;{}J^5$L0E)_Yc>H>u=?Md)J+p z>Tk3Y{?YZ`^>Uu+>3Go}%16WJ30&RdtkCXu$)-T9CG$Lw$2e`nRF>n0zs=d`iZ+n$;KNPE3d zh5e!JamH8nm_J*$duffug+HO|U)(z1_kPRG2eS^9UeH;Wkej4w!m&=}`qV847=M

1XL9Y5Oj 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\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." From a57d569e66edbef1ab8efe96e4b5230e82d0a6ec Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 18:08:49 +0200 Subject: [PATCH 11/11] fontconfig: iconos de Nerd Font antes que Linux Libertine G MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los separadores redondeados de yazi (U+E0B4 y U+E0B6) salían como una "i" y una "T": fontconfig elegía Linux Libertine G / Biolinum G (de LibreOffice), que usan esa zona de uso privado para otras cosas. Se les quita la zona al escanearlas y Symbols Nerd Font pasa a ser la primera fuente de reserva de Fira Code. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m --- home/.config/fontconfig/fonts.conf | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 home/.config/fontconfig/fonts.conf 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 + + + +