Krolyxon
Commit ecd5e71

Branches

Refs →

nsxivmain.c

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