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 }