Krolyxon
Commit 0a171a9

Branches

Refs →

nsxivmain.c

816 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 case XK_KP_Add:
435 changed = img_zoom_in(&img, &win);
436 break;
437 case XK_minus:
438 case XK_KP_Subtract:
439 changed = img_zoom_out(&img, &win);
440 break;
441 case XK_0:
442 case XK_KP_0:
443 changed = img_zoom(&img, &win, 1.0);
444 break;
445 case XK_w:
446 if ((changed = img_fit_win(&img, &win)))
447 img_center(&img, &win);
448 break;
449
450 /* panning */
451 case XK_h:
452 case XK_Left:
453 changed = img_pan(&img, &win, PAN_LEFT, ctrl);
454 break;
455 case XK_j:
456 case XK_Down:
457 changed = img_pan(&img, &win, PAN_DOWN, ctrl);
458 break;
459 case XK_k:
460 case XK_Up:
461 changed = img_pan(&img, &win, PAN_UP, ctrl);
462 break;
463 case XK_l:
464 case XK_Right:
465 changed = img_pan(&img, &win, PAN_RIGHT, ctrl);
466 break;
467 case XK_Prior:
468 changed = img_pan(&img, &win, PAN_UP, 1);
469 break;
470 case XK_Next:
471 changed = img_pan(&img, &win, PAN_DOWN, 1);
472 break;
473
474 case XK_H:
475 changed = img_pan_edge(&img, &win, PAN_LEFT);
476 break;
477 case XK_J:
478 changed = img_pan_edge(&img, &win, PAN_DOWN);
479 break;
480 case XK_K:
481 changed = img_pan_edge(&img, &win, PAN_UP);
482 break;
483 case XK_L:
484 changed = img_pan_edge(&img, &win, PAN_RIGHT);
485 break;
486
487 /* rotation */
488 case XK_less:
489 img_rotate_left(&img, &win);
490 changed = 1;
491 break;
492 case XK_greater:
493 img_rotate_right(&img, &win);
494 changed = 1;
495 break;
496
497 /* control window */
498 case XK_W:
499 x = MAX(0, win.x + img.x);
500 y = MAX(0, win.y + img.y);
501 w = img.w * img.zoom;
502 h = img.h * img.zoom;
503 if ((changed = win_moveresize(&win, x, y, w, h))) {
504 img.x = x - win.x;
505 img.y = y - win.y;
506 }
507 break;
508
509 /* switch to thumbnail mode */
510 case XK_Return:
511 if (!tns.thumbs)
512 tns_init(&tns, filecnt);
513 img_close(&img, 0);
514 mode = MODE_THUMBS;
515 win_set_cursor(&win, CURSOR_ARROW);
516 timo_cursor = 0;
517 tns.sel = fileidx;
518 changed = tns.dirty = 1;
519 break;
520
521 /* miscellaneous */
522 case XK_a:
523 img_toggle_antialias(&img);
524 changed = 1;
525 break;
526 case XK_A:
527 img.alpha ^= 1;
528 changed = 1;
529 break;
530 case XK_D:
531 remove_file(fileidx, 1);
532 changed = load_image(fileidx >= filecnt ? filecnt - 1 : fileidx);
533 break;
534 case XK_r:
535 changed = load_image(fileidx);
536 break;
537 }
538 } else {
539 /* thumbnail mode */
540 switch (ksym) {
541 /* open selected image */
542 case XK_Return:
543 load_image(tns.sel);
544 mode = MODE_NORMAL;
545 changed = 1;
546 break;
547
548 /* move selection */
549 case XK_h:
550 case XK_Left:
551 changed = tns_move_selection(&tns, &win, TNS_LEFT);
552 break;
553 case XK_j:
554 case XK_Down:
555 changed = tns_move_selection(&tns, &win, TNS_DOWN);
556 break;
557 case XK_k:
558 case XK_Up:
559 changed = tns_move_selection(&tns, &win, TNS_UP);
560 break;
561 case XK_l:
562 case XK_Right:
563 changed = tns_move_selection(&tns, &win, TNS_RIGHT);
564 break;
565 case XK_g:
566 if (tns.sel != 0) {
567 tns.sel = 0;
568 changed = tns.dirty = 1;
569 }
570 break;
571 case XK_G:
572 if (tns.sel != tns.cnt - 1) {
573 tns.sel = tns.cnt - 1;
574 changed = tns.dirty = 1;
575 }
576 break;
577
578 /* miscellaneous */
579 case XK_D:
580 if (tns.sel < tns.cnt) {
581 remove_file(tns.sel, 1);
582 changed = tns.dirty = 1;
583 if (tns.sel >= tns.cnt)
584 tns.sel = tns.cnt - 1;
585 }
586 break;
587 }
588 }
589
590 /* common key mappings */
591 switch (ksym) {
592 case XK_q:
593 cleanup();
594 exit(0);
595 case XK_f:
596 win_toggle_fullscreen(&win);
597 if (mode == MODE_NORMAL)
598 img.checkpan = 1;
599 else
600 tns.dirty = 1;
601 timo_redraw = TO_WIN_RESIZE;
602 break;
603 }
604
605 if (changed)
606 redraw();
607}
608
609void on_buttonpress(XButtonEvent *bev) {
610 int changed, sel;
611 unsigned int mask;
612
613 if (!bev)
614 return;
615
616 mask = CLEANMASK(bev->state);
617 changed = 0;
618
619 if (mode == MODE_NORMAL) {
620 if (!drag) {
621 win_set_cursor(&win, CURSOR_ARROW);
622 timo_cursor = TO_CURSOR_HIDE;
623 }
624
625 switch (bev->button) {
626 case Button1:
627 if (fileidx + 1 < filecnt)
628 changed = load_image(fileidx + 1);
629 break;
630 case Button2:
631 mox = bev->x;
632 moy = bev->y;
633 win_set_cursor(&win, CURSOR_HAND);
634 timo_cursor = 0;
635 drag = 1;
636 break;
637 case Button3:
638 if (fileidx > 0)
639 changed = load_image(fileidx - 1);
640 break;
641 case Button4:
642 if (mask == ControlMask)
643 changed = img_zoom_in(&img, &win);
644 else if (mask == ShiftMask)
645 changed = img_pan(&img, &win, PAN_LEFT, 0);
646 else
647 changed = img_pan(&img, &win, PAN_UP, 0);
648 break;
649 case Button5:
650 if (mask == ControlMask)
651 changed = img_zoom_out(&img, &win);
652 else if (mask == ShiftMask)
653 changed = img_pan(&img, &win, PAN_RIGHT, 0);
654 else
655 changed = img_pan(&img, &win, PAN_DOWN, 0);
656 break;
657 case 6:
658 changed = img_pan(&img, &win, PAN_LEFT, 0);
659 break;
660 case 7:
661 changed = img_pan(&img, &win, PAN_RIGHT, 0);
662 break;
663 }
664 } else {
665 /* thumbnail mode */
666 switch (bev->button) {
667 case Button1:
668 if ((sel = tns_translate(&tns, bev->x, bev->y)) >= 0) {
669 if (sel == tns.sel) {
670 load_image(tns.sel);
671 mode = MODE_NORMAL;
672 timo_cursor = TO_CURSOR_HIDE;
673 } else {
674 tns_highlight(&tns, &win, tns.sel, False);
675 tns_highlight(&tns, &win, sel, True);
676 tns.sel = sel;
677 }
678 changed = 1;
679 break;
680 }
681 break;
682 case Button4:
683 changed = tns_scroll(&tns, TNS_UP);
684 break;
685 case Button5:
686 changed = tns_scroll(&tns, TNS_DOWN);
687 break;
688 }
689 }
690
691 if (changed)
692 redraw();
693}
694
695void on_motionnotify(XMotionEvent *mev) {
696 if (!mev)
697 return;
698
699 if (mev->x >= 0 && mev->x <= win.w && mev->y >= 0 && mev->y <= win.h) {
700 if (img_move(&img, &win, mev->x - mox, mev->y - moy))
701 timo_redraw = TO_IMAGE_DRAG;
702
703 mox = mev->x;
704 moy = mev->y;
705 }
706}
707
708void run() {
709 int xfd, timeout;
710 fd_set fds;
711 struct timeval tt, t0, t1;
712 XEvent ev;
713
714 drag = 0;
715 timo_cursor = mode == MODE_NORMAL ? TO_CURSOR_HIDE : 0;
716
717 redraw();
718
719 while (1) {
720 if (mode == MODE_THUMBS && tns.cnt < filecnt) {
721 win_set_cursor(&win, CURSOR_WATCH);
722 gettimeofday(&t0, 0);
723
724 while (tns.cnt < filecnt && !XPending(win.env.dpy)) {
725 if (tns_load(&tns, tns.cnt, filenames[tns.cnt], 0))
726 ++tns.cnt;
727 else
728 remove_file(tns.cnt, 0);
729 gettimeofday(&t1, 0);
730 if (TIMEDIFF(&t1, &t0) >= TO_THUMBS_LOAD)
731 break;
732 }
733 if (tns.cnt == filecnt)
734 win_set_cursor(&win, CURSOR_ARROW);
735 if (!XPending(win.env.dpy)) {
736 redraw();
737 continue;
738 } else {
739 timo_redraw = TO_THUMBS_LOAD;
740 }
741 } else if (timo_cursor || timo_redraw) {
742 gettimeofday(&t0, 0);
743 if (timo_cursor && timo_redraw)
744 timeout = MIN(timo_cursor, timo_redraw);
745 else if (timo_cursor)
746 timeout = timo_cursor;
747 else
748 timeout = timo_redraw;
749 MSEC_TO_TIMEVAL(timeout, &tt);
750 xfd = ConnectionNumber(win.env.dpy);
751 FD_ZERO(&fds);
752 FD_SET(xfd, &fds);
753
754 if (!XPending(win.env.dpy))
755 select(xfd + 1, &fds, 0, 0, &tt);
756 gettimeofday(&t1, 0);
757 timeout = MIN(TIMEDIFF(&t1, &t0), timeout);
758
759 /* timeouts fired? */
760 if (timo_cursor) {
761 timo_cursor = MAX(0, timo_cursor - timeout);
762 if (!timo_cursor)
763 win_set_cursor(&win, CURSOR_NONE);
764 }
765 if (timo_redraw) {
766 timo_redraw = MAX(0, timo_redraw - timeout);
767 if (!timo_redraw)
768 redraw();
769 }
770 if (!XPending(win.env.dpy) && (timo_cursor || timo_redraw))
771 continue;
772 }
773
774 if (!XNextEvent(win.env.dpy, &ev)) {
775 switch (ev.type) {
776 case KeyPress:
777 on_keypress(&ev.xkey);
778 break;
779 case ButtonPress:
780 on_buttonpress(&ev.xbutton);
781 break;
782 case ButtonRelease:
783 if (ev.xbutton.button == Button2) {
784 drag = 0;
785 if (mode == MODE_NORMAL) {
786 win_set_cursor(&win, CURSOR_ARROW);
787 timo_cursor = TO_CURSOR_HIDE;
788 }
789 }
790 break;
791 case MotionNotify:
792 if (drag) {
793 on_motionnotify(&ev.xmotion);
794 } else if (mode == MODE_NORMAL) {
795 if (!timo_cursor)
796 win_set_cursor(&win, CURSOR_ARROW);
797 timo_cursor = TO_CURSOR_HIDE;
798 }
799 break;
800 case ConfigureNotify:
801 if (win_configure(&win, &ev.xconfigure)) {
802 timo_redraw = TO_WIN_RESIZE;
803 if (mode == MODE_NORMAL)
804 img.checkpan = 1;
805 else
806 tns.dirty = 1;
807 }
808 break;
809 case ClientMessage:
810 if ((Atom) ev.xclient.data.l[0] == wm_delete_win)
811 return;
812 break;
813 }
814 }
815 }
816}