croptool

Image cropping tool
git clone git://lumidify.org/croptool.git (fast, but not encrypted)
git clone https://lumidify.org/croptool.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/croptool.git (over tor)
Log | Files | Refs | README | LICENSE

selectool.c (10243B)


      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: deletetool [-mrs] [-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 }