URI: 
       Add selectool - croptool - Image cropping tool
  HTML git clone git://lumidify.org/croptool.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/croptool.git (encrypted, but very slow)
  HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/croptool.git (over tor)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit f5defee322437ec0c63a9d2872b7ef10936e9140
   DIR parent 022ad164b6516bbb4c5109e45c68248c17ff7115
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Tue, 14 May 2024 16:55:16 +0200
       
       Add selectool
       
       Diffstat:
         M .gitignore                          |       1 +
         M CHANGELOG                           |       6 ++++++
         M LICENSE                             |       2 +-
         M Makefile                            |      28 +++++++++++++++++++++-------
         M README                              |      12 +++++++++++-
         M TODO                                |       4 ++++
         A common.c                            |     332 +++++++++++++++++++++++++++++++
         A common.h                            |      83 +++++++++++++++++++++++++++++++
         M croptool.1                          |      14 +++++++++-----
         M croptool.c                          |     489 +++++++------------------------
         A selectool.1                         |     140 +++++++++++++++++++++++++++++++
         A selectool.c                         |     423 +++++++++++++++++++++++++++++++
       
       12 files changed, 1131 insertions(+), 403 deletions(-)
       ---
   DIR diff --git a/.gitignore b/.gitignore
       @@ -1,2 +1,3 @@
        croptool
        croptool_crop
       +selectool
   DIR diff --git a/CHANGELOG b/CHANGELOG
       @@ -1,3 +1,9 @@
       +1.2.1 -> 1.3.0-dev
       +* IMPORTANT: Change behavior of croptool so it only prints the
       +  cropping commands when exited by pressing 'q'
       +* Add selectool
       +* Fix compilation and linking on some systems
       +
        1.2.0 -> 1.2.1
        * Fix minor style issues
        * Fix minor bug in parsing of cropping rectangle in croptool_crop
   DIR diff --git a/LICENSE b/LICENSE
       @@ -1,4 +1,4 @@
       -Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
       +Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
        
        Permission to use, copy, modify, and/or distribute this software for any
        purpose with or without fee is hereby granted, provided that the above
   DIR diff --git a/Makefile b/Makefile
       @@ -1,15 +1,18 @@
        .POSIX:
        
        NAME = croptool
       -VERSION = 1.2.1
       +VERSION = 1.3.0-dev
        
        PREFIX = /usr/local
        MANPREFIX = ${PREFIX}/man
        
       -BIN = ${NAME} croptool_crop
       +BIN = ${NAME} croptool_crop selectool
        SRC = ${BIN:=.c}
        MAN1 = ${BIN:=.1}
       -MISCFILES = Makefile README CHANGELOG LICENSE TODO
       +MISCFILES = Makefile README CHANGELOG LICENSE TODO common.c common.h
       +
       +DEBUG = 0
       +SANITIZE = 0
        
        # Configuration options:
        
       @@ -20,14 +23,25 @@ DB_LDFLAGS = `pkg-config --libs xext`
        #DB_CFLAGS = -DNODB
        #DB_LDFLAGS =
        
       +EXTRA_CFLAGS_DEBUG0 =
       +EXTRA_CFLAGS_DEBUG1 = -g
       +EXTRA_FLAGS_SANITIZE0 =
       +EXTRA_FLAGS_SANITIZE1 = -fsanitize=address,undefined
       +
        # Note: Older systems might need `imlib2-config --cflags` and `imlib2-config --libs` instead of pkg-config.
       -CROP_CFLAGS = ${CFLAGS} ${DB_CFLAGS} -Wall -Wextra -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 imlib2`
       -CROP_LDFLAGS = ${LDFLAGS} ${DB_LDFLAGS} `pkg-config --libs x11 imlib2` -lm
       +CROP_CFLAGS = ${CFLAGS} ${DB_CFLAGS} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 imlib2`
       +CROP_LDFLAGS = ${LDFLAGS} ${DB_LDFLAGS} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} `pkg-config --libs x11 imlib2` -lm
        
        all: ${BIN}
        
       -.c:
       -        ${CC} -o $@ $< ${CROP_CFLAGS} ${CROP_LDFLAGS}
       +croptool: croptool.c common.c common.h
       +        ${CC} -o $@ croptool.c common.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
       +
       +selectool: selectool.c common.c common.h
       +        ${CC} -o $@ selectool.c common.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
       +
       +croptool_crop: croptool_crop.c
       +        ${CC} -o $@ croptool_crop.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
        
        install: all
                mkdir -p "${DESTDIR}${PREFIX}/bin"
   DIR diff --git a/README b/README
       @@ -1,4 +1,5 @@
        croptool - mass image cropping tool
       +selectool - image selection tool
        
        REQUIREMENTS: xlib, imlib2
        OPTIONAL: xext (for double-buffering extension)
       @@ -6,6 +7,15 @@ OPTIONAL: xext (for double-buffering extension)
        croptool is a simple tool to select cropping rectangles
        on images and print out a command to crop each image.
        
       +selectool is a simple tool to select images and output
       +a command for each selected image. It is mainly meant
       +to help quickly delete images that have been recovered
       +using programs like photorec or foremost.
       +
        See Makefile for compile-time options.
        
       -See croptool.1 and croptool_crop.1 for usage information.
       +See croptool.1, croptool_crop.1, and selectool.1 for usage information.
       +
       +Note: I know the names aren't very creative and might
       +cause issues if this ever makes its way into any package
       +repositories. Let me know if you have any better ideas.
   DIR diff --git a/TODO b/TODO
       @@ -1,4 +1,8 @@
        * Proper path handling (allow paths including "'", etc.)
       +  - Option 1: Implement command parsing inside croptool/selectool
       +    and call commands directly instead of printing them.
       +  - Option 2: Add option 'escape chars' for characters that
       +    must be escaped in the filename (kind of hacky).
        * Draw pixmap on exposure events instead of doing the
          expensive image resizing each time
        * Maybe add zooming support
   DIR diff --git a/common.c b/common.c
       @@ -0,0 +1,332 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * 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.
       + */
       +
       +#include <stdio.h>
       +#include <errno.h>
       +#include <string.h>
       +#include <stdlib.h>
       +#include <limits.h>
       +
       +#include <X11/X.h>
       +#include <X11/Xlib.h>
       +#include <X11/Xutil.h>
       +#ifndef NODB
       +#include <X11/extensions/Xdbe.h>
       +#endif
       +
       +#include <Imlib2.h>
       +
       +#include "common.h"
       +
       +void
       +setup_x(GraphicsContext *ctx, int window_w, int window_h, int line_width, int cache_size) {
       +        XSetWindowAttributes attrs;
       +        XGCValues gcv;
       +
       +        ctx->dpy = XOpenDisplay(NULL);
       +        ctx->screen = DefaultScreen(ctx->dpy);
       +        ctx->vis = DefaultVisual(ctx->dpy, ctx->screen);
       +        ctx->depth = DefaultDepth(ctx->dpy, ctx->screen);
       +        ctx->cm = DefaultColormap(ctx->dpy, ctx->screen);
       +        ctx->dirty = 1;
       +
       +        #ifndef NODB
       +        ctx->db_enabled = 0;
       +        /* based on http://wili.cc/blog/xdbe.html */
       +        int major, minor;
       +        if (XdbeQueryExtension(ctx->dpy, &major, &minor)) {
       +                int num_screens = 1;
       +                Drawable screens[] = { DefaultRootWindow(ctx->dpy) };
       +                XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
       +                    ctx->dpy, screens, &num_screens
       +                );
       +                if (!info || num_screens < 1 || info->count < 1) {
       +                        fprintf(stderr,
       +                            "Warning: No visuals support Xdbe, "
       +                            "double buffering disabled.\n"
       +                        );
       +                } else {
       +                        XVisualInfo xvisinfo_templ;
       +                        xvisinfo_templ.visualid = info->visinfo[0].visual;
       +                        xvisinfo_templ.screen = 0;
       +                        xvisinfo_templ.depth = info->visinfo[0].depth;
       +                        int matches;
       +                        XVisualInfo *xvisinfo_match = XGetVisualInfo(
       +                            ctx->dpy,
       +                            VisualIDMask | VisualScreenMask | VisualDepthMask,
       +                            &xvisinfo_templ, &matches
       +                        );
       +                        if (!xvisinfo_match || matches < 1) {
       +                                fprintf(stderr,
       +                                    "Warning: Couldn't match a Visual with "
       +                                    "double buffering, double buffering disabled.\n"
       +                                );
       +                        } else {
       +                                ctx->vis = xvisinfo_match->visual;
       +                                ctx->depth = xvisinfo_match->depth;
       +                                ctx->db_enabled = 1;
       +                        }
       +                        XFree(xvisinfo_match);
       +                }
       +                XdbeFreeVisualInfo(info);
       +        } else {
       +                fprintf(stderr, "Warning: No Xdbe support, double buffering disabled.\n");
       +        }
       +        #endif
       +
       +        memset(&attrs, 0, sizeof(attrs));
       +        attrs.background_pixel = BlackPixel(ctx->dpy, ctx->screen);
       +        attrs.colormap = ctx->cm;
       +        /* this causes the window contents to be kept
       +         * when it is resized, leading to less flicker */
       +        attrs.bit_gravity = NorthWestGravity;
       +        ctx->win = XCreateWindow(ctx->dpy, DefaultRootWindow(ctx->dpy), 0, 0,
       +            window_w, window_h, 0, ctx->depth,
       +            InputOutput, ctx->vis, CWBackPixel | CWColormap | CWBitGravity, &attrs);
       +
       +        #ifndef NODB
       +        if (ctx->db_enabled) {
       +                ctx->back_buf = XdbeAllocateBackBufferName(
       +                    ctx->dpy, ctx->win, XdbeCopied
       +                );
       +                ctx->drawable = ctx->back_buf;
       +        } else {
       +                ctx->drawable = ctx->win;
       +        }
       +        #else
       +        ctx->drawable = ctx->win;
       +        #endif
       +
       +        memset(&gcv, 0, sizeof(gcv));
       +        gcv.line_width = line_width;
       +        ctx->gc = XCreateGC(ctx->dpy, ctx->win, GCLineWidth, &gcv);
       +
       +        XSelectInput(
       +            ctx->dpy, ctx->win,
       +            StructureNotifyMask | KeyPressMask | ButtonPressMask |
       +            ButtonReleaseMask | PointerMotionMask | ExposureMask
       +        );
       +
       +        ctx->wm_delete_msg = XInternAtom(ctx->dpy, "WM_DELETE_WINDOW", False);
       +        XSetWMProtocols(ctx->dpy, ctx->win, &ctx->wm_delete_msg, 1);
       +
       +        /* note: since cache_size is <= 1024, this definitely fits in long */
       +        long cs = (long)cache_size * 1024 * 1024;
       +        if (cs > INT_MAX) {
       +                fprintf(stderr, "Cache size would cause integer overflow.\n");
       +                exit(1);
       +        }
       +        imlib_set_cache_size((int)cs);
       +        imlib_set_color_usage(128);
       +        imlib_context_set_dither(1);
       +        imlib_context_set_display(ctx->dpy);
       +        imlib_context_set_visual(ctx->vis);
       +        imlib_context_set_colormap(ctx->cm);
       +        imlib_context_set_drawable(ctx->drawable);
       +        ctx->updates = imlib_updates_init();
       +        ctx->cur_image = NULL;
       +}
       +
       +void
       +cleanup_x(GraphicsContext *ctx) {
       +        if (ctx->cur_image) {
       +                imlib_context_set_image(ctx->cur_image);
       +                imlib_free_image();
       +        }
       +        XDestroyWindow(ctx->dpy, ctx->win);
       +        XCloseDisplay(ctx->dpy);
       +}
       +
       +int
       +parse_int(const char *str, int min, int max, int *value) {
       +        char *end;
       +        long l = strtol(str, &end, 10);
       +        if (min > max)
       +                return 1;
       +        if (str == end || *end != '\0') {
       +                return 1;
       +        } else if (l < min || l > max || ((l == LONG_MIN ||
       +            l == LONG_MAX) && errno == ERANGE)) {
       +                return 1;
       +        }
       +        *value = (int)l;
       +
       +        return 0;
       +}
       +
       +void
       +queue_area_update(GraphicsContext *ctx, ImageSize *sz, int x, int y, int w, int h) {
       +        if (x > sz->scaled_w || y > sz->scaled_h)
       +                return;
       +        ctx->updates = imlib_update_append_rect(
       +            ctx->updates, x, y,
       +            w + x > sz->scaled_w ? sz->scaled_w - x : w,
       +            h + y > sz->scaled_h ? sz->scaled_h - y : h
       +        );
       +        ctx->dirty = 1;
       +}
       +
       +void
       +clear_screen(GraphicsContext *ctx) {
       +
       +        /* clear the window completely */
       +        XSetForeground(ctx->dpy, ctx->gc, BlackPixel(ctx->dpy, ctx->screen));
       +        XFillRectangle(
       +            ctx->dpy, ctx->drawable, ctx->gc,
       +            0, 0, ctx->window_w, ctx->window_h
       +        );
       +}
       +
       +void
       +draw_image_updates(GraphicsContext *ctx, ImageSize *sz) {
       +        Imlib_Image buffer;
       +        Imlib_Updates current_update;
       +
       +        /* draw the parts of the image that need to be redrawn */
       +        ctx->updates = imlib_updates_merge_for_rendering(
       +            ctx->updates, sz->scaled_w, sz->scaled_h
       +        );
       +        /* FIXME: check since when imlib_render_image_updates_on_drawable supported, also maybe just render_on_drawable part scaled */
       +        for (current_update = ctx->updates; current_update;
       +            current_update = imlib_updates_get_next(current_update)) {
       +                int up_x, up_y, up_w, up_h;
       +                imlib_updates_get_coordinates(current_update, &up_x, &up_y, &up_w, &up_h);
       +                buffer = imlib_create_image(up_w, up_h);
       +                imlib_context_set_blend(0);
       +                imlib_context_set_image(buffer);
       +                imlib_blend_image_onto_image(
       +                    ctx->cur_image, 0, 0, 0,
       +                    sz->orig_w, sz->orig_h,
       +                    -up_x, -up_y,
       +                    sz->scaled_w, sz->scaled_h);
       +                imlib_render_image_on_drawable(up_x, up_y);
       +                imlib_free_image();
       +        }
       +        if (ctx->updates)
       +                imlib_updates_free(ctx->updates);
       +        ctx->updates = imlib_updates_init();
       +}
       +
       +void
       +wipe_around_image(GraphicsContext *ctx, ImageSize *sz) {
       +
       +        /* wipe the black area around the image */
       +        XSetForeground(ctx->dpy, ctx->gc, BlackPixel(ctx->dpy, ctx->screen));
       +        XFillRectangle(
       +            ctx->dpy, ctx->drawable, ctx->gc,
       +            0, sz->scaled_h, sz->scaled_w, ctx->window_h - sz->scaled_h
       +        );
       +        XFillRectangle(
       +            ctx->dpy, ctx->drawable, ctx->gc,
       +            sz->scaled_w, 0, ctx->window_w - sz->scaled_w, ctx->window_h
       +        );
       +}
       +
       +void
       +swap_buffers(GraphicsContext *ctx) {
       +        #ifndef NODB
       +        if (ctx->db_enabled) {
       +                XdbeSwapInfo swap_info;
       +                swap_info.swap_window = ctx->win;
       +                swap_info.swap_action = XdbeCopied;
       +
       +                if (!XdbeSwapBuffers(ctx->dpy, &swap_info, 1))
       +                        fprintf(stderr, "Warning: Unable to swap buffers.\n");
       +        }
       +        #endif
       +        ctx->dirty = 0;
       +}
       +
       +/* get the scaled size of an image based on the current window size */
       +void
       +get_scaled_size(GraphicsContext *ctx, int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
       +        double scale_w, scale_h;
       +        scale_w = (double)ctx->window_w / (double)orig_w;
       +        scale_h = (double)ctx->window_h / (double)orig_h;
       +        if (orig_w <= ctx->window_w && orig_h <= ctx->window_h) {
       +                *scaled_w = orig_w;
       +                *scaled_h = orig_h;
       +        } else if (scale_w * orig_h > ctx->window_h) {
       +                *scaled_w = (int)(scale_h * orig_w);
       +                *scaled_h = ctx->window_h;
       +        } else {
       +                *scaled_w = ctx->window_w;
       +                *scaled_h = (int)(scale_w * orig_h);
       +        }
       +}
       +
       +void
       +next_picture(int cur_selection, char **filenames, int num_files, int copy_box) {
       +        if (cur_selection + 1 >= num_files)
       +                return;
       +        Imlib_Image tmp_image = NULL;
       +        int tmp_cur_selection = cur_selection;
       +        /* loop until we find a loadable file */
       +        while (!tmp_image && tmp_cur_selection + 1 < num_files) {
       +                tmp_cur_selection++;
       +                if (!filenames[tmp_cur_selection])
       +                        continue;
       +                tmp_image = imlib_load_image_immediately(
       +                    filenames[tmp_cur_selection]
       +                );
       +                if (!tmp_image) {
       +                        fprintf(
       +                                stderr, "Warning: Unable to load image '%s'.\n",
       +                                filenames[tmp_cur_selection]
       +                        );
       +                        filenames[tmp_cur_selection] = NULL;
       +                }
       +        }
       +        /* immediately exit program if no loadable image is found on startup */
       +        if (cur_selection < 0 && !tmp_image) {
       +                fprintf(stderr, "No loadable images found.\n");
       +                cleanup();
       +                exit(1);
       +        }
       +        if (!tmp_image)
       +                return;
       +
       +        change_picture(tmp_image, tmp_cur_selection, copy_box);
       +}
       +
       +void
       +last_picture(int cur_selection, char **filenames, int copy_box) {
       +        if (cur_selection <= 0)
       +                return;
       +        Imlib_Image tmp_image = NULL;
       +        int tmp_cur_selection = cur_selection;
       +        /* loop until we find a loadable file */
       +        while (!tmp_image && tmp_cur_selection > 0) {
       +                tmp_cur_selection--;
       +                if (!filenames[tmp_cur_selection])
       +                        continue;
       +                tmp_image = imlib_load_image_immediately(
       +                    filenames[tmp_cur_selection]
       +                );
       +                if (!tmp_image) {
       +                        fprintf(
       +                                stderr, "Warning: Unable to load image '%s'.\n",
       +                                filenames[tmp_cur_selection]
       +                        );
       +                        filenames[tmp_cur_selection] = NULL;
       +                }
       +        }
       +
       +        if (!tmp_image)
       +                return;
       +
       +        change_picture(tmp_image, tmp_cur_selection, copy_box);
       +}
   DIR diff --git a/common.h b/common.h
       @@ -0,0 +1,83 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * 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.
       + */
       +
       +#ifndef CROPTOOL_COMMON
       +#define CROPTOOL_COMMON
       +
       +#include <X11/X.h>
       +#include <X11/Xlib.h>
       +#ifndef NODB
       +#include <X11/extensions/Xdbe.h>
       +#endif
       +#include <Imlib2.h>
       +
       +typedef struct {
       +        int orig_w;
       +        int orig_h;
       +        int scaled_w;
       +        int scaled_h;
       +} ImageSize;
       +
       +typedef struct {
       +        Display *dpy;
       +        GC gc;
       +        Window win;
       +        Visual *vis;
       +        Drawable drawable;
       +        #ifndef NODB
       +        XdbeBackBuffer back_buf;
       +        int db_enabled;
       +        #endif
       +        Colormap cm;
       +        int screen;
       +        int depth;
       +
       +        int window_w;
       +        int window_h;
       +        char dirty;
       +        Atom wm_delete_msg;
       +        Imlib_Image cur_image;
       +        Imlib_Updates updates;
       +} GraphicsContext;
       +
       +void setup_x(GraphicsContext *ctx, int window_w, int window_h, int line_height, int cache_size);
       +void cleanup_x(GraphicsContext *ctx);
       +/* Parse integer between min and max (inclusive).
       +   Returns 1 on error, 0 otherwise.
       +   The result is stored in *value.
       +   Based on OpenBSD's strtonum. */
       +int parse_int(const char *str, int min, int max, int *value);
       +/* queue a part of the image for redrawing */
       +void queue_area_update(GraphicsContext *ctx, ImageSize *sz, int x, int y, int w, int h);
       +void clear_screen(GraphicsContext *ctx);
       +void draw_image_updates(GraphicsContext *ctx, ImageSize *sz);
       +void wipe_around_image(GraphicsContext *ctx, ImageSize *sz);
       +void swap_buffers(GraphicsContext *ctx);
       +void get_scaled_size(GraphicsContext *ctx, int orig_w, int orig_h, int *scaled_w, int *scaled_h);
       +/* show the next image in the argument list - unloadable files are skipped
       + * copy_box determines whether the current selection is copied
       + * (only relevant in croptool, not in selectool) */
       +void next_picture(int cur_selection, char **filenames, int num_files, int copy_box);
       +/* show the previous image in the argument list - unloadable files are skipped
       + * copy_box determines whether the current selection is copied
       + * (only relevant in croptool, not in selectool) */
       +void last_picture(int cur_selection, char **filenames, int copy_box);
       +
       +/* these are actually defined in croptool.c and selectool.c */
       +void cleanup(void);
       +void change_picture(Imlib_Image new_image, int new_selection, int copy_box);
       +
       +#endif /* CROPTOOL_COMMON */
   DIR diff --git a/croptool.1 b/croptool.1
       @@ -1,4 +1,4 @@
       -.Dd August 18, 2023
       +.Dd May 14, 2024
        .Dt CROPTOOL 1
        .Os
        .Sh NAME
       @@ -113,15 +113,18 @@ though the pixels covered in the original image are different.
        Go to the previous image, copying the current cropping rectangle.
        The same caveat as above applies.
        .It TAB
       -Switch the color of the cropping rectangle between the primary and secondary colors.
       +Switch the color of the cropping rectangle between the primary and
       +secondary colors.
        .It DELETE
       -Delete the cropping rectangle of the current image.
       +Remove the cropping rectangle of the current image.
        .It SPACE
        Redraw the window.
        This is useful when automatic redrawing is disabled with
        .Fl m .
        .It q
       -Exit the program.
       +Exit the program, printing the cropping command for any images with a
       +cropping rectangle set.
       +If the window is closed through some other means, no commands are printed.
        .El
        .Sh MOUSE ACTIONS
        .Bl -tag -width Ds
       @@ -168,7 +171,8 @@ filenames containing quotes).
        .Sh SEE ALSO
        .Xr convert 1 ,
        .Xr croptool_crop 1 ,
       -.Xr mogrify 1
       +.Xr mogrify 1 ,
       +.Xr selectool 1
        .Sh AUTHORS
        .An lumidify Aq Mt nobody@lumidify.org
        .Sh BUGS
   DIR diff --git a/croptool.c b/croptool.c
       @@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       @@ -16,21 +16,19 @@
        
        #include <math.h>
        #include <stdio.h>
       -#include <errno.h>
       -#include <string.h>
        #include <stdlib.h>
       -#include <limits.h>
        #include <unistd.h>
       +
       +#include <X11/X.h>
        #include <X11/Xlib.h>
        #include <X11/Xutil.h>
        #include <X11/keysym.h>
       -#include <X11/XF86keysym.h>
        #include <X11/cursorfont.h>
       -#ifndef NODB
       -#include <X11/extensions/Xdbe.h>
       -#endif
       +
        #include <Imlib2.h>
        
       +#include "common.h"
       +
        /* The number of pixels to check on each side when checking
         * if a corner or edge of the selection box was clicked 
         * (in order to change the size of the box) */
       @@ -77,47 +75,28 @@ struct Point {
        
        struct Selection {
                struct Rect rect;
       -        int orig_w;
       -        int orig_h;
       -        int scaled_w;
       -        int scaled_h;
       +        ImageSize sz;
                char valid;
        };
        
        static struct {
       -        Display *dpy;
       -        GC gc;
       -        Window win;
       -        Visual *vis;
       -        Drawable drawable;
       -        #ifndef NODB
       -        XdbeBackBuffer back_buf;
       -        int db_enabled;
       -        #endif
       -        Colormap cm;
       -        int screen;
       -        int depth;
       +        GraphicsContext ctx;
        
                struct Selection *selections;
                char **filenames;
                int cur_selection;
                int num_files;
       -        int window_w;
       -        int window_h;
                int cursor_x;
                int cursor_y;
                struct Point move_handle;
       +        XColor col1;
       +        XColor col2;
       +        int cur_col;
                char moving;
                char resizing;
                char lock_x;
                char lock_y;
       -        char dirty;
       -        XColor col1;
       -        XColor col2;
       -        int cur_col;
       -        Atom wm_delete_msg;
       -        Imlib_Image cur_image;
       -        Imlib_Updates updates;
       +        char print_on_exit;
        } state;
        
        static struct {
       @@ -135,7 +114,6 @@ static struct {
        static void usage(void);
        static void mainloop(void);
        static void setup(int argc, char *argv[]);
       -static void cleanup(void);
        static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
        static void swap(int *a, int *b);
        static void redraw(void);
       @@ -146,14 +124,11 @@ static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
        static int collide_rect(int x, int y, struct Rect rect);
        static void switch_color(void);
        static void clear_selection(void);
       -static void next_picture(int copy_box);
       -static void last_picture(int copy_box);
       -static void change_picture(Imlib_Image new_image, int new_selection, int copy_box);
       -static void get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h);
        static void set_selection(
            struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
            int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h
        );
       +static void queue_update(int x, int y, int w, int h);
        static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
        static void set_cursor(struct Rect rect);
        static void drag_motion(XEvent event);
       @@ -161,8 +136,6 @@ static void resize_window(int w, int h);
        static void button_release(void);
        static void button_press(XEvent event);
        static int key_press(XEvent event);
       -static void queue_update(int x, int y, int w, int h);
       -static int parse_int(const char *str, int min, int max, int *value);
        
        static void
        usage(void) {
       @@ -229,9 +202,11 @@ main(int argc, char *argv[]) {
        
                mainloop();
        
       -        for (int i = 0; i < argc; i++) {
       -                if (state.selections[i].valid) {
       -                        print_selection(&state.selections[i], state.filenames[i]);
       +        if (state.print_on_exit) {
       +                for (int i = 0; i < argc; i++) {
       +                        if (state.selections[i].valid) {
       +                                print_selection(&state.selections[i], state.filenames[i]);
       +                        }
                        }
                }
        
       @@ -247,7 +222,7 @@ mainloop(void) {
        
                while (running) {
                        do {
       -                        XNextEvent(state.dpy, &event);
       +                        XNextEvent(state.ctx.dpy, &event);
                                switch (event.type) {
                                case Expose:
                                        if (RESIZE_REDRAW)
       @@ -276,12 +251,12 @@ mainloop(void) {
                                        running = key_press(event);
                                        break;
                                case ClientMessage:
       -                                if ((Atom)event.xclient.data.l[0] == state.wm_delete_msg)
       +                                if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
                                                running = 0;
                                default:
                                        break;
                                }
       -                } while (XPending(state.dpy));
       +                } while (XPending(state.ctx.dpy));
        
                        redraw();
                }
       @@ -289,10 +264,6 @@ mainloop(void) {
        
        static void
        setup(int argc, char *argv[]) {
       -        XSetWindowAttributes attrs;
       -        XGCValues gcv;
       -
       -        state.cur_image = NULL;
                state.selections = malloc(argc * sizeof(struct Selection));
                if (!state.selections) {
                        fprintf(stderr, "Unable to allocate memory.\n");
       @@ -305,155 +276,50 @@ setup(int argc, char *argv[]) {
                state.resizing = 0;
                state.lock_x = 0;
                state.lock_y = 0;
       -        state.window_w = 500;
       -        state.window_h = 500;
                state.cursor_x = 0;
                state.cursor_y = 0;
                state.cur_col = 1;
       +        state.print_on_exit = 0;
        
                for (int i = 0; i < argc; i++) {
                        state.selections[i].valid = 0;
                }
        
       -        state.dpy = XOpenDisplay(NULL);
       -        state.screen = DefaultScreen(state.dpy);
       -        state.vis = DefaultVisual(state.dpy, state.screen);
       -        state.depth = DefaultDepth(state.dpy, state.screen);
       -        state.cm = DefaultColormap(state.dpy, state.screen);
       -
       -        #ifndef NODB
       -        state.db_enabled = 0;
       -        /* based on http://wili.cc/blog/xdbe.html */
       -        int major, minor;
       -        if (XdbeQueryExtension(state.dpy, &major, &minor)) {
       -                int num_screens = 1;
       -                Drawable screens[] = { DefaultRootWindow(state.dpy) };
       -                XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
       -                    state.dpy, screens, &num_screens
       -                );
       -                if (!info || num_screens < 1 || info->count < 1) {
       -                        fprintf(stderr,
       -                            "Warning: No visuals support Xdbe, "
       -                            "double buffering disabled.\n"
       -                        );
       -                } else {
       -                        XVisualInfo xvisinfo_templ;
       -                        xvisinfo_templ.visualid = info->visinfo[0].visual;
       -                        xvisinfo_templ.screen = 0;
       -                        xvisinfo_templ.depth = info->visinfo[0].depth;
       -                        int matches;
       -                        XVisualInfo *xvisinfo_match = XGetVisualInfo(
       -                            state.dpy,
       -                            VisualIDMask | VisualScreenMask | VisualDepthMask,
       -                            &xvisinfo_templ, &matches
       -                        );
       -                        if (!xvisinfo_match || matches < 1) {
       -                                fprintf(stderr,
       -                                    "Warning: Couldn't match a Visual with "
       -                                    "double buffering, double buffering disabled.\n"
       -                                );
       -                        } else {
       -                                state.vis = xvisinfo_match->visual;
       -                                state.depth = xvisinfo_match->depth;
       -                                state.db_enabled = 1;
       -                        }
       -                        XFree(xvisinfo_match);
       -                }
       -                XdbeFreeVisualInfo(info);
       -        } else {
       -                fprintf(stderr, "Warning: No Xdbe support, double buffering disabled.\n");
       -        }
       -        #endif
       -
       -        memset(&attrs, 0, sizeof(attrs));
       -        attrs.background_pixel = BlackPixel(state.dpy, state.screen);
       -        attrs.colormap = state.cm;
       -        /* this causes the window contents to be kept
       -         * when it is resized, leading to less flicker */
       -        attrs.bit_gravity = NorthWestGravity;
       -        state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0,
       -            state.window_w, state.window_h, 0, state.depth,
       -            InputOutput, state.vis, CWBackPixel | CWColormap | CWBitGravity, &attrs);
       +        setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
        
       -        #ifndef NODB
       -        if (state.db_enabled) {
       -                state.back_buf = XdbeAllocateBackBufferName(
       -                    state.dpy, state.win, XdbeCopied
       -                );
       -                state.drawable = state.back_buf;
       -        } else {
       -                state.drawable = state.win;
       -        }
       -        #else
       -        state.drawable = state.win;
       -        #endif
       -
       -        memset(&gcv, 0, sizeof(gcv));
       -        gcv.line_width = LINE_WIDTH;
       -        state.gc = XCreateGC(state.dpy, state.win, GCLineWidth, &gcv);
       -
       -        if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR1, &state.col1)) {
       +        if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR1, &state.col1)) {
                        fprintf(stderr, "Primary color invalid.\n");
                        exit(1);
                }
       -        XAllocColor(state.dpy, state.cm, &state.col1);
       -        if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR2, &state.col2)) {
       +        XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1);
       +        if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR2, &state.col2)) {
                        fprintf(stderr, "Secondary color invalid.\n");
                        exit(1);
                }
       -        XAllocColor(state.dpy, state.cm, &state.col2);
       -
       -        XSelectInput(
       -            state.dpy, state.win,
       -            StructureNotifyMask | KeyPressMask | ButtonPressMask |
       -            ButtonReleaseMask | PointerMotionMask | ExposureMask
       -        );
       -
       -        state.wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False);
       -        XSetWMProtocols(state.dpy, state.win, &state.wm_delete_msg, 1);
       -
       -        cursors.top = XCreateFontCursor(state.dpy, XC_top_side);
       -        cursors.bottom = XCreateFontCursor(state.dpy, XC_bottom_side);
       -        cursors.left = XCreateFontCursor(state.dpy, XC_left_side);
       -        cursors.right = XCreateFontCursor(state.dpy, XC_right_side);
       -        cursors.topleft = XCreateFontCursor(state.dpy, XC_top_left_corner);
       -        cursors.topright = XCreateFontCursor(state.dpy, XC_top_right_corner);
       -        cursors.bottomleft = XCreateFontCursor(state.dpy, XC_bottom_left_corner);
       -        cursors.bottomright = XCreateFontCursor(state.dpy, XC_bottom_right_corner);
       -        cursors.grab = XCreateFontCursor(state.dpy, XC_fleur);
       -
       -        /* note: since CACHE_SIZE is <= 1024, this definitely fits in long */
       -        long cs = (long)CACHE_SIZE * 1024 * 1024;
       -        if (cs > INT_MAX) {
       -                fprintf(stderr, "Cache size would cause integer overflow.\n");
       -                exit(1);
       -        }
       -        imlib_set_cache_size((int)cs);
       -        imlib_set_color_usage(128);
       -        imlib_context_set_dither(1);
       -        imlib_context_set_display(state.dpy);
       -        imlib_context_set_visual(state.vis);
       -        imlib_context_set_colormap(state.cm);
       -        imlib_context_set_drawable(state.drawable);
       -        state.updates = imlib_updates_init();
       -
       -        next_picture(0);
       +        XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2);
       +
       +        cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side);
       +        cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side);
       +        cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side);
       +        cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side);
       +        cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner);
       +        cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner);
       +        cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner);
       +        cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner);
       +        cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur);
       +
       +        next_picture(state.cur_selection, state.filenames, state.num_files, 0);
                /* Only map window here so the program exits immediately if
                   there are no loadable images, without first opening the
                   window and closing it again immediately */
       -        XMapWindow(state.dpy, state.win);
       +        XMapWindow(state.ctx.dpy, state.ctx.win);
                redraw();
        }
        
       -static void
       +void
        cleanup(void) {
       -        if (state.cur_image) {
       -                imlib_context_set_image(state.cur_image);
       -                imlib_free_image();
       -        }
                free(state.selections);
       -        XDestroyWindow(state.dpy, state.win);
       -        XCloseDisplay(state.dpy);
       +        cleanup_x(&state.ctx);
        }
        
        /* TODO: Escape filename properly
       @@ -465,6 +331,7 @@ print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
                const char *c;
                int length = 0;
                int start_index = 0;
       +        /* FIXME: just use putc instead of this complex printf dance */
                for (c = CMD_FORMAT; *c != '\0'; c++) {
                        if (percent)
                                start_index++;
       @@ -529,118 +396,34 @@ print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
                }
        }
        
       -/* Parse integer between min and max (inclusive).
       -   Returns 1 on error, 0 otherwise.
       -   The result is stored in *value.
       -   Based on OpenBSD's strtonum. */
       -static int
       -parse_int(const char *str, int min, int max, int *value) {
       -        char *end;
       -        long l = strtol(str, &end, 10);
       -        if (min > max)
       -                return 1;
       -        if (str == end || *end != '\0') {
       -                return 1; 
       -        } else if (l < min || l > max || ((l == LONG_MIN ||
       -            l == LONG_MAX) && errno == ERANGE)) {
       -                return 1;
       -        }
       -        *value = (int)l;
       -
       -        return 0;
       -}
       -
       -/* queue a part of the image for redrawing */
       -static void
       -queue_update(int x, int y, int w, int h) {
       -        if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
       -                return;
       -        state.dirty = 1;
       -        struct Selection *sel = &state.selections[state.cur_selection];
       -        if (x > sel->scaled_w || y > sel->scaled_h)
       -                return;
       -        state.updates = imlib_update_append_rect(
       -            state.updates, x, y,
       -            w + x > sel->scaled_w ? sel->scaled_w - x : w,
       -            h + y > sel->scaled_h ? sel->scaled_h - y : h
       -        );
       -}
       -
        static void
        redraw(void) {
       -        Imlib_Image buffer;
       -        Imlib_Updates current_update;
       -        if (!state.dirty)
       +        if (!state.ctx.dirty)
       +                return;
       +        if (!state.ctx.cur_image || state.cur_selection < 0) {
       +                clear_screen(&state.ctx);
       +                swap_buffers(&state.ctx);
                        return;
       -        if (!state.cur_image || state.cur_selection < 0) {
       -                /* clear the window completely */
       -                XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
       -                XFillRectangle(
       -                    state.dpy, state.drawable, state.gc,
       -                    0, 0, state.window_w, state.window_h
       -                );
       -                goto swap_buffers;
                }
        
                /* draw the parts of the image that need to be redrawn */
                struct Selection *sel = &state.selections[state.cur_selection];
       -        state.updates = imlib_updates_merge_for_rendering(
       -            state.updates, sel->scaled_w, sel->scaled_h
       -        );
       -        for (current_update = state.updates; current_update;
       -            current_update = imlib_updates_get_next(current_update)) {
       -                int up_x, up_y, up_w, up_h;
       -                imlib_updates_get_coordinates(current_update, &up_x, &up_y, &up_w, &up_h);
       -                buffer = imlib_create_image(up_w, up_h);
       -                imlib_context_set_blend(0);
       -                imlib_context_set_image(buffer);
       -                imlib_blend_image_onto_image(
       -                    state.cur_image, 0, 0, 0,
       -                    sel->orig_w, sel->orig_h,
       -                    -up_x, -up_y,
       -                    sel->scaled_w, sel->scaled_h);
       -                imlib_render_image_on_drawable(up_x, up_y);
       -                imlib_free_image();
       -        }
       -        if (state.updates)
       -                imlib_updates_free(state.updates);
       -        state.updates = imlib_updates_init();
       +        draw_image_updates(&state.ctx, &sel->sz);
        
       -        /* wipe the black area around the image */
       -        XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
       -        XFillRectangle(
       -            state.dpy, state.drawable, state.gc,
       -            0, sel->scaled_h, sel->scaled_w, state.window_h - sel->scaled_h
       -        );
       -        XFillRectangle(
       -            state.dpy, state.drawable, state.gc,
       -            sel->scaled_w, 0, state.window_w - sel->scaled_w, state.window_h
       -        );
       +        wipe_around_image(&state.ctx, &sel->sz);
        
                /* draw the rectangle */
                struct Rect rect = sel->rect;
                if (rect.x0 != -200) {
                        XColor col = state.cur_col == 1 ? state.col1 : state.col2;
       -                XSetForeground(state.dpy, state.gc, col.pixel);
       +                XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel);
                        sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
                        XDrawRectangle(
       -                    state.dpy, state.drawable, state.gc,
       +                    state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
                            rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0
                        );
                }
       -
       -swap_buffers:
       -        #ifndef NODB
       -        if (state.db_enabled) {
       -                XdbeSwapInfo swap_info;
       -                swap_info.swap_window = state.win;
       -                swap_info.swap_action = XdbeCopied;
       -
       -                if (!XdbeSwapBuffers(state.dpy, &swap_info, 1))
       -                        fprintf(stderr, "Warning: Unable to swap buffers.\n");
       -        }
       -        #endif
       -        state.dirty = 0;
       +        swap_buffers(&state.ctx);
        }
        
        static void
       @@ -665,7 +448,7 @@ print_selection(struct Selection *sel, const char *filename) {
                /* The box was never actually used */
                if (sel->rect.x0 == -200)
                        return;
       -        double scale = (double)sel->orig_w / sel->scaled_w;
       +        double scale = (double)sel->sz.orig_w / sel->sz.scaled_w;
                int x0 = sel->rect.x0, y0 = sel->rect.y0;
                int x1 = sel->rect.x1, y1 = sel->rect.y1;
                sort_coordinates(&x0, &y0, &x1, &y1);
       @@ -674,13 +457,13 @@ print_selection(struct Selection *sel, const char *filename) {
                x1 = round(x1 * scale);
                y1 = round(y1 * scale);
                /* The box is completely outside of the picture. */
       -        if (x0 >= sel->orig_w || y0 >= sel->orig_h)
       +        if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h)
                        return;
                /* Cut the bounding box if it goes past the end of the picture. */
                x0 = x0 < 0 ? 0 : x0;
                y0 = y0 < 0 ? 0 : y0;
       -        x1 = x1 > sel->orig_w ? sel->orig_w : x1;
       -        y1 = y1 > sel->orig_h ? sel->orig_h : y1;
       +        x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1;
       +        y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1;
                print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0);
        }
        
       @@ -768,6 +551,14 @@ button_press(XEvent event) {
        }
        
        static void
       +queue_update(int x, int y, int w, int h) {
       +        if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
       +                return;
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
       +}
       +
       +static void
        button_release(void) {
                state.moving = 0;
                state.resizing = 0;
       @@ -776,35 +567,35 @@ button_release(void) {
                /* redraw everything if automatic redrawing of the rectangle
                   is disabled (so it's redrawn when the mouse is released) */
                if (!SELECTION_REDRAW)
       -                queue_update(0, 0, state.window_w, state.window_h);
       +                queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
        }
        
        static void
        resize_window(int w, int h) {
                int actual_w, actual_h;
                struct Selection *sel;
       -        state.window_w = w;
       -        state.window_h = h;
       +        state.ctx.window_w = w;
       +        state.ctx.window_h = h;
        
                if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
                        return;
                sel = &state.selections[state.cur_selection];
       -        get_scaled_size(sel->orig_w, sel->orig_h, &actual_w, &actual_h);
       -        if (actual_w != sel->scaled_w) {
       +        get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
       +        if (actual_w != sel->sz.scaled_w) {
                        if (sel->rect.x0 != -200) {
                                /* If there is a selection, we need to convert it to
                                 * the new scale. This only takes width into account
                                 * because the aspect ratio should have been preserved
                                 * anyways */
       -                        double scale = (double)actual_w / sel->scaled_w;
       +                        double scale = (double)actual_w / sel->sz.scaled_w;
                                sel->rect.x0 = round(sel->rect.x0 * scale);
                                sel->rect.y0 = round(sel->rect.y0 * scale);
                                sel->rect.x1 = round(sel->rect.x1 * scale);
                                sel->rect.y1 = round(sel->rect.y1 * scale);
                        }
       -                sel->scaled_w = actual_w;
       -                sel->scaled_h = actual_h;
       -                queue_update(0, 0, sel->scaled_w, sel->scaled_h);
       +                sel->sz.scaled_w = actual_w;
       +                sel->sz.scaled_h = actual_h;
       +                queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
                }
        }
        
       @@ -874,7 +665,7 @@ set_cursor(struct Rect rect) {
                } else if (collide_rect(state.cursor_x, state.cursor_y, rect)) {
                        c = cursors.grab;
                }
       -        XDefineCursor(state.dpy, state.win, c);
       +        XDefineCursor(state.ctx.dpy, state.ctx.win, c);
        }
        
        static void
       @@ -937,28 +728,10 @@ set_selection(
                sel->rect.y0 = rect_y0;
                sel->rect.x1 = rect_x1;
                sel->rect.y1 = rect_y1;
       -        sel->orig_w = orig_w;
       -        sel->orig_h = orig_h;
       -        sel->scaled_w = scaled_w;
       -        sel->scaled_h = scaled_h;
       -}
       -
       -/* get the scaled size of an image based on the current window size */
       -static void
       -get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
       -        double scale_w, scale_h;
       -        scale_w = (double)state.window_w / (double)orig_w;
       -        scale_h = (double)state.window_h / (double)orig_h;
       -        if (orig_w <= state.window_w && orig_h <= state.window_h) {
       -                *scaled_w = orig_w;
       -                *scaled_h = orig_h;
       -        } else if (scale_w * orig_h > state.window_h) {
       -                *scaled_w = (int)(scale_h * orig_w);
       -                *scaled_h = state.window_h;
       -        } else {
       -                *scaled_w = state.window_w;
       -                *scaled_h = (int)(scale_w * orig_h);
       -        }
       +        sel->sz.orig_w = orig_w;
       +        sel->sz.orig_h = orig_h;
       +        sel->sz.scaled_w = scaled_w;
       +        sel->sz.scaled_h = scaled_h;
        }
        
        /* change the shown image
       @@ -966,27 +739,27 @@ get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
         * copy_box determines whether the cropping rectangle of the current
         * selection should be copied (i.e. this is a true value when return
         * is pressed) */
       -static void
       +void
        change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
                int orig_w, orig_h, actual_w, actual_h;
                /* set window title to filename */
                XSetStandardProperties(
       -            state.dpy, state.win,
       +            state.ctx.dpy, state.ctx.win,
                    state.filenames[new_selection],
                    NULL, None, NULL, 0, NULL
                );
       -        if (state.cur_image) {
       -                imlib_context_set_image(state.cur_image);
       +        if (state.ctx.cur_image) {
       +                imlib_context_set_image(state.ctx.cur_image);
                        imlib_free_image();
                }
       -        state.cur_image = new_image;
       -        imlib_context_set_image(state.cur_image);
       +        state.ctx.cur_image = new_image;
       +        imlib_context_set_image(state.ctx.cur_image);
                int old_selection = state.cur_selection;
                state.cur_selection = new_selection;
        
                orig_w = imlib_image_get_width();
                orig_h = imlib_image_get_height();
       -        get_scaled_size(orig_w, orig_h, &actual_w, &actual_h);
       +        get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
        
                struct Selection *sel = &state.selections[state.cur_selection];
                if (copy_box && old_selection >= 0 && old_selection < state.num_files) {
       @@ -1004,95 +777,32 @@ change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
                            -200, -200, -200, -200,
                            orig_w, orig_h, actual_w, actual_h
                        );
       -        } else if (sel->rect.x0 != -200 && actual_w != sel->scaled_w) {
       +        } else if (sel->rect.x0 != -200 && actual_w != sel->sz.scaled_w) {
                        /* If there is a selection, we need to convert it to the
                         * new scale. This only takes width into account because
                         * the aspect ratio should have been preserved anyways */
       -                double scale = (double)actual_w / sel->scaled_w;
       +                double scale = (double)actual_w / sel->sz.scaled_w;
                        sel->rect.x0 = round(sel->rect.x0 * scale);
                        sel->rect.y0 = round(sel->rect.y0 * scale);
                        sel->rect.x1 = round(sel->rect.x1 * scale);
                        sel->rect.y1 = round(sel->rect.y1 * scale);
                }
       -        sel->scaled_w = actual_w;
       -        sel->scaled_h = actual_h;
       +        sel->sz.scaled_w = actual_w;
       +        sel->sz.scaled_h = actual_h;
                sel->valid = 1;
       -        queue_update(0, 0, sel->scaled_w, sel->scaled_h);
       +        queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
        
                /* set the cursor since the cropping rectangle may have changed */
                set_cursor(sel->rect);
        }
        
       -/* show the next image in the argument list - unloadable files are skipped
       - * copy_box determines whether the current selection is copied */
       -static void
       -next_picture(int copy_box) {
       -        if (state.cur_selection + 1 >= state.num_files)
       -                return;
       -        Imlib_Image tmp_image = NULL;
       -        int tmp_cur_selection = state.cur_selection;
       -        /* loop until we find a loadable file */
       -        while (!tmp_image && tmp_cur_selection + 1 < state.num_files) {
       -                tmp_cur_selection++;
       -                if (!state.filenames[tmp_cur_selection])
       -                        continue;
       -                tmp_image = imlib_load_image_immediately(
       -                    state.filenames[tmp_cur_selection]
       -                );
       -                if (!tmp_image) {
       -                        fprintf(stderr, "Warning: Unable to load image '%s'.\n",
       -                            state.filenames[tmp_cur_selection]);
       -                        state.filenames[tmp_cur_selection] = NULL;
       -                }
       -        }
       -        /* immediately exit program if no loadable image is found on startup */
       -        if (state.cur_selection < 0 && !tmp_image) {
       -                fprintf(stderr, "No loadable images found.\n");
       -                cleanup();
       -                exit(1);
       -        }
       -        if (!tmp_image)
       -                return;
       -
       -        change_picture(tmp_image, tmp_cur_selection, copy_box);
       -}
       -
       -/* show the previous image in the argument list - unloadable files are skipped
       - * copy_box determines whether the current selection is copied */
       -static void
       -last_picture(int copy_box) {
       -        if (state.cur_selection <= 0)
       -                return;
       -        Imlib_Image tmp_image = NULL;
       -        int tmp_cur_selection = state.cur_selection;
       -        /* loop until we find a loadable file */
       -        while (!tmp_image && tmp_cur_selection > 0) {
       -                tmp_cur_selection--;
       -                if (!state.filenames[tmp_cur_selection])
       -                        continue;
       -                tmp_image = imlib_load_image_immediately(
       -                    state.filenames[tmp_cur_selection]
       -                );
       -                if (!tmp_image) {
       -                        fprintf(stderr, "Warning: Unable to load image '%s'.\n",
       -                            state.filenames[tmp_cur_selection]);
       -                        state.filenames[tmp_cur_selection] = NULL;
       -                }
       -        }
       -
       -        if (!tmp_image)
       -                return;
       -
       -        change_picture(tmp_image, tmp_cur_selection, copy_box);
       -}
       -
        static void
        clear_selection(void) {
                if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
                        return;
                struct Selection *sel = &state.selections[state.cur_selection];
                sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
       -        queue_update(0, 0, sel->scaled_w, sel->scaled_h);
       +        queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
        }
        
        static void
       @@ -1100,7 +810,7 @@ switch_color(void) {
                if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
                        return;
                state.cur_col = state.cur_col == 1 ? 2 : 1;
       -        queue_update(0, 0, state.window_w, state.window_h);
       +        queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
        }
        
        static int
       @@ -1111,16 +821,16 @@ key_press(XEvent event) {
                XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
                switch (sym) {
                case XK_Left:
       -                last_picture(0);
       +                last_picture(state.cur_selection, state.filenames, 0);
                        break;
                case XK_Right:
       -                next_picture(0);
       +                next_picture(state.cur_selection, state.filenames, state.num_files, 0);
                        break;
                case XK_Return:
                        if (event.xkey.state & ShiftMask)
       -                        last_picture(1);
       +                        last_picture(state.cur_selection, state.filenames, 1);
                        else
       -                        next_picture(1);
       +                        next_picture(state.cur_selection, state.filenames, state.num_files, 1);
                        break;
                case XK_Delete:
                        clear_selection();
       @@ -1129,13 +839,14 @@ key_press(XEvent event) {
                        switch_color();
                        break;
                case XK_space:
       -                XGetWindowAttributes(state.dpy, state.win, &attrs);
       +                XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
                        resize_window(attrs.width, attrs.height);
                        /* queue update separately so it also redraws when
                           size didn't change */
       -                queue_update(0, 0, state.window_w, state.window_h);
       +                queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
                        break;
                case XK_q:
       +                state.print_on_exit = 1;
                        return 0;
                default:
                        break;
   DIR diff --git a/selectool.1 b/selectool.1
       @@ -0,0 +1,140 @@
       +.Dd May 14, 2024
       +.Dt SELECTOOL 1
       +.Os
       +.Sh NAME
       +.Nm selectool
       +.Nd image selection tool
       +.Sh SYNOPSIS
       +.Nm
       +.Op Ar -ms
       +.Op Ar -f format
       +.Op Ar -w width
       +.Op Ar -c color
       +.Op Ar -z size
       +.Ar file ...
       +.Sh DESCRIPTION
       +.Nm
       +shows each of the given images and allows them to be selected or deselected.
       +On exit, the given command is printed for each of the files.
       +.Sh OPTIONS
       +.Bl -tag -width Ds
       +.It Fl m
       +Disable automatic redrawing when the window is resized (the
       +.Fl m
       +stands for 'manual').
       +This may be useful on older machines that start accelerating global
       +warming when the image is redrawn constantly while resizing.
       +Note that this also disables exposure events, so the window has to be
       +manually redrawn when switching back to it from another window.
       +.It Fl s
       +Select all images by default.
       +.It Fl f Ar format
       +Set the format to be used when the commands are output.
       +See
       +.Sx OUTPUT FORMAT
       +for details.
       +.It Fl w Ar width
       +Set the line width of the cross that is drawn over selected images
       +in pixels (valid values: 1-99).
       +Default: 5.
       +.It Fl c Ar color
       +Set the color of the cross that is drawn over selected images.
       +Default: #FF0000.
       +.It Fl z Ar size
       +Set the Imlib2 in-memory cache to
       +.Ar size
       +MiB (valid values: 0-1024).
       +Default: 4.
       +.El
       +.Sh OUTPUT FORMAT
       +The command for each selected image is output using the format given by
       +.Fl f ,
       +or the default of
       +.Ql rm -- '%f' .
       +.Pp
       +The following substitutions are performed:
       +.Bl -tag -width Ds
       +.It %%
       +Print
       +.Ql % .
       +.It %f
       +Print the filename of the image.
       +Warning: This is printed as is, without any escaping.
       +.El
       +.Pp
       +If an unknown substitution is encountered, a warning is printed to
       +standard error and the characters are printed verbatim.
       +.Sh KEYBINDS
       +.Bl -tag -width Ds
       +.It ARROW LEFT
       +Go to the previous image.
       +.It ARROW RIGHT
       +Go to the next image.
       +.It RETURN
       +Deselect the current image and go to the next image.
       +.It SHIFT + RETURN
       +Deselect the current image and go to the previous image.
       +.It d
       +Select the current image and go to the next image.
       +.It D
       +Select the current image and go to the previous image.
       +.It t
       +Toggle the selection status of the current image.
       +.It SPACE
       +Redraw the window.
       +This is useful when automatic redrawing is disabled with
       +.Fl m .
       +.It q
       +Exit the program, printing the set command for all selected images.
       +If the window is closed through some other means, no commands are printed.
       +.El
       +.Sh EXIT STATUS
       +.Ex -std
       +.Sh EXAMPLES
       +Normal usage to delete selected images:
       +.Bd -literal
       +$ selectool *.jpg > tmp.sh
       +$ sh tmp.sh
       +.Ed
       +.Pp
       +Or, if you're brave:
       +.Bd -literal
       +$ selectool *.jpg | sh
       +.Ed
       +.Pp
       +The original use case for
       +.Nm
       +was to quickly delete images that have been recovered using programs like
       +.Xr photorec 8
       +or
       +.Xr foremost 8 .
       +When used on a system partition, these programs generally recover a lot of
       +images that aren't important, which then need to be sorted manually.
       +Other programs that the author used for this task in the past were not ideal
       +because they either were much too slow or allowed mistakes to be made too
       +easily by deleting images immediately.
       +.Pp
       +It is also possible to do more advanced things.
       +For instance, to move the selected images into a different directory,
       +something like this can be done:
       +.Bd -literal
       +$ selectool -f "mv -- '%f' '/path/to/dir/'" *.jpg | sh
       +.Ed
       +.Pp
       +Note that no great care has been taken to deal with filenames containing
       +single or double quotes.
       +That is left as an exercise to the reader (hint: just don't have
       +filenames containing quotes).
       +.Sh SEE ALSO
       +.Xr croptool 1 ,
       +.Xr rm 1 ,
       +.Xr foremost 8 ,
       +.Xr photorec 8
       +.Sh AUTHORS
       +.An lumidify Aq Mt nobody@lumidify.org
       +.Sh BUGS
       +The filenames are printed without any escaping, so filenames with
       +quotes may cause issues depending on the output format.
       +.Pp
       +Transparent portions of images should probably be shown differently,
       +but I'm too lazy to fix that and don't really care at the moment.
   DIR diff --git a/selectool.c b/selectool.c
       @@ -0,0 +1,423 @@
       +/*
       + * Copyright (c) 2024 lumidify <nobody@lumidify.org>
       + *
       + * 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.
       + */
       +
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <unistd.h>
       +
       +#include <X11/X.h>
       +#include <X11/Xlib.h>
       +#include <X11/Xutil.h>
       +#include <X11/keysym.h>
       +
       +#include <Imlib2.h>
       +
       +#include "common.h"
       +
       +/* Whether to select all images by default */
       +static int SELECT_DEFAULT = 0;
       +/* The color of the selection box */
       +static const char *SELECTION_COLOR = "#FF0000";
       +/* The width of the selection line */
       +static int LINE_WIDTH = 5;
       +/* When set to 1, the display is redrawn on window resize */
       +static short RESIZE_REDRAW = 1;
       +/*
       +  The command printed for each image.
       +  %f: Filename of image.
       +*/
       +static const char *CMD_FORMAT = "rm -- '%f'";
       +/* Size of Imlib2 in-memory cache in MiB */
       +static int CACHE_SIZE = 4;
       +
       +extern char *optarg;
       +extern int optind;
       +
       +struct Selection {
       +        ImageSize sz;
       +        char selected;
       +};
       +
       +static struct {
       +        GraphicsContext ctx;
       +
       +        struct Selection *selections;
       +        char **filenames;
       +        int cur_selection;
       +        int num_files;
       +        int cursor_x;
       +        int cursor_y;
       +        XColor col;
       +        char print_on_exit;
       +} state;
       +
       +static void usage(void);
       +static void mainloop(void);
       +static void setup(int argc, char *argv[]);
       +static void redraw(void);
       +static void set_selection(char selected);
       +static void toggle_selection(void);
       +static void resize_window(int w, int h);
       +static int key_press(XEvent event);
       +static void queue_update(int x, int y, int w, int h);
       +static void print_cmd(const char *filename, int dry_run);
       +
       +static void
       +usage(void) {
       +        fprintf(stderr, "USAGE: deletetool [-mrs] [-f format] "
       +            "[-w width] [-c color] "
       +            "[-z size] file ...\n");
       +}
       +
       +int
       +main(int argc, char *argv[]) {
       +        char c;
       +
       +        while ((c = getopt(argc, argv, "f:w:c:msz:")) != -1) {
       +                switch (c) {
       +                case 'f':
       +                        CMD_FORMAT = optarg;
       +                        break;
       +                case 'm':
       +                        RESIZE_REDRAW = 0;
       +                        break;
       +                case 'c':
       +                        SELECTION_COLOR = optarg;
       +                        break;
       +                case 'w':
       +                        if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
       +                                fprintf(stderr, "Invalid line width.\n");
       +                                exit(1);
       +                        }
       +                        break;
       +                case 'z':
       +                        if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
       +                                fprintf(stderr, "Invalid cache size.\n");
       +                                exit(1);
       +                        }
       +                        break;
       +                case 's':
       +                        SELECT_DEFAULT = 1;
       +                        break;
       +                default:
       +                        usage();
       +                        exit(1);
       +                        break;
       +                }
       +        }
       +
       +        /* print warning if command format is invalid */
       +        print_cmd("", 1);
       +
       +        argc -= optind;
       +        argv += optind;
       +        if (argc < 1) {
       +                usage();
       +                exit(1);
       +        }
       +        setup(argc, argv);
       +
       +        mainloop();
       +
       +        if (state.print_on_exit) {
       +                for (int i = 0; i < argc; i++) {
       +                        if (state.selections[i].selected)
       +                                print_cmd(state.filenames[i], 0);
       +                }
       +        }
       +
       +        cleanup();
       +
       +        return 0;
       +}
       +
       +static void
       +mainloop(void) {
       +        XEvent event;
       +        int running = 1;
       +
       +        while (running) {
       +                do {
       +                        XNextEvent(state.ctx.dpy, &event);
       +                        switch (event.type) {
       +                        case Expose:
       +                                if (RESIZE_REDRAW)
       +                                        queue_update(event.xexpose.x, event.xexpose.y,
       +                                            event.xexpose.width, event.xexpose.height);
       +                                break;
       +                        case ConfigureNotify:
       +                                if (RESIZE_REDRAW)
       +                                        resize_window(
       +                                            event.xconfigure.width,
       +                                            event.xconfigure.height
       +                                        );
       +                                break;
       +                        case KeyPress:
       +                                running = key_press(event);
       +                                break;
       +                        case ClientMessage:
       +                                if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
       +                                        running = 0;
       +                        default:
       +                                break;
       +                        }
       +                } while (XPending(state.ctx.dpy));
       +
       +                redraw();
       +        }
       +}
       +
       +static void
       +setup(int argc, char *argv[]) {
       +        state.selections = malloc(argc * sizeof(struct Selection));
       +        if (!state.selections) {
       +                fprintf(stderr, "Unable to allocate memory.\n");
       +                exit(1);
       +        }
       +        state.num_files = argc;
       +        state.filenames = argv;
       +        state.cur_selection = -1;
       +        state.print_on_exit = 0;
       +
       +        for (int i = 0; i < argc; i++) {
       +                state.selections[i].selected = SELECT_DEFAULT;
       +        }
       +
       +        setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
       +
       +        if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR, &state.col)) {
       +                fprintf(stderr, "Selection color invalid.\n");
       +                exit(1);
       +        }
       +        XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col);
       +
       +        next_picture(state.cur_selection, state.filenames, state.num_files, 0);
       +        /* Only map window here so the program exits immediately if
       +           there are no loadable images, without first opening the
       +           window and closing it again immediately */
       +        XMapWindow(state.ctx.dpy, state.ctx.win);
       +        redraw();
       +}
       +
       +void
       +cleanup(void) {
       +        free(state.selections);
       +        cleanup_x(&state.ctx);
       +}
       +
       +/* queue a part of the image for redrawing */
       +static void
       +queue_update(int x, int y, int w, int h) {
       +        if (state.cur_selection < 0)
       +                return;
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
       +}
       +
       +/* TODO: Escape filename properly
       + * -> But how? Since the format can be set by the user,
       + * it isn't really clear *what* needs to be escaped. */
       +static void
       +print_cmd(const char *filename, int dry_run) {
       +        short percent = 0;
       +        const char *c;
       +        int length = 0;
       +        int start_index = 0;
       +        /* FIXME: just use putc instead of this complex printf dance */
       +        for (c = CMD_FORMAT; *c != '\0'; c++) {
       +                if (percent)
       +                        start_index++;
       +                if (*c == '%') {
       +                        if (length) {
       +                                if (!dry_run)
       +                                        printf("%.*s", length, CMD_FORMAT + start_index);
       +                                start_index += length;
       +                                length = 0;
       +                        }
       +                        if (percent && !dry_run)
       +                                printf("%%");
       +                        percent++;
       +                        percent %= 2;
       +                        start_index++;
       +                } else if (percent && *c == 'f') {
       +                        if (!dry_run)
       +                                printf("%s", filename);
       +                        percent = 0;
       +                } else if (percent) {
       +                        if (dry_run) {
       +                                fprintf(stderr,
       +                                    "Warning: Unknown substitution '%c' "
       +                                    "in format string.\n", *c
       +                                );
       +                        } else {
       +                                printf("%%%c", *c);
       +                        }
       +                        percent = 0;
       +                } else {
       +                        length++;
       +                }
       +        }
       +        if (!dry_run) {
       +                if (length)
       +                        printf("%.*s", length, CMD_FORMAT + start_index);
       +                printf("\n");
       +        }
       +}
       +
       +static void
       +redraw(void) {
       +        if (!state.ctx.dirty)
       +                return;
       +        if (!state.ctx.cur_image || state.cur_selection < 0) {
       +                clear_screen(&state.ctx);
       +                swap_buffers(&state.ctx);
       +                return;
       +        }
       +
       +        /* draw the parts of the image that need to be redrawn */
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        draw_image_updates(&state.ctx, &sel->sz);
       +
       +        wipe_around_image(&state.ctx, &sel->sz);
       +
       +        /* draw the 'X' */
       +        if (sel->selected) {
       +                XSetForeground(state.ctx.dpy, state.ctx.gc, state.col.pixel);
       +                XDrawLine(
       +                    state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
       +                    0, 0, sel->sz.scaled_w, sel->sz.scaled_h
       +                );
       +                XDrawLine(
       +                    state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
       +                    0, sel->sz.scaled_h, sel->sz.scaled_w, 0
       +                );
       +        }
       +        swap_buffers(&state.ctx);
       +}
       +
       +static void
       +resize_window(int w, int h) {
       +        int actual_w, actual_h;
       +        struct Selection *sel;
       +        state.ctx.window_w = w;
       +        state.ctx.window_h = h;
       +
       +        if (state.cur_selection < 0)
       +                return;
       +        sel = &state.selections[state.cur_selection];
       +        get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
       +        if (actual_w != sel->sz.scaled_w) {
       +                sel->sz.scaled_w = actual_w;
       +                sel->sz.scaled_h = actual_h;
       +                queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
       +        }
       +}
       +
       +/* change the shown image
       + * new_selection is the index of the new selection */
       +void
       +change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
       +        (void)copy_box;
       +        int orig_w, orig_h, actual_w, actual_h;
       +        /* set window title to filename */
       +        XSetStandardProperties(
       +            state.ctx.dpy, state.ctx.win,
       +            state.filenames[new_selection],
       +            NULL, None, NULL, 0, NULL
       +        );
       +        if (state.ctx.cur_image) {
       +                imlib_context_set_image(state.ctx.cur_image);
       +                imlib_free_image();
       +        }
       +        state.ctx.cur_image = new_image;
       +        imlib_context_set_image(state.ctx.cur_image);
       +        state.cur_selection = new_selection;
       +
       +        orig_w = imlib_image_get_width();
       +        orig_h = imlib_image_get_height();
       +        get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
       +
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        sel->sz.orig_w = orig_w;
       +        sel->sz.orig_h = orig_h;
       +        sel->sz.scaled_w = actual_w;
       +        sel->sz.scaled_h = actual_h;
       +        queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
       +}
       +
       +static void
       +set_selection(char selected) {
       +        if (state.cur_selection < 0)
       +                return;
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        sel->selected = selected;
       +        queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
       +}
       +
       +static void
       +toggle_selection(void) {
       +        if (state.cur_selection < 0)
       +                return;
       +        struct Selection *sel = &state.selections[state.cur_selection];
       +        set_selection(!sel->selected);
       +}
       +
       +static int
       +key_press(XEvent event) {
       +        XWindowAttributes attrs;
       +        char buf[32];
       +        KeySym sym;
       +        XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
       +        switch (sym) {
       +        case XK_Left:
       +                last_picture(state.cur_selection, state.filenames, 0);
       +                break;
       +        case XK_Right:
       +                next_picture(state.cur_selection, state.filenames, state.num_files, 0);
       +                break;
       +        case XK_Return:
       +                set_selection(0);
       +                if (event.xkey.state & ShiftMask)
       +                        last_picture(state.cur_selection, state.filenames, 0);
       +                else
       +                        next_picture(state.cur_selection, state.filenames, state.num_files, 0);
       +                break;
       +        case XK_d:
       +                set_selection(1);
       +                next_picture(state.cur_selection, state.filenames, state.num_files, 0);
       +                break;
       +        case XK_D:
       +                set_selection(1);
       +                last_picture(state.cur_selection, state.filenames, 0);
       +                break;
       +        case XK_space:
       +                XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
       +                resize_window(attrs.width, attrs.height);
       +                /* queue update separately so it also redraws when
       +                   size didn't change */
       +                queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
       +                break;
       +        case XK_q:
       +                state.print_on_exit = 1;
       +                return 0;
       +        case XK_t:
       +                toggle_selection();
       +                break;
       +        default:
       +                break;
       +        }
       +        return 1;
       +}