Krolyxon
Commit ea23115

Branches

Refs →

nsxivmain.c

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