A Simple X Image Viewer
 
 
 
 
 
 

711 rindas
15 KiB

  1. /* Copyright 2011-2013 Bert Muennich
  2. *
  3. * This file is part of sxiv.
  4. *
  5. * sxiv is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published
  7. * by the Free Software Foundation; either version 2 of the License,
  8. * or (at your option) any later version.
  9. *
  10. * sxiv is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with sxiv. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. #define _POSIX_C_SOURCE 200112L
  19. #define _MAPPINGS_CONFIG
  20. #include <stdlib.h>
  21. #include <stdio.h>
  22. #include <string.h>
  23. #include <fcntl.h>
  24. #include <unistd.h>
  25. #include <errno.h>
  26. #include <signal.h>
  27. #include <sys/select.h>
  28. #include <sys/stat.h>
  29. #include <sys/time.h>
  30. #include <sys/wait.h>
  31. #include <X11/keysym.h>
  32. #include "types.h"
  33. #include "commands.h"
  34. #include "image.h"
  35. #include "options.h"
  36. #include "thumbs.h"
  37. #include "util.h"
  38. #include "window.h"
  39. #include "config.h"
  40. enum {
  41. FILENAME_CNT = 1024,
  42. TITLE_LEN = 256
  43. };
  44. typedef struct {
  45. struct timeval when;
  46. bool active;
  47. timeout_f handler;
  48. } timeout_t;
  49. /* timeout handler functions: */
  50. void redraw(void);
  51. void reset_cursor(void);
  52. void animate(void);
  53. void clear_resize(void);
  54. appmode_t mode;
  55. img_t img;
  56. tns_t tns;
  57. win_t win;
  58. fileinfo_t *files;
  59. int filecnt, fileidx;
  60. int alternate;
  61. int prefix;
  62. bool resized = false;
  63. const char * const INFO_SCRIPT = ".sxiv/exec/image-info";
  64. struct {
  65. char *script;
  66. int fd;
  67. unsigned int i, lastsep;
  68. } info;
  69. timeout_t timeouts[] = {
  70. { { 0, 0 }, false, redraw },
  71. { { 0, 0 }, false, reset_cursor },
  72. { { 0, 0 }, false, animate },
  73. { { 0, 0 }, false, clear_resize },
  74. };
  75. void cleanup(void)
  76. {
  77. static bool in = false;
  78. if (!in) {
  79. in = true;
  80. img_close(&img, false);
  81. tns_free(&tns);
  82. win_close(&win);
  83. }
  84. }
  85. void check_add_file(char *filename)
  86. {
  87. const char *bn;
  88. if (filename == NULL || *filename == '\0')
  89. return;
  90. if (access(filename, R_OK) < 0) {
  91. warn("could not open file: %s", filename);
  92. return;
  93. }
  94. if (fileidx == filecnt) {
  95. filecnt *= 2;
  96. files = (fileinfo_t*) s_realloc(files, filecnt * sizeof(fileinfo_t));
  97. }
  98. if (*filename != '/') {
  99. files[fileidx].path = absolute_path(filename);
  100. if (files[fileidx].path == NULL) {
  101. warn("could not get absolute path of file: %s\n", filename);
  102. return;
  103. }
  104. }
  105. files[fileidx].loaded = false;
  106. files[fileidx].name = s_strdup(filename);
  107. if (*filename == '/')
  108. files[fileidx].path = files[fileidx].name;
  109. if ((bn = strrchr(files[fileidx].name , '/')) != NULL && bn[1] != '\0')
  110. files[fileidx].base = ++bn;
  111. else
  112. files[fileidx].base = files[fileidx].name;
  113. fileidx++;
  114. }
  115. void remove_file(int n, bool manual)
  116. {
  117. if (n < 0 || n >= filecnt)
  118. return;
  119. if (filecnt == 1) {
  120. if (!manual)
  121. fprintf(stderr, "sxiv: no more files to display, aborting\n");
  122. cleanup();
  123. exit(manual ? EXIT_SUCCESS : EXIT_FAILURE);
  124. }
  125. if (files[n].path != files[n].name)
  126. free((void*) files[n].path);
  127. free((void*) files[n].name);
  128. if (n + 1 < filecnt)
  129. memmove(files + n, files + n + 1, (filecnt - n - 1) * sizeof(fileinfo_t));
  130. if (n + 1 < tns.cnt) {
  131. memmove(tns.thumbs + n, tns.thumbs + n + 1, (tns.cnt - n - 1) *
  132. sizeof(thumb_t));
  133. memset(tns.thumbs + tns.cnt - 1, 0, sizeof(thumb_t));
  134. }
  135. filecnt--;
  136. if (n < tns.cnt)
  137. tns.cnt--;
  138. }
  139. void set_timeout(timeout_f handler, int time, bool overwrite)
  140. {
  141. int i;
  142. for (i = 0; i < ARRLEN(timeouts); i++) {
  143. if (timeouts[i].handler == handler) {
  144. if (!timeouts[i].active || overwrite) {
  145. gettimeofday(&timeouts[i].when, 0);
  146. TV_ADD_MSEC(&timeouts[i].when, time);
  147. timeouts[i].active = true;
  148. }
  149. return;
  150. }
  151. }
  152. }
  153. void reset_timeout(timeout_f handler)
  154. {
  155. int i;
  156. for (i = 0; i < ARRLEN(timeouts); i++) {
  157. if (timeouts[i].handler == handler) {
  158. timeouts[i].active = false;
  159. return;
  160. }
  161. }
  162. }
  163. bool check_timeouts(struct timeval *t)
  164. {
  165. int i = 0, tdiff, tmin = -1;
  166. struct timeval now;
  167. while (i < ARRLEN(timeouts)) {
  168. if (timeouts[i].active) {
  169. gettimeofday(&now, 0);
  170. tdiff = TV_DIFF(&timeouts[i].when, &now);
  171. if (tdiff <= 0) {
  172. timeouts[i].active = false;
  173. if (timeouts[i].handler != NULL)
  174. timeouts[i].handler();
  175. i = tmin = -1;
  176. } else if (tmin < 0 || tdiff < tmin) {
  177. tmin = tdiff;
  178. }
  179. }
  180. i++;
  181. }
  182. if (tmin > 0 && t != NULL)
  183. TV_SET_MSEC(t, tmin);
  184. return tmin > 0;
  185. }
  186. void open_info(void)
  187. {
  188. static pid_t pid;
  189. int pfd[2];
  190. win.bar.l[0] = '\0';
  191. if (info.fd != -1) {
  192. close(info.fd);
  193. kill(pid, SIGTERM);
  194. while (waitpid(-1, NULL, WNOHANG) > 0);
  195. info.fd = -1;
  196. }
  197. if (info.script == NULL || pipe(pfd) < 0)
  198. return;
  199. pid = fork();
  200. if (pid > 0) {
  201. close(pfd[1]);
  202. fcntl(pfd[0], F_SETFL, O_NONBLOCK);
  203. info.fd = pfd[0];
  204. info.i = info.lastsep = 0;
  205. } else if (pid == 0) {
  206. close(pfd[0]);
  207. dup2(pfd[1], 1);
  208. execl(info.script, info.script, files[fileidx].name, NULL);
  209. }
  210. }
  211. void read_info(void)
  212. {
  213. ssize_t i, n;
  214. char buf[BAR_L_LEN];
  215. while (true) {
  216. n = read(info.fd, buf, sizeof(buf));
  217. if (n < 0 && errno == EAGAIN)
  218. return;
  219. else if (n == 0)
  220. goto end;
  221. for (i = 0; i < n; i++) {
  222. if (buf[i] == '\n') {
  223. if (info.lastsep == 0) {
  224. win.bar.l[info.i++] = ' ';
  225. info.lastsep = 1;
  226. }
  227. } else {
  228. win.bar.l[info.i++] = buf[i];
  229. info.lastsep = 0;
  230. }
  231. if (info.i + 1 == sizeof(win.bar.l))
  232. goto end;
  233. }
  234. }
  235. end:
  236. info.i -= info.lastsep;
  237. win.bar.l[info.i] = '\0';
  238. win_update_bar(&win);
  239. info.fd = -1;
  240. while (waitpid(-1, NULL, WNOHANG) > 0);
  241. }
  242. void load_image(int new)
  243. {
  244. if (new < 0 || new >= filecnt)
  245. return;
  246. win_set_cursor(&win, CURSOR_WATCH);
  247. img_close(&img, false);
  248. while (!img_load(&img, &files[new])) {
  249. remove_file(new, false);
  250. if (new >= filecnt)
  251. new = filecnt - 1;
  252. }
  253. files[new].loaded = true;
  254. alternate = fileidx;
  255. fileidx = new;
  256. open_info();
  257. if (img.multi.cnt > 0 && img.multi.animate)
  258. set_timeout(animate, img.multi.frames[img.multi.sel].delay, true);
  259. else
  260. reset_timeout(animate);
  261. }
  262. void update_info(void)
  263. {
  264. int sel;
  265. unsigned int i, fn, fw, n;
  266. unsigned int llen = sizeof(win.bar.l), rlen = sizeof(win.bar.r);
  267. char *lt = win.bar.l, *rt = win.bar.r, title[TITLE_LEN];
  268. bool ow_info;
  269. for (fw = 0, i = filecnt; i > 0; fw++, i /= 10);
  270. sel = mode == MODE_IMAGE ? fileidx : tns.sel;
  271. if (mode == MODE_THUMB) {
  272. win_set_title(&win, "sxiv");
  273. if (tns.cnt == filecnt) {
  274. n = snprintf(rt, rlen, "%0*d/%d", fw, sel + 1, filecnt);
  275. ow_info = true;
  276. } else {
  277. snprintf(lt, llen, "Loading... %0*d/%d", fw, tns.cnt, filecnt);
  278. rt[0] = '\0';
  279. ow_info = false;
  280. }
  281. } else {
  282. snprintf(title, sizeof(title), "sxiv - %s", files[sel].name);
  283. win_set_title(&win, title);
  284. n = snprintf(rt, rlen, "%3d%% ", (int) (img.zoom * 100.0));
  285. if (img.multi.cnt > 0) {
  286. for (fn = 0, i = img.multi.cnt; i > 0; fn++, i /= 10);
  287. n += snprintf(rt + n, rlen - n, "(%0*d/%d) ",
  288. fn, img.multi.sel + 1, img.multi.cnt);
  289. }
  290. n += snprintf(rt + n, rlen - n, "%0*d/%d", fw, sel + 1, filecnt);
  291. ow_info = info.script == NULL;
  292. }
  293. if (ow_info) {
  294. fn = strlen(files[sel].name);
  295. if (fn < llen &&
  296. win_textwidth(files[sel].name, fn, true) +
  297. win_textwidth(rt, n, true) < win.w)
  298. {
  299. strncpy(lt, files[sel].name, llen);
  300. } else {
  301. strncpy(lt, files[sel].base, llen);
  302. }
  303. }
  304. }
  305. void redraw(void)
  306. {
  307. if (mode == MODE_IMAGE)
  308. img_render(&img);
  309. else
  310. tns_render(&tns);
  311. update_info();
  312. win_draw(&win);
  313. reset_timeout(redraw);
  314. reset_cursor();
  315. }
  316. void reset_cursor(void)
  317. {
  318. int i;
  319. cursor_t cursor = CURSOR_NONE;
  320. if (mode == MODE_IMAGE) {
  321. for (i = 0; i < ARRLEN(timeouts); i++) {
  322. if (timeouts[i].handler == reset_cursor) {
  323. if (timeouts[i].active)
  324. cursor = CURSOR_ARROW;
  325. break;
  326. }
  327. }
  328. } else {
  329. if (tns.cnt != filecnt)
  330. cursor = CURSOR_WATCH;
  331. else
  332. cursor = CURSOR_ARROW;
  333. }
  334. win_set_cursor(&win, cursor);
  335. }
  336. void animate(void)
  337. {
  338. if (img_frame_animate(&img, false)) {
  339. redraw();
  340. set_timeout(animate, img.multi.frames[img.multi.sel].delay, true);
  341. }
  342. }
  343. void clear_resize(void)
  344. {
  345. resized = false;
  346. }
  347. bool keymask(const keymap_t *k, unsigned int state)
  348. {
  349. return (k->ctrl ? ControlMask : 0) == (state & ControlMask);
  350. }
  351. bool buttonmask(const button_t *b, unsigned int state)
  352. {
  353. return ((b->ctrl ? ControlMask : 0) | (b->shift ? ShiftMask : 0)) ==
  354. (state & (ControlMask | ShiftMask));
  355. }
  356. void on_keypress(XKeyEvent *kev)
  357. {
  358. int i;
  359. KeySym ksym;
  360. char key;
  361. if (kev == NULL)
  362. return;
  363. XLookupString(kev, &key, 1, &ksym, NULL);
  364. if ((ksym == XK_Escape || (key >= '0' && key <= '9')) &&
  365. (kev->state & ControlMask) == 0)
  366. {
  367. /* number prefix for commands */
  368. prefix = ksym == XK_Escape ? 0 : prefix * 10 + (int) (key - '0');
  369. return;
  370. }
  371. for (i = 0; i < ARRLEN(keys); i++) {
  372. if (keys[i].ksym == ksym && keymask(&keys[i], kev->state)) {
  373. if (keys[i].cmd != NULL && keys[i].cmd(keys[i].arg))
  374. redraw();
  375. prefix = 0;
  376. return;
  377. }
  378. }
  379. }
  380. void on_buttonpress(XButtonEvent *bev)
  381. {
  382. int i, sel;
  383. if (bev == NULL)
  384. return;
  385. if (mode == MODE_IMAGE) {
  386. win_set_cursor(&win, CURSOR_ARROW);
  387. set_timeout(reset_cursor, TO_CURSOR_HIDE, true);
  388. for (i = 0; i < ARRLEN(buttons); i++) {
  389. if (buttons[i].button == bev->button &&
  390. buttonmask(&buttons[i], bev->state))
  391. {
  392. if (buttons[i].cmd != NULL && buttons[i].cmd(buttons[i].arg))
  393. redraw();
  394. return;
  395. }
  396. }
  397. } else {
  398. /* thumbnail mode (hard-coded) */
  399. switch (bev->button) {
  400. case Button1:
  401. if ((sel = tns_translate(&tns, bev->x, bev->y)) >= 0) {
  402. if (sel == tns.sel) {
  403. mode = MODE_IMAGE;
  404. set_timeout(reset_cursor, TO_CURSOR_HIDE, true);
  405. load_image(tns.sel);
  406. } else {
  407. tns_highlight(&tns, tns.sel, false);
  408. tns_highlight(&tns, sel, true);
  409. tns.sel = sel;
  410. }
  411. redraw();
  412. break;
  413. }
  414. break;
  415. case Button4:
  416. case Button5:
  417. if (tns_scroll(&tns, bev->button == Button4 ? DIR_UP : DIR_DOWN,
  418. (bev->state & ControlMask) != 0))
  419. redraw();
  420. break;
  421. }
  422. }
  423. }
  424. void run(void)
  425. {
  426. int xfd;
  427. fd_set fds;
  428. struct timeval timeout;
  429. bool discard, to_set;
  430. XEvent ev, nextev;
  431. redraw();
  432. while (true) {
  433. while (mode == MODE_THUMB && tns.cnt < filecnt &&
  434. XPending(win.env.dpy) == 0)
  435. {
  436. /* load thumbnails */
  437. set_timeout(redraw, TO_REDRAW_THUMBS, false);
  438. if (tns_load(&tns, tns.cnt, &files[tns.cnt], false, false)) {
  439. tns.cnt++;
  440. } else {
  441. remove_file(tns.cnt, false);
  442. if (tns.sel >= tns.cnt)
  443. tns.sel--;
  444. }
  445. if (tns.cnt == filecnt)
  446. redraw();
  447. else
  448. check_timeouts(NULL);
  449. }
  450. while (XPending(win.env.dpy) == 0
  451. && ((to_set = check_timeouts(&timeout)) || info.fd != -1))
  452. {
  453. /* check for timeouts & input */
  454. xfd = ConnectionNumber(win.env.dpy);
  455. FD_ZERO(&fds);
  456. FD_SET(xfd, &fds);
  457. if (info.fd != -1) {
  458. FD_SET(info.fd, &fds);
  459. xfd = MAX(xfd, info.fd);
  460. }
  461. select(xfd + 1, &fds, 0, 0, to_set ? &timeout : NULL);
  462. if (FD_ISSET(info.fd, &fds))
  463. read_info();
  464. }
  465. do {
  466. XNextEvent(win.env.dpy, &ev);
  467. discard = false;
  468. if (XEventsQueued(win.env.dpy, QueuedAlready) > 0) {
  469. XPeekEvent(win.env.dpy, &nextev);
  470. switch (ev.type) {
  471. case ConfigureNotify:
  472. discard = ev.type == nextev.type;
  473. break;
  474. case KeyPress:
  475. discard = (nextev.type == KeyPress || nextev.type == KeyRelease)
  476. && ev.xkey.keycode == nextev.xkey.keycode;
  477. break;
  478. }
  479. }
  480. } while (discard);
  481. switch (ev.type) {
  482. /* handle events */
  483. case ButtonPress:
  484. on_buttonpress(&ev.xbutton);
  485. break;
  486. case ClientMessage:
  487. if ((Atom) ev.xclient.data.l[0] == wm_delete_win)
  488. return;
  489. break;
  490. case ConfigureNotify:
  491. if (win_configure(&win, &ev.xconfigure)) {
  492. if (mode == MODE_IMAGE) {
  493. img.dirty = true;
  494. img.checkpan = true;
  495. } else {
  496. tns.dirty = true;
  497. }
  498. if (!resized || win.fullscreen) {
  499. redraw();
  500. set_timeout(clear_resize, TO_REDRAW_RESIZE, false);
  501. resized = true;
  502. } else {
  503. set_timeout(redraw, TO_REDRAW_RESIZE, false);
  504. }
  505. }
  506. break;
  507. case Expose:
  508. win_expose(&win, &ev.xexpose);
  509. break;
  510. case KeyPress:
  511. on_keypress(&ev.xkey);
  512. break;
  513. case MotionNotify:
  514. if (mode == MODE_IMAGE) {
  515. win_set_cursor(&win, CURSOR_ARROW);
  516. set_timeout(reset_cursor, TO_CURSOR_HIDE, true);
  517. }
  518. break;
  519. }
  520. }
  521. }
  522. int fncmp(const void *a, const void *b)
  523. {
  524. return strcoll(((fileinfo_t*) a)->name, ((fileinfo_t*) b)->name);
  525. }
  526. int main(int argc, char **argv)
  527. {
  528. int i, start;
  529. size_t n;
  530. ssize_t len;
  531. char *filename;
  532. const char *homedir;
  533. struct stat fstats;
  534. r_dir_t dir;
  535. parse_options(argc, argv);
  536. if (options->clean_cache) {
  537. tns_init(&tns, 0, NULL);
  538. tns_clean_cache(&tns);
  539. exit(EXIT_SUCCESS);
  540. }
  541. if (options->filecnt == 0) {
  542. print_usage();
  543. exit(EXIT_FAILURE);
  544. }
  545. if (options->recursive || options->from_stdin)
  546. filecnt = FILENAME_CNT;
  547. else
  548. filecnt = options->filecnt;
  549. files = (fileinfo_t*) s_malloc(filecnt * sizeof(fileinfo_t));
  550. fileidx = 0;
  551. /* build file list: */
  552. if (options->from_stdin) {
  553. filename = NULL;
  554. while ((len = get_line(&filename, &n, stdin)) > 0) {
  555. if (filename[len-1] == '\n')
  556. filename[len-1] = '\0';
  557. check_add_file(filename);
  558. }
  559. if (filename != NULL)
  560. free(filename);
  561. } else {
  562. for (i = 0; i < options->filecnt; i++) {
  563. filename = options->filenames[i];
  564. if (stat(filename, &fstats) < 0) {
  565. warn("could not stat file: %s", filename);
  566. continue;
  567. }
  568. if (!S_ISDIR(fstats.st_mode)) {
  569. check_add_file(filename);
  570. } else {
  571. if (!options->recursive) {
  572. warn("ignoring directory: %s", filename);
  573. continue;
  574. }
  575. if (r_opendir(&dir, filename) < 0) {
  576. warn("could not open directory: %s", filename);
  577. continue;
  578. }
  579. start = fileidx;
  580. while ((filename = r_readdir(&dir)) != NULL) {
  581. check_add_file(filename);
  582. free((void*) filename);
  583. }
  584. r_closedir(&dir);
  585. if (fileidx - start > 1)
  586. qsort(files + start, fileidx - start, sizeof(fileinfo_t), fncmp);
  587. }
  588. }
  589. }
  590. if (fileidx == 0) {
  591. fprintf(stderr, "sxiv: no valid image file given, aborting\n");
  592. exit(EXIT_FAILURE);
  593. }
  594. filecnt = fileidx;
  595. fileidx = options->startnum < filecnt ? options->startnum : 0;
  596. win_init(&win);
  597. img_init(&img, &win);
  598. if ((homedir = getenv("HOME")) == NULL) {
  599. warn("could not locate home directory");
  600. } else {
  601. len = strlen(homedir) + strlen(INFO_SCRIPT) + 2;
  602. info.script = (char*) s_malloc(len);
  603. snprintf(info.script, len, "%s/%s", homedir, INFO_SCRIPT);
  604. if (access(info.script, X_OK) != 0) {
  605. free(info.script);
  606. info.script = NULL;
  607. }
  608. }
  609. info.fd = -1;
  610. if (options->thumb_mode) {
  611. mode = MODE_THUMB;
  612. tns_init(&tns, filecnt, &win);
  613. while (!tns_load(&tns, 0, &files[0], false, false))
  614. remove_file(0, false);
  615. tns.cnt = 1;
  616. } else {
  617. mode = MODE_IMAGE;
  618. tns.thumbs = NULL;
  619. load_image(fileidx);
  620. }
  621. win_open(&win);
  622. run();
  623. cleanup();
  624. return 0;
  625. }