Krolyxon
Commit a271e16

Branches

Refs →

nsxivmain.c

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