URI:
       selectool.c - croptool - Image cropping tool
  HTML git clone git://lumidify.org/croptool.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/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
       ---
       selectool.c (10241B)
       ---
            1 /*
            2  * Copyright (c) 2024 lumidify <nobody@lumidify.org>
            3  *
            4  * Permission to use, copy, modify, and/or distribute this software for any
            5  * purpose with or without fee is hereby granted, provided that the above
            6  * copyright notice and this permission notice appear in all copies.
            7  *
            8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
            9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
           10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
           11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
           12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
           13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
           14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
           15  */
           16 
           17 #include <stdio.h>
           18 #include <stdlib.h>
           19 #include <unistd.h>
           20 
           21 #include <X11/X.h>
           22 #include <X11/Xlib.h>
           23 #include <X11/Xutil.h>
           24 #include <X11/keysym.h>
           25 
           26 #include <Imlib2.h>
           27 
           28 #include "common.h"
           29 
           30 /* Whether to select all images by default */
           31 static int SELECT_DEFAULT = 0;
           32 /* The color of the selection box */
           33 static const char *SELECTION_COLOR = "#FF0000";
           34 /* The width of the selection line */
           35 static int LINE_WIDTH = 5;
           36 /* When set to 1, the display is redrawn on window resize */
           37 static short RESIZE_REDRAW = 1;
           38 /*
           39   The command printed for each image.
           40   %f: Filename of image.
           41 */
           42 static const char *CMD_FORMAT = "rm -- '%f'";
           43 /* Size of Imlib2 in-memory cache in MiB */
           44 static int CACHE_SIZE = 4;
           45 
           46 extern char *optarg;
           47 extern int optind;
           48 
           49 struct Selection {
           50         ImageSize sz;
           51         char selected;
           52 };
           53 
           54 static struct {
           55         GraphicsContext ctx;
           56 
           57         struct Selection *selections;
           58         char **filenames;
           59         int cur_selection;
           60         int num_files;
           61         int cursor_x;
           62         int cursor_y;
           63         XColor col;
           64         char print_on_exit;
           65 } state;
           66 
           67 static void usage(void);
           68 static void mainloop(void);
           69 static void setup(int argc, char *argv[]);
           70 static void redraw(void);
           71 static void set_selection(char selected);
           72 static void toggle_selection(void);
           73 static void resize_window(int w, int h);
           74 static int key_press(XEvent event);
           75 static void queue_update(int x, int y, int w, int h);
           76 static void print_cmd(const char *filename, int dry_run);
           77 
           78 static void
           79 usage(void) {
           80         fprintf(stderr, "USAGE: selectool [-ms] [-f format] "
           81             "[-w width] [-c color] "
           82             "[-z size] file ...\n");
           83 }
           84 
           85 int
           86 main(int argc, char *argv[]) {
           87         char c;
           88 
           89         while ((c = getopt(argc, argv, "f:w:c:msz:")) != -1) {
           90                 switch (c) {
           91                 case 'f':
           92                         CMD_FORMAT = optarg;
           93                         break;
           94                 case 'm':
           95                         RESIZE_REDRAW = 0;
           96                         break;
           97                 case 'c':
           98                         SELECTION_COLOR = optarg;
           99                         break;
          100                 case 'w':
          101                         if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
          102                                 fprintf(stderr, "Invalid line width.\n");
          103                                 exit(1);
          104                         }
          105                         break;
          106                 case 'z':
          107                         if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
          108                                 fprintf(stderr, "Invalid cache size.\n");
          109                                 exit(1);
          110                         }
          111                         break;
          112                 case 's':
          113                         SELECT_DEFAULT = 1;
          114                         break;
          115                 default:
          116                         usage();
          117                         exit(1);
          118                         break;
          119                 }
          120         }
          121 
          122         /* print warning if command format is invalid */
          123         print_cmd("", 1);
          124 
          125         argc -= optind;
          126         argv += optind;
          127         if (argc < 1) {
          128                 usage();
          129                 exit(1);
          130         }
          131         setup(argc, argv);
          132 
          133         mainloop();
          134 
          135         if (state.print_on_exit) {
          136                 for (int i = 0; i < argc; i++) {
          137                         if (state.selections[i].selected)
          138                                 print_cmd(state.filenames[i], 0);
          139                 }
          140         }
          141 
          142         cleanup();
          143 
          144         return 0;
          145 }
          146 
          147 static void
          148 mainloop(void) {
          149         XEvent event;
          150         int running = 1;
          151 
          152         while (running) {
          153                 do {
          154                         XNextEvent(state.ctx.dpy, &event);
          155                         switch (event.type) {
          156                         case Expose:
          157                                 if (RESIZE_REDRAW)
          158                                         queue_update(event.xexpose.x, event.xexpose.y,
          159                                             event.xexpose.width, event.xexpose.height);
          160                                 break;
          161                         case ConfigureNotify:
          162                                 if (RESIZE_REDRAW)
          163                                         resize_window(
          164                                             event.xconfigure.width,
          165                                             event.xconfigure.height
          166                                         );
          167                                 break;
          168                         case KeyPress:
          169                                 running = key_press(event);
          170                                 break;
          171                         case ClientMessage:
          172                                 if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
          173                                         running = 0;
          174                         default:
          175                                 break;
          176                         }
          177                 } while (XPending(state.ctx.dpy));
          178 
          179                 redraw();
          180         }
          181 }
          182 
          183 static void
          184 setup(int argc, char *argv[]) {
          185         state.selections = malloc(argc * sizeof(struct Selection));
          186         if (!state.selections) {
          187                 fprintf(stderr, "Unable to allocate memory.\n");
          188                 exit(1);
          189         }
          190         state.num_files = argc;
          191         state.filenames = argv;
          192         state.cur_selection = -1;
          193         state.print_on_exit = 0;
          194 
          195         for (int i = 0; i < argc; i++) {
          196                 state.selections[i].selected = SELECT_DEFAULT;
          197         }
          198 
          199         setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
          200 
          201         if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR, &state.col)) {
          202                 fprintf(stderr, "Selection color invalid.\n");
          203                 exit(1);
          204         }
          205         XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col);
          206 
          207         next_picture(state.cur_selection, state.filenames, state.num_files, 0);
          208         /* Only map window here so the program exits immediately if
          209            there are no loadable images, without first opening the
          210            window and closing it again immediately */
          211         XMapWindow(state.ctx.dpy, state.ctx.win);
          212         redraw();
          213 }
          214 
          215 void
          216 cleanup(void) {
          217         free(state.selections);
          218         cleanup_x(&state.ctx);
          219 }
          220 
          221 /* queue a part of the image for redrawing */
          222 static void
          223 queue_update(int x, int y, int w, int h) {
          224         if (state.cur_selection < 0)
          225                 return;
          226         struct Selection *sel = &state.selections[state.cur_selection];
          227         queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
          228 }
          229 
          230 /* TODO: Escape filename properly
          231  * -> But how? Since the format can be set by the user,
          232  * it isn't really clear *what* needs to be escaped. */
          233 static void
          234 print_cmd(const char *filename, int dry_run) {
          235         short percent = 0;
          236         const char *c;
          237         int length = 0;
          238         int start_index = 0;
          239         /* FIXME: just use putc instead of this complex printf dance */
          240         for (c = CMD_FORMAT; *c != '\0'; c++) {
          241                 if (percent)
          242                         start_index++;
          243                 if (*c == '%') {
          244                         if (length) {
          245                                 if (!dry_run)
          246                                         printf("%.*s", length, CMD_FORMAT + start_index);
          247                                 start_index += length;
          248                                 length = 0;
          249                         }
          250                         if (percent && !dry_run)
          251                                 printf("%%");
          252                         percent++;
          253                         percent %= 2;
          254                         start_index++;
          255                 } else if (percent && *c == 'f') {
          256                         if (!dry_run)
          257                                 printf("%s", filename);
          258                         percent = 0;
          259                 } else if (percent) {
          260                         if (dry_run) {
          261                                 fprintf(stderr,
          262                                     "Warning: Unknown substitution '%c' "
          263                                     "in format string.\n", *c
          264                                 );
          265                         } else {
          266                                 printf("%%%c", *c);
          267                         }
          268                         percent = 0;
          269                 } else {
          270                         length++;
          271                 }
          272         }
          273         if (!dry_run) {
          274                 if (length)
          275                         printf("%.*s", length, CMD_FORMAT + start_index);
          276                 printf("\n");
          277         }
          278 }
          279 
          280 static void
          281 redraw(void) {
          282         if (!state.ctx.dirty)
          283                 return;
          284         if (!state.ctx.cur_image || state.cur_selection < 0) {
          285                 clear_screen(&state.ctx);
          286                 swap_buffers(&state.ctx);
          287                 return;
          288         }
          289 
          290         /* draw the parts of the image that need to be redrawn */
          291         struct Selection *sel = &state.selections[state.cur_selection];
          292         draw_image_updates(&state.ctx, &sel->sz);
          293 
          294         wipe_around_image(&state.ctx, &sel->sz);
          295 
          296         /* draw the 'X' */
          297         if (sel->selected) {
          298                 XSetForeground(state.ctx.dpy, state.ctx.gc, state.col.pixel);
          299                 XDrawLine(
          300                     state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
          301                     0, 0, sel->sz.scaled_w, sel->sz.scaled_h
          302                 );
          303                 XDrawLine(
          304                     state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
          305                     0, sel->sz.scaled_h, sel->sz.scaled_w, 0
          306                 );
          307         }
          308         swap_buffers(&state.ctx);
          309 }
          310 
          311 static void
          312 resize_window(int w, int h) {
          313         int actual_w, actual_h;
          314         struct Selection *sel;
          315         state.ctx.window_w = w;
          316         state.ctx.window_h = h;
          317 
          318         if (state.cur_selection < 0)
          319                 return;
          320         sel = &state.selections[state.cur_selection];
          321         get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
          322         if (actual_w != sel->sz.scaled_w) {
          323                 sel->sz.scaled_w = actual_w;
          324                 sel->sz.scaled_h = actual_h;
          325                 queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
          326         }
          327 }
          328 
          329 /* change the shown image
          330  * new_selection is the index of the new selection */
          331 void
          332 change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
          333         (void)copy_box;
          334         int orig_w, orig_h, actual_w, actual_h;
          335         /* set window title to filename */
          336         XSetStandardProperties(
          337             state.ctx.dpy, state.ctx.win,
          338             state.filenames[new_selection],
          339             NULL, None, NULL, 0, NULL
          340         );
          341         if (state.ctx.cur_image) {
          342                 imlib_context_set_image(state.ctx.cur_image);
          343                 imlib_free_image();
          344         }
          345         state.ctx.cur_image = new_image;
          346         imlib_context_set_image(state.ctx.cur_image);
          347         state.cur_selection = new_selection;
          348 
          349         orig_w = imlib_image_get_width();
          350         orig_h = imlib_image_get_height();
          351         get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
          352 
          353         struct Selection *sel = &state.selections[state.cur_selection];
          354         sel->sz.orig_w = orig_w;
          355         sel->sz.orig_h = orig_h;
          356         sel->sz.scaled_w = actual_w;
          357         sel->sz.scaled_h = actual_h;
          358         queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
          359 }
          360 
          361 static void
          362 set_selection(char selected) {
          363         if (state.cur_selection < 0)
          364                 return;
          365         struct Selection *sel = &state.selections[state.cur_selection];
          366         sel->selected = selected;
          367         queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
          368 }
          369 
          370 static void
          371 toggle_selection(void) {
          372         if (state.cur_selection < 0)
          373                 return;
          374         struct Selection *sel = &state.selections[state.cur_selection];
          375         set_selection(!sel->selected);
          376 }
          377 
          378 static int
          379 key_press(XEvent event) {
          380         XWindowAttributes attrs;
          381         char buf[32];
          382         KeySym sym;
          383         XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
          384         switch (sym) {
          385         case XK_Left:
          386                 last_picture(state.cur_selection, state.filenames, 0);
          387                 break;
          388         case XK_Right:
          389                 next_picture(state.cur_selection, state.filenames, state.num_files, 0);
          390                 break;
          391         case XK_Return:
          392                 set_selection(0);
          393                 if (event.xkey.state & ShiftMask)
          394                         last_picture(state.cur_selection, state.filenames, 0);
          395                 else
          396                         next_picture(state.cur_selection, state.filenames, state.num_files, 0);
          397                 break;
          398         case XK_d:
          399                 set_selection(1);
          400                 next_picture(state.cur_selection, state.filenames, state.num_files, 0);
          401                 break;
          402         case XK_D:
          403                 set_selection(1);
          404                 last_picture(state.cur_selection, state.filenames, 0);
          405                 break;
          406         case XK_space:
          407                 XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
          408                 resize_window(attrs.width, attrs.height);
          409                 /* queue update separately so it also redraws when
          410                    size didn't change */
          411                 queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
          412                 break;
          413         case XK_q:
          414                 state.print_on_exit = 1;
          415                 return 0;
          416         case XK_t:
          417                 toggle_selection();
          418                 break;
          419         default:
          420                 break;
          421         }
          422         return 1;
          423 }