Krolyxon
Commit fdafbf7

Branches

Refs →

nsxivmain.c

788 lines · 16.4 KB · C

1/* sxiv: main.c
2 * Copyright (c) 2011 Bert Muennich <muennich at informatik.hu-berlin.de>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 */
18
19#include <stdlib.h>
20#include <stdio.h>
21#include <string.h>
22#include <sys/select.h>
23#include <sys/stat.h>
24#include <sys/time.h>
25#include <sys/wait.h>
26#include <unistd.h>
27
28#include <X11/Xlib.h>
29#include <X11/Xutil.h>
30#include <X11/keysym.h>
31
32#include "image.h"
33#include "options.h"
34#include "thumbs.h"
35#include "util.h"
36#include "window.h"
37
38#define FNAME_CNT 1024
39#define TITLE_LEN 256
40
41typedef enum {
42 MODE_NORMAL = 0,
43 MODE_THUMBS
44} appmode_t;
45
46typedef struct {
47 KeySym ksym;
48 Bool reload;
49 const char *cmdline;
50} command_t;
51
52#define MAIN_C
53#include "config.h"
54
55void run();
56
57appmode_t mode;
58img_t img;
59tns_t tns;
60win_t win;
61
62const char **filenames;
63int filecnt, fileidx;
64size_t filesize;
65
66char win_title[TITLE_LEN];
67
68void cleanup() {
69 static int in = 0;
70
71 if (!in++) {
72 img_close(&img, 0);
73 tns_free(&tns);
74 win_close(&win);
75 }
76}
77
78void remove_file(int n, unsigned char silent) {
79 if (n < 0 || n >= filecnt)
80 return;
81
82 if (filecnt == 1) {
83 if (!silent)
84 fprintf(stderr, "sxiv: no more files to display, aborting\n");
85 cleanup();
86 exit(!silent);
87 }
88
89 if (n + 1 < filecnt)
90 memmove(filenames + n, filenames + n + 1, (filecnt - n - 1) *
91 sizeof(const char*));
92 if (n + 1 < tns.cnt) {
93 memmove(tns.thumbs + n, tns.thumbs + n + 1, (tns.cnt - n - 1) *
94 sizeof(thumb_t));
95 memset(tns.thumbs + tns.cnt - 1, 0, sizeof(thumb_t));
96 }
97
98 --filecnt;
99 if (n < tns.cnt)
100 --tns.cnt;
101}
102
103int load_image(int new) {
104 struct stat fstats;
105
106 if (new >= 0 && new < filecnt) {
107 win_set_cursor(&win, CURSOR_WATCH);
108 img_close(&img, 0);
109
110 while (!img_load(&img, filenames[new])) {
111 remove_file(new, 0);
112 if (new >= filecnt)
113 new = filecnt - 1;
114 }
115 fileidx = new;
116 if (!stat(filenames[new], &fstats))
117 filesize = fstats.st_size;
118 else
119 filesize = 0;
120
121 /* cursor is reset in redraw() */
122 }
123 return 1;
124}
125
126void update_title() {
127 int n;
128 float size;
129 const char *unit;
130
131 if (mode == MODE_THUMBS) {
132 n = snprintf(win_title, TITLE_LEN, "sxiv: [%d/%d] %s",
133 tns.cnt ? tns.sel + 1 : 0, tns.cnt,
134 tns.cnt ? filenames[tns.sel] : "");
135 } else {
136 size = filesize;
137 size_readable(&size, &unit);
138 n = snprintf(win_title, TITLE_LEN, "sxiv: [%d/%d] <%d%%> (%.2f%s) %s",
139 fileidx + 1, filecnt, (int) (img.zoom * 100.0), size, unit,
140 filenames[fileidx]);
141 }
142
143 if (n >= TITLE_LEN) {
144 win_title[TITLE_LEN - 2] = '.';
145 win_title[TITLE_LEN - 3] = '.';
146 win_title[TITLE_LEN - 4] = '.';
147 }
148
149 win_set_title(&win, win_title);
150}
151
152int check_append(const char *filename) {
153 if (!filename)
154 return 0;
155
156 if (access(filename, R_OK)) {
157 warn("could not open file: %s", filename);
158 return 0;
159 } else {
160 if (fileidx == filecnt) {
161 filecnt *= 2;
162 filenames = (const char**) s_realloc(filenames,
163 filecnt * sizeof(const char*));
164 }
165 filenames[fileidx++] = filename;
166 return 1;
167 }
168}
169
170int fncmp(const void *a, const void *b) {
171 return strcoll(*((char* const*) a), *((char* const*) b));
172}
173
174int main(int argc, char **argv) {
175 int i, start;
176 const char *filename;
177 struct stat fstats;
178 r_dir_t dir;
179
180 parse_options(argc, argv);
181
182 if (options->clean_cache) {
183 tns_init(&tns, 0);
184 tns_clean_cache(&tns);
185 exit(0);
186 }
187
188 if (!options->filecnt) {
189 print_usage();
190 exit(1);
191 }
192
193 if (options->recursive || options->from_stdin)
194 filecnt = FNAME_CNT;
195 else
196 filecnt = options->filecnt;
197
198 filenames = (const char**) s_malloc(filecnt * sizeof(const char*));
199 fileidx = 0;
200
201 if (options->from_stdin) {
202 while ((filename = readline(stdin))) {
203 if (!*filename || !check_append(filename))
204 free((void*) filename);
205 }
206 } else {
207 for (i = 0; i < options->filecnt; ++i) {
208 filename = options->filenames[i];
209
210 if (stat(filename, &fstats) || !S_ISDIR(fstats.st_mode)) {
211 check_append(filename);
212 } else {
213 if (!options->recursive) {
214 warn("ignoring directory: %s", filename);
215 continue;
216 }
217 if (r_opendir(&dir, filename)) {
218 warn("could not open directory: %s", filename);
219 continue;
220 }
221 start = fileidx;
222 while ((filename = r_readdir(&dir))) {
223 if (!check_append(filename))
224 free((void*) filename);
225 }
226 r_closedir(&dir);
227 if (fileidx - start > 1)
228 qsort(filenames + start, fileidx - start, sizeof(char*), fncmp);
229 }
230 }
231 }
232
233 filecnt = fileidx;
234 fileidx = 0;
235
236 if (!filecnt) {
237 fprintf(stderr, "sxiv: no valid image file given, aborting\n");
238 exit(1);
239 }
240
241 win_init(&win);
242 img_init(&img, &win);
243
244 if (options->thumbnails) {
245 mode = MODE_THUMBS;
246 tns_init(&tns, filecnt);
247 while (!tns_load(&tns, 0, filenames[0], 0))
248 remove_file(0, 0);
249 tns.cnt = 1;
250 } else {
251 mode = MODE_NORMAL;
252 tns.thumbs = NULL;
253 load_image(fileidx);
254 }
255
256 win_open(&win);
257
258 run();
259 cleanup();
260
261 return 0;
262}
263
264#if EXT_COMMANDS
265int run_command(const char *cline, Bool reload) {
266 int fncnt, fnlen;
267 char *cn, *cmdline;
268 const char *co, *fname;
269 pid_t pid;
270 int ret, status;
271
272 if (!cline || !*cline)
273 return 0;
274
275 fncnt = 0;
276 co = cline - 1;
277 while ((co = strchr(co + 1, '#')))
278 ++fncnt;
279
280 if (!fncnt)
281 return 0;
282
283 ret = 0;
284 fname = filenames[mode == MODE_NORMAL ? fileidx : tns.sel];
285 fnlen = strlen(fname);
286 cn = cmdline = (char*) s_malloc((strlen(cline) + fncnt * (fnlen + 2)) *
287 sizeof(char));
288
289 /* replace all '#' with filename */
290 for (co = cline; *co; ++co) {
291 if (*co == '#') {
292 *cn++ = '"';
293 strcpy(cn, fname);
294 cn += fnlen;
295 *cn++ = '"';
296 } else {
297 *cn++ = *co;
298 }
299 }
300 *cn = '\0';
301
302 if ((pid = fork()) == 0) {
303 execlp("/bin/sh", "/bin/sh", "-c", cmdline, NULL);
304 warn("could not exec: /bin/sh");
305 exit(1);
306 } else if (pid < 0) {
307 warn("could not fork. command line was: %s", cmdline);
308 } else if (reload) {
309 waitpid(pid, &status, 0);
310 if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
311 ret = 1;
312 else
313 warn("child exited with non-zero return value: %d. command line was: %s",
314 WEXITSTATUS(status), cmdline);
315 }
316
317 free(cmdline);
318 return ret;
319}
320#endif /* EXT_COMMANDS */
321
322
323/* event handling */
324
325#define TO_WIN_RESIZE 75000
326#define TO_IMAGE_DRAG 1000
327#define TO_CURSOR_HIDE 1500000
328#define TO_THUMBS_LOAD 75000
329
330int timo_cursor;
331int timo_redraw;
332unsigned char drag;
333int mox, moy;
334
335void redraw() {
336 if (mode == MODE_NORMAL) {
337 img_render(&img, &win);
338 if (timo_cursor)
339 win_set_cursor(&win, CURSOR_ARROW);
340 else if (!drag)
341 win_set_cursor(&win, CURSOR_NONE);
342 } else {
343 tns_render(&tns, &win);
344 }
345 update_title();
346 timo_redraw = 0;
347}
348
349void on_keypress(XKeyEvent *kev) {
350 int x, y;
351 unsigned int w, h;
352 char key;
353 KeySym ksym;
354 int changed;
355
356 if (!kev)
357 return;
358
359 XLookupString(kev, &key, 1, &ksym, NULL);
360 changed = 0;
361
362#if EXT_COMMANDS
363 /* external commands from commands.h */
364 if (CLEANMASK(kev->state) & ControlMask) {
365 for (x = 0; x < LEN(commands); ++x) {
366 if (commands[x].ksym == ksym) {
367 win_set_cursor(&win, CURSOR_WATCH);
368 if (run_command(commands[x].cmdline, commands[x].reload)) {
369 if (mode == MODE_NORMAL) {
370 if (fileidx < tns.cnt)
371 tns_load(&tns, fileidx, filenames[fileidx], 1);
372 img_close(&img, 1);
373 load_image(fileidx);
374 } else {
375 if (!tns_load(&tns, tns.sel, filenames[tns.sel], 0)) {
376 remove_file(tns.sel, 0);
377 tns.dirty = 1;
378 if (tns.sel >= tns.cnt)
379 tns.sel = tns.cnt - 1;
380 }
381 }
382 redraw();
383 }
384 if (mode == MODE_THUMBS)
385 win_set_cursor(&win, CURSOR_ARROW);
386 else if (!timo_cursor)
387 win_set_cursor(&win, CURSOR_NONE);
388 return;
389 }
390 }
391 }
392#endif
393
394 if (mode == MODE_NORMAL) {
395 switch (ksym) {
396 /* navigate image list */
397 case XK_n:
398 case XK_space:
399 if (fileidx + 1 < filecnt)
400 changed = load_image(fileidx + 1);
401 break;
402 case XK_p:
403 case XK_BackSpace:
404 if (fileidx > 0)
405 changed = load_image(fileidx - 1);
406 break;
407 case XK_bracketleft:
408 if (fileidx != 0)
409 changed = load_image(MAX(0, fileidx - 10));
410 break;
411 case XK_bracketright:
412 if (fileidx != filecnt - 1)
413 changed = load_image(MIN(fileidx + 10, filecnt - 1));
414 break;
415 case XK_g:
416 if (fileidx != 0)
417 changed = load_image(0);
418 break;
419 case XK_G:
420 if (fileidx != filecnt - 1)
421 changed = load_image(filecnt - 1);
422 break;
423
424 /* zooming */
425 case XK_plus:
426 case XK_equal:
427 changed = img_zoom_in(&img, &win);
428 break;
429 case XK_minus:
430 changed = img_zoom_out(&img, &win);
431 break;
432 case XK_0:
433 changed = img_zoom(&img, &win, 1.0);
434 break;
435 case XK_w:
436 if ((changed = img_fit_win(&img, &win)))
437 img_center(&img, &win);
438 break;
439
440 /* panning */
441 case XK_h:
442 case XK_Left:
443 changed = img_pan(&img, &win, PAN_LEFT);
444 break;
445 case XK_j:
446 case XK_Down:
447 changed = img_pan(&img, &win, PAN_DOWN);
448 break;
449 case XK_k:
450 case XK_Up:
451 changed = img_pan(&img, &win, PAN_UP);
452 break;
453 case XK_l:
454 case XK_Right:
455 changed = img_pan(&img, &win, PAN_RIGHT);
456 break;
457
458 /* rotation */
459 case XK_less:
460 img_rotate_left(&img, &win);
461 changed = 1;
462 break;
463 case XK_greater:
464 img_rotate_right(&img, &win);
465 changed = 1;
466 break;
467
468 /* control window */
469 case XK_W:
470 x = MAX(0, win.x + img.x);
471 y = MAX(0, win.y + img.y);
472 w = img.w * img.zoom;
473 h = img.h * img.zoom;
474 if ((changed = win_moveresize(&win, x, y, w, h))) {
475 img.x = x - win.x;
476 img.y = y - win.y;
477 }
478 break;
479
480 /* switch to thumbnail mode */
481 case XK_Return:
482 if (!tns.thumbs)
483 tns_init(&tns, filecnt);
484 img_close(&img, 0);
485 mode = MODE_THUMBS;
486 win_set_cursor(&win, CURSOR_ARROW);
487 timo_cursor = 0;
488 tns.sel = fileidx;
489 changed = tns.dirty = 1;
490 break;
491
492 /* miscellaneous */
493 case XK_a:
494 img_toggle_antialias(&img);
495 changed = 1;
496 break;
497 case XK_A:
498 img.alpha ^= 1;
499 changed = 1;
500 break;
501 case XK_D:
502 remove_file(fileidx, 1);
503 changed = load_image(fileidx >= filecnt ? filecnt - 1 : fileidx);
504 break;
505 case XK_r:
506 changed = load_image(fileidx);
507 break;
508 }
509 } else {
510 /* thumbnail mode */
511 switch (ksym) {
512 /* open selected image */
513 case XK_Return:
514 load_image(tns.sel);
515 mode = MODE_NORMAL;
516 changed = 1;
517 break;
518
519 /* move selection */
520 case XK_h:
521 case XK_Left:
522 changed = tns_move_selection(&tns, &win, TNS_LEFT);
523 break;
524 case XK_j:
525 case XK_Down:
526 changed = tns_move_selection(&tns, &win, TNS_DOWN);
527 break;
528 case XK_k:
529 case XK_Up:
530 changed = tns_move_selection(&tns, &win, TNS_UP);
531 break;
532 case XK_l:
533 case XK_Right:
534 changed = tns_move_selection(&tns, &win, TNS_RIGHT);
535 break;
536 case XK_g:
537 if (tns.sel != 0) {
538 tns.sel = 0;
539 changed = tns.dirty = 1;
540 }
541 break;
542 case XK_G:
543 if (tns.sel != tns.cnt - 1) {
544 tns.sel = tns.cnt - 1;
545 changed = tns.dirty = 1;
546 }
547 break;
548
549 /* miscellaneous */
550 case XK_D:
551 if (tns.sel < tns.cnt) {
552 remove_file(tns.sel, 1);
553 changed = tns.dirty = 1;
554 if (tns.sel >= tns.cnt)
555 tns.sel = tns.cnt - 1;
556 }
557 break;
558 }
559 }
560
561 /* common key mappings */
562 switch (ksym) {
563 case XK_q:
564 cleanup();
565 exit(0);
566 case XK_f:
567 win_toggle_fullscreen(&win);
568 if (mode == MODE_NORMAL)
569 img.checkpan = 1;
570 else
571 tns.dirty = 1;
572 timo_redraw = TO_WIN_RESIZE;
573 break;
574 }
575
576 if (changed)
577 redraw();
578}
579
580void on_buttonpress(XButtonEvent *bev) {
581 int changed, sel;
582 unsigned int mask;
583
584 if (!bev)
585 return;
586
587 mask = CLEANMASK(bev->state);
588 changed = 0;
589
590 if (mode == MODE_NORMAL) {
591 if (!drag) {
592 win_set_cursor(&win, CURSOR_ARROW);
593 timo_cursor = TO_CURSOR_HIDE;
594 }
595
596 switch (bev->button) {
597 case Button1:
598 if (fileidx + 1 < filecnt)
599 changed = load_image(fileidx + 1);
600 break;
601 case Button2:
602 mox = bev->x;
603 moy = bev->y;
604 win_set_cursor(&win, CURSOR_HAND);
605 timo_cursor = 0;
606 drag = 1;
607 break;
608 case Button3:
609 if (fileidx > 0)
610 changed = load_image(fileidx - 1);
611 break;
612 case Button4:
613 if (mask == ControlMask)
614 changed = img_zoom_in(&img, &win);
615 else if (mask == ShiftMask)
616 changed = img_pan(&img, &win, PAN_LEFT);
617 else
618 changed = img_pan(&img, &win, PAN_UP);
619 break;
620 case Button5:
621 if (mask == ControlMask)
622 changed = img_zoom_out(&img, &win);
623 else if (mask == ShiftMask)
624 changed = img_pan(&img, &win, PAN_RIGHT);
625 else
626 changed = img_pan(&img, &win, PAN_DOWN);
627 break;
628 case 6:
629 changed = img_pan(&img, &win, PAN_LEFT);
630 break;
631 case 7:
632 changed = img_pan(&img, &win, PAN_RIGHT);
633 break;
634 }
635 } else {
636 /* thumbnail mode */
637 switch (bev->button) {
638 case Button1:
639 if ((sel = tns_translate(&tns, bev->x, bev->y)) >= 0) {
640 if (sel == tns.sel) {
641 load_image(tns.sel);
642 mode = MODE_NORMAL;
643 timo_cursor = TO_CURSOR_HIDE;
644 } else {
645 tns_highlight(&tns, &win, tns.sel, False);
646 tns_highlight(&tns, &win, sel, True);
647 tns.sel = sel;
648 }
649 changed = 1;
650 break;
651 }
652 break;
653 case Button4:
654 changed = tns_scroll(&tns, TNS_UP);
655 break;
656 case Button5:
657 changed = tns_scroll(&tns, TNS_DOWN);
658 break;
659 }
660 }
661
662 if (changed)
663 redraw();
664}
665
666void on_motionnotify(XMotionEvent *mev) {
667 if (!mev)
668 return;
669
670 if (mev->x >= 0 && mev->x <= win.w && mev->y >= 0 && mev->y <= win.h) {
671 if (img_move(&img, &win, mev->x - mox, mev->y - moy))
672 timo_redraw = TO_IMAGE_DRAG;
673
674 mox = mev->x;
675 moy = mev->y;
676 }
677}
678
679void run() {
680 int xfd, timeout;
681 fd_set fds;
682 struct timeval tt, t0, t1;
683 XEvent ev;
684
685 drag = 0;
686 timo_cursor = mode == MODE_NORMAL ? TO_CURSOR_HIDE : 0;
687
688 redraw();
689
690 while (1) {
691 if (mode == MODE_THUMBS && tns.cnt < filecnt) {
692 win_set_cursor(&win, CURSOR_WATCH);
693 gettimeofday(&t0, 0);
694
695 while (tns.cnt < filecnt && !XPending(win.env.dpy)) {
696 if (tns_load(&tns, tns.cnt, filenames[tns.cnt], 0))
697 ++tns.cnt;
698 else
699 remove_file(tns.cnt, 0);
700 gettimeofday(&t1, 0);
701 if (TV_TO_DOUBLE(t1) - TV_TO_DOUBLE(t0) >= 0.25)
702 break;
703 }
704 if (tns.cnt == filecnt)
705 win_set_cursor(&win, CURSOR_ARROW);
706 if (!XPending(win.env.dpy)) {
707 redraw();
708 continue;
709 } else {
710 timo_redraw = TO_THUMBS_LOAD;
711 }
712 } else if (timo_cursor || timo_redraw) {
713 gettimeofday(&t0, 0);
714 if (timo_cursor && timo_redraw)
715 timeout = MIN(timo_cursor, timo_redraw);
716 else if (timo_cursor)
717 timeout = timo_cursor;
718 else
719 timeout = timo_redraw;
720 tt.tv_sec = timeout / 1000000;
721 tt.tv_usec = timeout % 1000000;
722 xfd = ConnectionNumber(win.env.dpy);
723 FD_ZERO(&fds);
724 FD_SET(xfd, &fds);
725
726 if (!XPending(win.env.dpy))
727 select(xfd + 1, &fds, 0, 0, &tt);
728 gettimeofday(&t1, 0);
729 timeout = MIN((TV_TO_DOUBLE(t1) - TV_TO_DOUBLE(t0)) * 1000000, timeout);
730
731 /* timeouts fired? */
732 if (timo_cursor) {
733 timo_cursor = MAX(0, timo_cursor - timeout);
734 if (!timo_cursor)
735 win_set_cursor(&win, CURSOR_NONE);
736 }
737 if (timo_redraw) {
738 timo_redraw = MAX(0, timo_redraw - timeout);
739 if (!timo_redraw)
740 redraw();
741 }
742 if (!XPending(win.env.dpy) && (timo_cursor || timo_redraw))
743 continue;
744 }
745
746 if (!XNextEvent(win.env.dpy, &ev)) {
747 switch (ev.type) {
748 case KeyPress:
749 on_keypress(&ev.xkey);
750 break;
751 case ButtonPress:
752 on_buttonpress(&ev.xbutton);
753 break;
754 case ButtonRelease:
755 if (ev.xbutton.button == Button2) {
756 drag = 0;
757 if (mode == MODE_NORMAL) {
758 win_set_cursor(&win, CURSOR_ARROW);
759 timo_cursor = TO_CURSOR_HIDE;
760 }
761 }
762 break;
763 case MotionNotify:
764 if (drag) {
765 on_motionnotify(&ev.xmotion);
766 } else if (mode == MODE_NORMAL) {
767 if (!timo_cursor)
768 win_set_cursor(&win, CURSOR_ARROW);
769 timo_cursor = TO_CURSOR_HIDE;
770 }
771 break;
772 case ConfigureNotify:
773 if (win_configure(&win, &ev.xconfigure)) {
774 timo_redraw = TO_WIN_RESIZE;
775 if (mode == MODE_NORMAL)
776 img.checkpan = 1;
777 else
778 tns.dirty = 1;
779 }
780 break;
781 case ClientMessage:
782 if ((Atom) ev.xclient.data.l[0] == wm_delete_win)
783 return;
784 break;
785 }
786 }
787 }
788}