Krolyxon
Commit bd87ae9

Branches

Refs →

nsxivmain.c

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