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 }