Krolyxon
Commit 00e6cd2

Branches

Refs →

nsxivmain.c

802 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#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,
139 "sxiv: [%d/%d] <%d%%> <%dx%d> (%.2f%s) %s",
140 fileidx + 1, filecnt, (int) (img.zoom * 100.0), img.w, img.h,
141 size, unit, filenames[fileidx]);
142 }
143
144 if (n >= TITLE_LEN) {
145 win_title[TITLE_LEN - 2] = '.';
146 win_title[TITLE_LEN - 3] = '.';
147 win_title[TITLE_LEN - 4] = '.';
148 }
149
150 win_set_title(&win, win_title);
151}
152
153int check_append(const char *filename) {
154 if (!filename)
155 return 0;
156
157 if (access(filename, R_OK)) {
158 warn("could not open file: %s", filename);
159 return 0;
160 } else {
161 if (fileidx == filecnt) {
162 filecnt *= 2;
163 filenames = (const char**) s_realloc(filenames,
164 filecnt * sizeof(const 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, start;
177 const char *filename;
178 struct stat fstats;
179 r_dir_t dir;
180
181 parse_options(argc, argv);
182
183 if (options->clean_cache) {
184 tns_init(&tns, 0);
185 tns_clean_cache(&tns);
186 exit(0);
187 }
188
189 if (!options->filecnt) {
190 print_usage();
191 exit(1);
192 }
193
194 if (options->recursive || options->from_stdin)
195 filecnt = FNAME_CNT;
196 else
197 filecnt = options->filecnt;
198
199 filenames = (const char**) s_malloc(filecnt * sizeof(const char*));
200 fileidx = 0;
201
202 if (options->from_stdin) {
203 while ((filename = readline(stdin))) {
204 if (!*filename || !check_append(filename))
205 free((void*) filename);
206 }
207 } else {
208 for (i = 0; i < options->filecnt; ++i) {
209 filename = options->filenames[i];
210
211 if (stat(filename, &fstats) || !S_ISDIR(fstats.st_mode)) {
212 check_append(filename);
213 } else {
214 if (!options->recursive) {
215 warn("ignoring directory: %s", filename);
216 continue;
217 }
218 if (r_opendir(&dir, filename)) {
219 warn("could not open directory: %s", filename);
220 continue;
221 }
222 start = fileidx;
223 while ((filename = r_readdir(&dir))) {
224 if (!check_append(filename))
225 free((void*) filename);
226 }
227 r_closedir(&dir);
228 if (fileidx - start > 1)
229 qsort(filenames + start, fileidx - start, sizeof(char*), fncmp);
230 }
231 }
232 }
233
234 filecnt = fileidx;
235 fileidx = 0;
236
237 if (!filecnt) {
238 fprintf(stderr, "sxiv: no valid image file given, aborting\n");
239 exit(1);
240 }
241
242 win_init(&win);
243 img_init(&img, &win);
244
245 if (options->thumbnails) {
246 mode = MODE_THUMBS;
247 tns_init(&tns, filecnt);
248 while (!tns_load(&tns, 0, filenames[0], 0))
249 remove_file(0, 0);
250 tns.cnt = 1;
251 } else {
252 mode = MODE_NORMAL;
253 tns.thumbs = NULL;
254 load_image(fileidx);
255 }
256
257 win_open(&win);
258
259 run();
260 cleanup();
261
262 return 0;
263}
264
265#if EXT_COMMANDS
266int run_command(const char *cline, Bool reload) {
267 int fncnt, fnlen;
268 char *cn, *cmdline;
269 const char *co, *fname;
270 pid_t pid;
271 int ret, status;
272
273 if (!cline || !*cline)
274 return 0;
275
276 fncnt = 0;
277 co = cline - 1;
278 while ((co = strchr(co + 1, '#')))
279 ++fncnt;
280
281 if (!fncnt)
282 return 0;
283
284 ret = 0;
285 fname = filenames[mode == MODE_NORMAL ? fileidx : tns.sel];
286 fnlen = strlen(fname);
287 cn = cmdline = (char*) s_malloc((strlen(cline) + fncnt * (fnlen + 2)) *
288 sizeof(char));
289
290 /* replace all '#' with filename */
291 for (co = cline; *co; ++co) {
292 if (*co == '#') {
293 *cn++ = '"';
294 strcpy(cn, fname);
295 cn += fnlen;
296 *cn++ = '"';
297 } else {
298 *cn++ = *co;
299 }
300 }
301 *cn = '\0';
302
303 if ((pid = fork()) == 0) {
304 execlp("/bin/sh", "/bin/sh", "-c", cmdline, NULL);
305 warn("could not exec: /bin/sh");
306 exit(1);
307 } else if (pid < 0) {
308 warn("could not fork. command line was: %s", cmdline);
309 } else if (reload) {
310 waitpid(pid, &status, 0);
311 if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
312 ret = 1;
313 else
314 warn("child exited with non-zero return value: %d. command line was: %s",
315 WEXITSTATUS(status), cmdline);
316 }
317
318 free(cmdline);
319 return ret;
320}
321#endif /* EXT_COMMANDS */
322
323
324/* event handling */
325
326#define TO_WIN_RESIZE 75000
327#define TO_IMAGE_DRAG 1000
328#define TO_CURSOR_HIDE 1500000
329#define TO_THUMBS_LOAD 75000
330
331int timo_cursor;
332int timo_redraw;
333unsigned char drag;
334int mox, moy;
335
336void redraw() {
337 if (mode == MODE_NORMAL) {
338 img_render(&img, &win);
339 if (timo_cursor)
340 win_set_cursor(&win, CURSOR_ARROW);
341 else if (!drag)
342 win_set_cursor(&win, CURSOR_NONE);
343 } else {
344 tns_render(&tns, &win);
345 }
346 update_title();
347 timo_redraw = 0;
348}
349
350void on_keypress(XKeyEvent *kev) {
351 int x, y;
352 unsigned int w, h;
353 char key;
354 KeySym ksym;
355 int changed;
356
357 if (!kev)
358 return;
359
360 XLookupString(kev, &key, 1, &ksym, NULL);
361 changed = 0;
362
363#if EXT_COMMANDS
364 /* external commands from commands.h */
365 if (CLEANMASK(kev->state) & ControlMask) {
366 for (x = 0; x < LEN(commands); ++x) {
367 if (commands[x].ksym == ksym) {
368 win_set_cursor(&win, CURSOR_WATCH);
369 if (run_command(commands[x].cmdline, commands[x].reload)) {
370 if (mode == MODE_NORMAL) {
371 if (fileidx < tns.cnt)
372 tns_load(&tns, fileidx, filenames[fileidx], 1);
373 img_close(&img, 1);
374 load_image(fileidx);
375 } else {
376 if (!tns_load(&tns, tns.sel, filenames[tns.sel], 0)) {
377 remove_file(tns.sel, 0);
378 tns.dirty = 1;
379 if (tns.sel >= tns.cnt)
380 tns.sel = tns.cnt - 1;
381 }
382 }
383 redraw();
384 }
385 if (mode == MODE_THUMBS)
386 win_set_cursor(&win, CURSOR_ARROW);
387 else if (!timo_cursor)
388 win_set_cursor(&win, CURSOR_NONE);
389 return;
390 }
391 }
392 }
393#endif
394
395 if (mode == MODE_NORMAL) {
396 switch (ksym) {
397 /* navigate image list */
398 case XK_n:
399 case XK_space:
400 if (fileidx + 1 < filecnt)
401 changed = load_image(fileidx + 1);
402 break;
403 case XK_p:
404 case XK_BackSpace:
405 if (fileidx > 0)
406 changed = load_image(fileidx - 1);
407 break;
408 case XK_bracketleft:
409 if (fileidx != 0)
410 changed = load_image(MAX(0, fileidx - 10));
411 break;
412 case XK_bracketright:
413 if (fileidx != filecnt - 1)
414 changed = load_image(MIN(fileidx + 10, filecnt - 1));
415 break;
416 case XK_g:
417 if (fileidx != 0)
418 changed = load_image(0);
419 break;
420 case XK_G:
421 if (fileidx != filecnt - 1)
422 changed = load_image(filecnt - 1);
423 break;
424
425 /* zooming */
426 case XK_plus:
427 case XK_equal:
428 changed = img_zoom_in(&img, &win);
429 break;
430 case XK_minus:
431 changed = img_zoom_out(&img, &win);
432 break;
433 case XK_0:
434 changed = img_zoom(&img, &win, 1.0);
435 break;
436 case XK_w:
437 if ((changed = img_fit_win(&img, &win)))
438 img_center(&img, &win);
439 break;
440
441 /* panning */
442 case XK_h:
443 case XK_Left:
444 changed = img_pan(&img, &win, PAN_LEFT);
445 break;
446 case XK_j:
447 case XK_Down:
448 changed = img_pan(&img, &win, PAN_DOWN);
449 break;
450 case XK_k:
451 case XK_Up:
452 changed = img_pan(&img, &win, PAN_UP);
453 break;
454 case XK_l:
455 case XK_Right:
456 changed = img_pan(&img, &win, PAN_RIGHT);
457 break;
458
459 case XK_H:
460 changed = img_pan_edge(&img, &win, PAN_LEFT);
461 break;
462 case XK_J:
463 changed = img_pan_edge(&img, &win, PAN_DOWN);
464 break;
465 case XK_K:
466 changed = img_pan_edge(&img, &win, PAN_UP);
467 break;
468 case XK_L:
469 changed = img_pan_edge(&img, &win, PAN_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, TNS_LEFT);
537 break;
538 case XK_j:
539 case XK_Down:
540 changed = tns_move_selection(&tns, &win, TNS_DOWN);
541 break;
542 case XK_k:
543 case XK_Up:
544 changed = tns_move_selection(&tns, &win, TNS_UP);
545 break;
546 case XK_l:
547 case XK_Right:
548 changed = tns_move_selection(&tns, &win, TNS_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, PAN_LEFT);
631 else
632 changed = img_pan(&img, &win, PAN_UP);
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, PAN_RIGHT);
639 else
640 changed = img_pan(&img, &win, PAN_DOWN);
641 break;
642 case 6:
643 changed = img_pan(&img, &win, PAN_LEFT);
644 break;
645 case 7:
646 changed = img_pan(&img, &win, PAN_RIGHT);
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, TNS_UP);
669 break;
670 case Button5:
671 changed = tns_scroll(&tns, TNS_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 (TV_TO_DOUBLE(t1) - TV_TO_DOUBLE(t0) >= 0.25)
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 tt.tv_sec = timeout / 1000000;
735 tt.tv_usec = timeout % 1000000;
736 xfd = ConnectionNumber(win.env.dpy);
737 FD_ZERO(&fds);
738 FD_SET(xfd, &fds);
739
740 if (!XPending(win.env.dpy))
741 select(xfd + 1, &fds, 0, 0, &tt);
742 gettimeofday(&t1, 0);
743 timeout = MIN((TV_TO_DOUBLE(t1) - TV_TO_DOUBLE(t0)) * 1000000, timeout);
744
745 /* timeouts fired? */
746 if (timo_cursor) {
747 timo_cursor = MAX(0, timo_cursor - timeout);
748 if (!timo_cursor)
749 win_set_cursor(&win, CURSOR_NONE);
750 }
751 if (timo_redraw) {
752 timo_redraw = MAX(0, timo_redraw - timeout);
753 if (!timo_redraw)
754 redraw();
755 }
756 if (!XPending(win.env.dpy) && (timo_cursor || timo_redraw))
757 continue;
758 }
759
760 if (!XNextEvent(win.env.dpy, &ev)) {
761 switch (ev.type) {
762 case KeyPress:
763 on_keypress(&ev.xkey);
764 break;
765 case ButtonPress:
766 on_buttonpress(&ev.xbutton);
767 break;
768 case ButtonRelease:
769 if (ev.xbutton.button == Button2) {
770 drag = 0;
771 if (mode == MODE_NORMAL) {
772 win_set_cursor(&win, CURSOR_ARROW);
773 timo_cursor = TO_CURSOR_HIDE;
774 }
775 }
776 break;
777 case MotionNotify:
778 if (drag) {
779 on_motionnotify(&ev.xmotion);
780 } else if (mode == MODE_NORMAL) {
781 if (!timo_cursor)
782 win_set_cursor(&win, CURSOR_ARROW);
783 timo_cursor = TO_CURSOR_HIDE;
784 }
785 break;
786 case ConfigureNotify:
787 if (win_configure(&win, &ev.xconfigure)) {
788 timo_redraw = TO_WIN_RESIZE;
789 if (mode == MODE_NORMAL)
790 img.checkpan = 1;
791 else
792 tns.dirty = 1;
793 }
794 break;
795 case ClientMessage:
796 if ((Atom) ev.xclient.data.l[0] == wm_delete_win)
797 return;
798 break;
799 }
800 }
801 }
802}