Krolyxon
Commit 87a9828

Branches

Refs →

nsxivmain.c

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