diff --git a/Makefile b/Makefile index d814b2a..274ae1e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ all: sxiv -VERSION=git-20110407 +VERSION=git-20110408 CC?=gcc PREFIX?=/usr/local diff --git a/README.md b/README.md index 0d50674..424caf2 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ sxiv supports the following command-line options: -a Display all given files, do not filter out unsupported files (shorter startup time for long file list or slow file types) + -C Remove all orphaned cache files from thumbnail cache and exit -d Scale all images to 100%, but fit large images into window -F Use size-hints to make the window fixed/floating -f Start in fullscreen mode diff --git a/main.c b/main.c index 45f3666..0a66aab 100644 --- a/main.c +++ b/main.c @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -48,7 +47,6 @@ typedef enum { void update_title(); int check_append(const char*); -void read_dir_rec(const char*); void run(); appmode_t mode; @@ -56,7 +54,6 @@ img_t img; tns_t tns; win_t win; -#define DNAME_CNT 512 #define FNAME_CNT 1024 const char **filenames; int filecnt, fileidx; @@ -95,13 +92,24 @@ int load_image(int new) { return ret; } +int fncmp(const void *a, const void *b) { + return strcoll(*((char* const*) a), *((char* const*) b)); +} + int main(int argc, char **argv) { - int i; + int i, start; const char *filename; struct stat fstats; + r_dir_t dir; parse_options(argc, argv); + if (options->clean_cache) { + tns_init(&tns, 0); + tns_clear_cache(&tns); + exit(0); + } + if (!options->filecnt) { print_usage(); exit(1); @@ -123,13 +131,26 @@ int main(int argc, char **argv) { } else { for (i = 0; i < options->filecnt; ++i) { filename = options->filenames[i]; - if (!stat(filename, &fstats) && S_ISDIR(fstats.st_mode)) { - if (options->recursive) - read_dir_rec(filename); - else - warn("ignoring directory: %s", filename); - } else { + + if (stat(filename, &fstats) || !S_ISDIR(fstats.st_mode)) { check_append(filename); + } else { + if (!options->recursive) { + warn("ignoring directory: %s", filename); + continue; + } + if (r_opendir(&dir, filename)) { + warn("could not open directory: %s", filename); + continue; + } + start = fileidx; + while ((filename = r_readdir(&dir))) { + if (!check_append(filename)) + free((void*) filename); + } + r_closedir(&dir); + if (fileidx - start > 1) + qsort(filenames + start, fileidx - start, sizeof(char*), fncmp); } } } @@ -215,75 +236,6 @@ int check_append(const char *filename) { } } -int fncmp(const void *a, const void *b) { - return strcoll(*((char* const*) a), *((char* const*) b)); -} - -void read_dir_rec(const char *dirname) { - char *filename; - const char **dirnames; - int dircnt, diridx; - int fcnt, fstart; - unsigned char first; - size_t len; - DIR *dir; - struct dirent *dentry; - struct stat fstats; - - if (!dirname) - return; - - dircnt = DNAME_CNT; - diridx = first = 1; - dirnames = (const char**) s_malloc(dircnt * sizeof(const char*)); - dirnames[0] = dirname; - - fcnt = 0; - fstart = fileidx; - - while (diridx > 0) { - dirname = dirnames[--diridx]; - if (!(dir = opendir(dirname))) { - warn("could not open directory: %s", dirname); - } else { - while ((dentry = readdir(dir))) { - if (!strcmp(dentry->d_name, ".") || !strcmp(dentry->d_name, "..")) - continue; - - len = strlen(dirname) + strlen(dentry->d_name) + 2; - filename = (char*) s_malloc(len * sizeof(char)); - snprintf(filename, len, "%s%s%s", dirname, - dirname[strlen(dirname)-1] == '/' ? "" : "/", dentry->d_name); - - if (!stat(filename, &fstats) && S_ISDIR(fstats.st_mode)) { - if (diridx == dircnt) { - dircnt *= 2; - dirnames = (const char**) s_realloc(dirnames, - dircnt * sizeof(const char*)); - } - dirnames[diridx++] = filename; - } else { - if (check_append(filename)) - ++fcnt; - else - free(filename); - } - } - closedir(dir); - } - - if (!first) - free((void*) dirname); - else - first = 0; - } - - if (fcnt > 1) - qsort(filenames + fstart, fcnt, sizeof(char*), fncmp); - - free(dirnames); -} - #if EXT_COMMANDS int run_command(const char *cline, Bool reload) { int fncnt, fnlen; diff --git a/options.c b/options.c index f5066f4..8105485 100644 --- a/options.c +++ b/options.c @@ -31,7 +31,7 @@ options_t _options; const options_t *options = (const options_t*) &_options; void print_usage() { - printf("usage: sxiv [-adFfhpqrstvZ] [-g GEOMETRY] [-z ZOOM] FILES...\n"); + printf("usage: sxiv [-aCdFfhpqrstvZ] [-g GEOMETRY] [-z ZOOM] FILES...\n"); } void print_version() { @@ -53,9 +53,10 @@ void parse_options(int argc, char **argv) { _options.all = 0; _options.quiet = 0; + _options.clean_cache = 0; _options.recursive = 0; - while ((opt = getopt(argc, argv, "adFfg:hpqrstvZz:")) != -1) { + while ((opt = getopt(argc, argv, "aCdFfg:hpqrstvZz:")) != -1) { switch (opt) { case '?': print_usage(); @@ -63,6 +64,9 @@ void parse_options(int argc, char **argv) { case 'a': _options.all = 1; break; + case 'C': + _options.clean_cache = 1; + break; case 'd': _options.scalemode = SCALE_DOWN; break; diff --git a/options.h b/options.h index c32b50a..b7b8ba5 100644 --- a/options.h +++ b/options.h @@ -37,6 +37,7 @@ typedef struct { unsigned char all; unsigned char quiet; + unsigned char clean_cache; unsigned char recursive; } options_t; diff --git a/sxiv.1 b/sxiv.1 index 35e7bfb..e7f19e2 100644 --- a/sxiv.1 +++ b/sxiv.1 @@ -3,7 +3,7 @@ sxiv \- Simple (or small or suckless) X Image Viewer .SH SYNOPSIS .B sxiv -.RB [ \-adFfhpqrstvZ ] +.RB [ \-aCdFfhpqrstvZ ] .RB [ \-g .IR GEOMETRY ] .RB [ \-z @@ -24,6 +24,9 @@ sxiv has two modes of operation: image and thumbnail mode. The default is image mode, in which only the current image is shown. In thumbnail mode a grid of small previews is displayed, making it easy to choose an image to open. .P +sxiv can also cache its thumbnails. Please see the section THUMBNAIL CACHING +for information on how to enable this feature. +.P Please note, that the fullscreen mode requires an EWMH/NetWM compliant window manager. .SH OPTIONS @@ -33,6 +36,9 @@ Display all given files, do not filter out unsupported files. This might result in a much shorter startup time, when the file list is very long or contains large files of slow loadable types, e.g. gif and progressive jpg. .TP +.B \-C +Remove all orphaned cache files from the thumbnail cache directory and exit. +.TP .B \-d Scale all images to 100%, but fit large images into window. .TP @@ -191,6 +197,23 @@ Pan image left. .TP .B Shift+ScrollDown Pan image right. +.SH THUMBNAIL CACHING +To enable thumbnail caching, please make sure to create the directory +.I ~/.sxiv/ +with write permissions. sxiv will then store all thumbnails inside this +directory, but it will not create this directory by itself. It rather uses the +existance of this directory as an affirmation, that the user wants thumbnails +to be cached. +.P +Use the command line option +.I \-C +to keep the cache directory clean by removing all orphaned cache files. +Additionally, run the following command afterwards inside the cache directory +to remove empty subdirectories: +.P +.RS +find -type d -empty -delete +.RE .SH AUTHORS .EX Bert Muennich diff --git a/thumbs.c b/thumbs.c index da2adee..74ed708 100644 --- a/thumbs.c +++ b/thumbs.c @@ -18,49 +18,214 @@ #include #include +#include +#include +#include +#include #include "config.h" #include "thumbs.h" #include "util.h" extern Imlib_Image *im_invalid; + const int thumb_dim = THUMB_SIZE + 10; +char *cache_dir = NULL; + +int tns_cache_enabled() { + struct stat stats; + + return cache_dir && !stat(cache_dir, &stats) && S_ISDIR(stats.st_mode) && + !access(cache_dir, W_OK); +} + +char* tns_cache_filename(const char *filename) { + size_t len; + char *cfile = NULL; + const char *abspath; + + if (!cache_dir || !filename) + return NULL; + + if (*filename != '/') { + if (!(abspath = absolute_path(filename))) + return NULL; + } else { + abspath = filename; + } + + if (strncmp(abspath, cache_dir, strlen(cache_dir))) { + len = strlen(cache_dir) + strlen(abspath) + 6; + cfile = (char*) s_malloc(len); + snprintf(cfile, len, "%s/%s.png", cache_dir, abspath + 1); + } + + if (abspath != filename) + free((void*) abspath); + + return cfile; +} + +Imlib_Image* tns_cache_load(const char *filename) { + char *cfile; + struct stat cstats, fstats; + Imlib_Image *im = NULL; + + if (!filename) + return NULL; + + if (stat(filename, &fstats)) + return NULL; + + if ((cfile = tns_cache_filename(filename))) { + if (!stat(cfile, &cstats) && + cstats.st_mtim.tv_sec == fstats.st_mtim.tv_sec && + cstats.st_mtim.tv_nsec == fstats.st_mtim.tv_nsec) + { + im = imlib_load_image(cfile); + } + free(cfile); + } + + return im; +} + +void tns_cache_write(thumb_t *t, Bool force) { + char *cfile, *dirend; + struct stat cstats, fstats; + struct timeval times[2]; + Imlib_Load_Error err = 0; + + if (!t || !t->im || !t->filename) + return; + + if (stat(t->filename, &fstats)) + return; + + if ((cfile = tns_cache_filename(t->filename))) { + if (force || stat(cfile, &cstats) || + cstats.st_mtim.tv_sec != fstats.st_mtim.tv_sec || + cstats.st_mtim.tv_nsec != fstats.st_mtim.tv_nsec) + { + if ((dirend = strrchr(cfile, '/'))) { + *dirend = '\0'; + err = r_mkdir(cfile); + *dirend = '/'; + } + + if (!err) { + imlib_context_set_image(t->im); + imlib_image_set_format("png"); + imlib_save_image_with_error_return(cfile, &err); + } + + if (err) { + warn("could not cache thumbnail: %s", t->filename); + } else { + TIMESPEC_TO_TIMEVAL(×[0], &fstats.st_atim); + TIMESPEC_TO_TIMEVAL(×[1], &fstats.st_mtim); + utimes(cfile, times); + } + } + free(cfile); + } +} + +void tns_clear_cache(tns_t *tns) { + int dirlen, delete; + char *cfile, *filename, *tpos; + r_dir_t dir; + + if (!cache_dir) + return; + + if (r_opendir(&dir, cache_dir)) { + warn("could not open thumbnail cache directory: %s", cache_dir); + return; + } + + dirlen = strlen(cache_dir); + + while ((cfile = r_readdir(&dir))) { + filename = cfile + dirlen; + delete = 0; + + if ((tpos = strrchr(filename, '.'))) { + *tpos = '\0'; + delete = access(filename, F_OK); + *tpos = '.'; + } + + if (delete && unlink(cfile)) + warn("could not delete cache file: %s", cfile); + + free(cfile); + } + + r_closedir(&dir); +} + void tns_init(tns_t *tns, int cnt) { + int len; + char *homedir; + if (!tns) return; + if (cnt) { + tns->thumbs = (thumb_t*) s_malloc(cnt * sizeof(thumb_t)); + memset(tns->thumbs, 0, cnt * sizeof(thumb_t)); + } else { + tns->thumbs = NULL; + } + tns->cnt = tns->first = tns->sel = 0; - tns->thumbs = (thumb_t*) s_malloc(cnt * sizeof(thumb_t)); - memset(tns->thumbs, 0, cnt * sizeof(thumb_t)); tns->cap = cnt; tns->dirty = 0; + + if ((homedir = getenv("HOME"))) { + if (cache_dir) + free(cache_dir); + len = strlen(homedir) + 10; + cache_dir = (char*) s_malloc(len * sizeof(char)); + snprintf(cache_dir, len, "%s/.sxiv", homedir); + } else { + warn("could not locate thumbnail cache directory"); + } } void tns_free(tns_t *tns, win_t *win) { int i; - if (!tns || !tns->thumbs) + if (!tns) return; - for (i = 0; i < tns->cnt; ++i) { - if (tns->thumbs[i].im) { - imlib_context_set_image(tns->thumbs[i].im); - imlib_free_image(); + if (tns->thumbs) { + for (i = 0; i < tns->cnt; ++i) { + if (tns->thumbs[i].im) { + imlib_context_set_image(tns->thumbs[i].im); + imlib_free_image(); + } } + free(tns->thumbs); + tns->thumbs = NULL; } - free(tns->thumbs); - tns->thumbs = NULL; + if (cache_dir) { + free(cache_dir); + cache_dir = NULL; + } } void tns_load(tns_t *tns, win_t *win, int n, const char *filename) { int w, h; + int use_cache, cached = 0; float z, zw, zh; thumb_t *t; Imlib_Image *im; - if (!tns || !win || !filename) + if (!tns || !tns->thumbs || !win || !filename) return; if (n >= tns->cap) @@ -75,7 +240,12 @@ void tns_load(tns_t *tns, win_t *win, int n, const char *filename) { imlib_free_image(); } - if ((im = imlib_load_image(filename))) + if ((use_cache = tns_cache_enabled())) { + if ((im = tns_cache_load(filename))) + cached = 1; + } + + if (cached || (im = imlib_load_image(filename))) imlib_context_set_image(im); else imlib_context_set_image(im_invalid); @@ -84,10 +254,12 @@ void tns_load(tns_t *tns, win_t *win, int n, const char *filename) { h = imlib_image_get_height(); if (im) { + t->filename = filename; zw = (float) THUMB_SIZE / (float) w; zh = (float) THUMB_SIZE / (float) h; z = MIN(zw, zh); } else { + t->filename = NULL; z = 1.0; } @@ -99,6 +271,8 @@ void tns_load(tns_t *tns, win_t *win, int n, const char *filename) { die("could not allocate memory"); if (im) imlib_free_image_and_decache(); + if (use_cache && !cached) + tns_cache_write(t, False); tns->dirty = 1; } @@ -134,7 +308,10 @@ void tns_render(tns_t *tns, win_t *win) { int i, cnt, r, x, y; thumb_t *t; - if (!tns || !tns->dirty || !win) + if (!tns || !tns->thumbs || !win) + return; + + if (!tns->dirty) return; win_clear(win); @@ -182,7 +359,7 @@ void tns_highlight(tns_t *tns, win_t *win, int n, Bool hl) { thumb_t *t; unsigned long col; - if (!tns || !win) + if (!tns || !tns->thumbs || !win) return; if (n >= 0 && n < tns->cnt) { @@ -205,7 +382,7 @@ void tns_highlight(tns_t *tns, win_t *win, int n, Bool hl) { int tns_move_selection(tns_t *tns, win_t *win, tnsdir_t dir) { int old; - if (!tns || !win) + if (!tns || !tns->thumbs || !win) return 0; old = tns->sel; @@ -264,7 +441,10 @@ int tns_translate(tns_t *tns, int x, int y) { int n; thumb_t *t; - if (!tns || x < tns->x || y < tns->y) + if (!tns || !tns->thumbs) + return -1; + + if (x < tns->x || y < tns->y) return -1; n = tns->first + (y - tns->y) / thumb_dim * tns->cols + diff --git a/thumbs.h b/thumbs.h index 4b428d7..4a8af09 100644 --- a/thumbs.h +++ b/thumbs.h @@ -32,6 +32,7 @@ typedef enum { typedef struct { Imlib_Image *im; + const char *filename; int x; int y; int w; @@ -51,6 +52,8 @@ typedef struct { unsigned char dirty; } tns_t; +void tns_clear_cache(tns_t*); + void tns_init(tns_t*, int); void tns_free(tns_t*, win_t*); diff --git a/util.c b/util.c index 5c5737b..e16de94 100644 --- a/util.c +++ b/util.c @@ -18,11 +18,16 @@ #include #include +#include +#include +#include +#include #include "options.h" #include "util.h" -#define FNAME_LEN 512 +#define DNAME_CNT 512 +#define FNAME_LEN 1024 void cleanup(); @@ -78,6 +83,221 @@ void size_readable(float *size, const char **unit) { *unit = units[MIN(i, LEN(units) - 1)]; } +char* absolute_path(const char *filename) { + size_t len; + char *path = NULL; + const char *basename; + char *dirname = NULL; + char *cwd = NULL; + char *twd = NULL; + char *dir; + char *s; + + if (!filename || *filename == '\0' || *filename == '/') + return NULL; + + len = FNAME_LEN; + cwd = (char*) s_malloc(len); + while (!(s = getcwd(cwd, len)) && errno == ERANGE) { + len *= 2; + cwd = (char*) s_realloc(cwd, len); + } + if (!s) + goto error; + + s = strrchr(filename, '/'); + if (s) { + len = s - filename; + dirname = (char*) s_malloc(len + 1); + strncpy(dirname, filename, len); + dirname[len] = '\0'; + basename = s + 1; + + if (chdir(cwd)) + /* we're not able to come back afterwards */ + goto error; + if (chdir(dirname)) + goto error; + + len = FNAME_LEN; + twd = (char*) s_malloc(len); + while (!(s = getcwd(twd, len)) && errno == ERANGE) { + len *= 2; + twd = (char*) s_realloc(twd, len); + } + if (chdir(cwd)) + die("could not revert to prior working directory"); + if (!s) + goto error; + dir = twd; + } else { + /* only a single filename given */ + basename = filename; + dir = cwd; + } + + len = strlen(dir) + strlen(basename) + 2; + path = (char*) s_malloc(len); + snprintf(path, len, "%s/%s", dir, basename); + +goto end; + +error: + if (path) { + free(path); + path = NULL; + } + +end: + if (dirname) + free(dirname); + if (cwd) + free(cwd); + if (twd) + free(twd); + + return path; +} + +int r_opendir(r_dir_t *rdir, const char *dirname) { + if (!rdir || !dirname || !*dirname) + return -1; + + if (!(rdir->dir = opendir(dirname))) { + rdir->name = NULL; + rdir->stack = NULL; + return -1; + } + + rdir->stcap = DNAME_CNT; + rdir->stack = (char**) s_malloc(rdir->stcap * sizeof(char*)); + rdir->stlen = 0; + + rdir->name = (char*) dirname; + rdir->d = 0; + + return 0; +} + +int r_closedir(r_dir_t *rdir) { + int ret = 0; + + if (!rdir) + return -1; + + if (rdir->stack) { + while (rdir->stlen > 0) + free(rdir->stack[--rdir->stlen]); + free(rdir->stack); + rdir->stack = NULL; + } + + if (rdir->dir) { + if (!(ret = closedir(rdir->dir))) + rdir->dir = NULL; + } + + if (rdir->d && rdir->name) { + free(rdir->name); + rdir->name = NULL; + } + + return ret; +} + +char* r_readdir(r_dir_t *rdir) { + size_t len; + char *filename; + struct dirent *dentry; + struct stat fstats; + + if (!rdir || !rdir->dir || !rdir->name) + return NULL; + + while (1) { + if (rdir->dir && (dentry = readdir(rdir->dir))) { + if (!strcmp(dentry->d_name, ".") || !strcmp(dentry->d_name, "..")) + continue; + + len = strlen(rdir->name) + strlen(dentry->d_name) + 2; + filename = (char*) s_malloc(len); + snprintf(filename, len, "%s%s%s", rdir->name, + rdir->name[strlen(rdir->name)-1] == '/' ? "" : "/", + dentry->d_name); + + if (!stat(filename, &fstats) && S_ISDIR(fstats.st_mode)) { + /* put subdirectory on the stack */ + if (rdir->stlen == rdir->stcap) { + rdir->stcap *= 2; + rdir->stack = (char**) s_realloc(rdir->stack, + rdir->stcap * sizeof(char*)); + } + rdir->stack[rdir->stlen++] = filename; + continue; + } + return filename; + } + + if (rdir->stlen > 0) { + /* open next subdirectory */ + closedir(rdir->dir); + if (rdir->d) + free(rdir->name); + rdir->name = rdir->stack[--rdir->stlen]; + rdir->d = 1; + if (!(rdir->dir = opendir(rdir->name))) + warn("could not open directory: %s", rdir->name); + continue; + } + + /* no more entries */ + break; + } + + return NULL; +} + +int r_mkdir(const char *path) { + char *dir, *d; + struct stat stats; + int err = 0; + + if (!path || !*path) + return -1; + + if (!stat(path, &stats)) { + if (S_ISDIR(stats.st_mode)) { + return 0; + } else { + warn("not a directory: %s", path); + return -1; + } + } + + d = dir = (char*) s_malloc(strlen(path) + 1); + strcpy(dir, path); + + while (d != NULL && !err) { + d = strchr(d + 1, '/'); + if (d != NULL) + *d = '\0'; + if (access(dir, F_OK) && errno == ENOENT) { + if (mkdir(dir, 0755)) { + warn("could not create directory: %s", dir); + err = -1; + } + } else if (stat(dir, &stats) || !S_ISDIR(stats.st_mode)) { + warn("not a directory: %s", dir); + err = -1; + } + if (d != NULL) + *d = '/'; + } + free(dir); + + return err; +} + char* readline(FILE *stream) { size_t len; char *buf, *s, *end; diff --git a/util.h b/util.h index f1db6b8..8e8a20d 100644 --- a/util.h +++ b/util.h @@ -21,6 +21,7 @@ #include #include +#include #define ABS(a) ((a) < 0 ? (-(a)) : (a)) #define MIN(a,b) ((a) < (b) ? (a) : (b)) @@ -30,6 +31,21 @@ #define TV_TO_DOUBLE(x) ((double) ((x).tv_sec) + 0.000001 * \ (double) ((x).tv_usec)) +#define TIMESPEC_TO_TIMEVAL(tv, ts) { \ + (tv)->tv_sec = (ts)->tv_sec; \ + (tv)->tv_usec = (ts)->tv_nsec / 1000; \ +} + +typedef struct { + DIR *dir; + char *name; + int d; + + char **stack; + int stcap; + int stlen; +} r_dir_t; + void* s_malloc(size_t); void* s_realloc(void*, size_t); @@ -38,6 +54,13 @@ void die(const char*, ...); void size_readable(float*, const char**); +char* absolute_path(const char*); + +int r_opendir(r_dir_t*, const char*); +int r_closedir(r_dir_t*); +char* r_readdir(r_dir_t*); +int r_mkdir(const char *); + char* readline(FILE*); #endif /* UTIL_H */