sfm

Simple file manager
git clone git://git.margiolis.net/sfm.git
Log | Files | Refs | README | LICENSE

sfm.c (14518B)


      1 /* See LICENSE file for copyright and license details. */
      2 #include <sys/stat.h>
      3 #include <sys/types.h>
      4 #include <sys/wait.h>
      5 
      6 #include <dirent.h>
      7 #include <err.h>
      8 #include <errno.h>
      9 #include <fcntl.h>
     10 #include <limits.h>
     11 #include <locale.h>
     12 #include <signal.h>
     13 #include <stdio.h>
     14 #include <stdlib.h>
     15 #include <string.h>
     16 #include <time.h>
     17 #include <unistd.h>
     18 
     19 #include <curses.h>
     20 
     21 #ifndef DT_DIR
     22 #define DT_DIR 4
     23 #endif /* DT_DIR */
     24 #ifndef DT_REG
     25 #define DT_REG 8
     26 #endif /* DT_REG */
     27 #ifndef DT_LNK
     28 #define DT_LNK 10
     29 #endif /* DT_LNK */
     30 
     31 #ifndef ESC
     32 #define ESC 27
     33 #endif /* ESC */
     34 
     35 #ifndef SIGWINCH
     36 #define SIGWINCH 28
     37 #endif /* SIGWINCH */
     38 
     39 #define F_SHOWALL		(1 << 1)
     40 #define F_INFO			(1 << 2)
     41 #define F_REDRAW		(1 << 3)
     42 #define F_QUIT			(1 << 4)
     43 
     44 #define CTRL(x)			((x) & 0x1f)
     45 #define YMAX			(getmaxy(stdscr))
     46 #define XMAX			(getmaxx(stdscr))
     47 #define TIMEOUT_SET()		(timeout(1000))
     48 #define TIMEOUT_CLEAR()		(timeout(-1))
     49 #define MIN(x, y)		((x) < (y) ? (x) : (y))
     50 #define MAX(x, y)		((x) > (y) ? (x) : (y))
     51 #define LEN(x)			(sizeof(x) / sizeof(x[0]))
     52 #define ENTSORT(e, n, f)	(qsort((e), (n), sizeof(*(e)), f))
     53 #define	UNUSED(x)		((void)(x))
     54 
     55 struct entry {
     56 	char name[NAME_MAX];
     57 	char date[NAME_MAX];
     58 	off_t size;
     59 	mode_t mode;
     60 	int flags;
     61 	int selected;
     62 };
     63 
     64 struct pane {
     65 	struct entry *ents;
     66 	char curdir[PATH_MAX];
     67 	int nents;
     68 	int sel;
     69 	int (*sortfn)(const void *, const void *);
     70 };
     71 
     72 typedef union arg {
     73 	int i;
     74 	const char *s;
     75 	const void *v;
     76 } arg;
     77 
     78 struct key {
     79 	int key;
     80 	void (*func)(const arg *);
     81 	const arg arg;
     82 };
     83 
     84 enum {
     85 	S_UP,
     86 	S_DOWN,
     87 	S_PGUP,
     88 	S_PGDOWN,
     89 	S_TOP,
     90 	S_BOTTOM,
     91 	S_LEFT,
     92 	S_RIGHT,
     93 };
     94 
     95 enum {
     96 	MSG_SORT,
     97 	MSG_FAIL,
     98 };
     99 
    100 static void curses_init(void);
    101 static struct entry *entries_read(const char *);
    102 static void pane_print(struct pane *);
    103 static void notify(const char *, ...);
    104 static int confirm(const char *);
    105 static char *escape(const char *);
    106 static char *size_human(size_t);
    107 static void str_print(char *);
    108 static char *prompt_read(const char *);
    109 static void xdelay(int);
    110 static void sel_clump(void);
    111 static void *emalloc(size_t);
    112 static void *erealloc(void *, size_t);
    113 static char *estrdup(const char *);
    114 static void sighandler(int);
    115 static void cleanup(void);
    116 static void chlevel(const arg *);
    117 static void scrolldir(const arg *);
    118 static void cyclepane(const arg *);
    119 static void setflag(const arg *);
    120 static void cd(const arg *);
    121 static void run(const arg *);
    122 static void sort(const arg *);
    123 
    124 static char *argv0;
    125 static struct pane *panes;
    126 static int selpane = 0;		/* current pane */
    127 static const char *msgs[] = {
    128 	[MSG_SORT] = "'n'ame 's'ize 'd'ate 'r'everse",
    129 	[MSG_FAIL] = "action failed: %s",
    130 };
    131 
    132 /* flags */
    133 static int f_showall = 0;	/* show hidden files */
    134 static int f_info = 0;
    135 static int f_noconfirm = 0;	/* exec without confirmation */
    136 static int f_namesort = 0;
    137 static int f_sizesort = 0;
    138 static int f_datesort = 0;
    139 static int f_revsort = 0;
    140 static volatile sig_atomic_t f_redraw = 0;
    141 static volatile sig_atomic_t f_running = 1;
    142 
    143 #include "config.h"
    144 
    145 static inline int
    146 namecmp(const void *x, const void *y)
    147 {
    148 	return (strcmp(((struct entry *)x)->name, ((struct entry *)y)->name));
    149 }
    150 
    151 static inline int
    152 revnamecmp(const void *x, const void *y)
    153 {
    154 	return (-namecmp(x, y));
    155 }
    156 
    157 static inline int
    158 datecmp(const void *x, const void *y)
    159 {
    160 	return (strcmp(((struct entry *)x)->date, ((struct entry *)y)->date));
    161 }
    162 
    163 static inline int
    164 revdatecmp(const void *x, const void *y)
    165 {
    166 	return (-datecmp(x, y));
    167 }
    168 
    169 static inline int
    170 sizecmp(const void *x, const void *y)
    171 {
    172 	return (-(((struct entry *)x)->size - ((struct entry *)y)->size));
    173 }
    174 
    175 static inline int
    176 revsizecmp(const void *x, const void *y)
    177 {
    178 	return (-sizecmp(x, y));
    179 }
    180 
    181 static void
    182 curses_init(void)
    183 {
    184 	if (!initscr())
    185 		errx(1, "initscr");
    186 	noecho();
    187 	cbreak();
    188 	curs_set(0);
    189 	keypad(stdscr, 1);
    190 	TIMEOUT_SET();
    191 	set_escdelay(0);
    192 }
    193 
    194 static struct entry *
    195 entries_read(const char *path)
    196 {
    197 	struct entry *ents, *ep;
    198 	struct dirent *dent;
    199 	struct stat st;
    200 	DIR *dir;
    201 	int i = 0;
    202 
    203 	/* XXX: should we die? */
    204 	if ((dir = opendir(path)) == NULL)
    205 		err(1, "opendir: %s", path);
    206 
    207 	ents = emalloc(sizeof(struct entry));
    208 
    209 	while ((dent = readdir(dir)) != NULL) {
    210 		if (!strcmp(dent->d_name, "..") || !strcmp(dent->d_name, "."))
    211 			continue;
    212 		if (!f_showall && dent->d_name[0] == '.')
    213 			continue;
    214 		/* TODO: handle filtering */
    215 
    216 		ents = erealloc(ents, (i + 1) * sizeof(struct entry));
    217 		ep = &ents[i];
    218 
    219 		(void)strlcpy(ep->name, dent->d_name, sizeof(ep->name));
    220 		/* XXX: use fstatat(3) */
    221 		lstat(ep->name, &st);
    222 		ep->size = st.st_size;
    223 		ep->mode = st.st_mode;
    224 		strftime(ep->date, sizeof(ep->date), datefmt,
    225 		    localtime(&st.st_ctime));
    226 
    227 		/* FIXME: resets on every redraw, keep track somehow */
    228 		ep->selected = 0;
    229 		ep->flags = dent->d_type | 0;
    230 
    231 		i++;
    232 
    233 	}
    234 	(void)closedir(dir);
    235 	panes[selpane].nents = i;
    236 	ENTSORT(ents, panes[selpane].nents, panes[selpane].sortfn);
    237 
    238 	return (ents);
    239 }
    240 
    241 static void
    242 pane_print(struct pane *p)
    243 {
    244 	struct entry *ep;
    245 	int n, i;
    246 	int attrs;
    247 	/*int color;*/
    248 	char ind;
    249 
    250 	/*attron(COLOR_PAIR(C_PAN));*/
    251 	addch('[');
    252 	for (i = 0; i < npanes; i++) {
    253 		if (i == selpane)
    254 			attron(A_BOLD | A_REVERSE);
    255 		addch(i + '0' + 1);
    256 		attroff(A_BOLD | A_REVERSE);
    257 		if (i != npanes - 1)
    258 			addch(' ');
    259 	}
    260 	addstr("] ");
    261 	/*attroff(COLOR_PAIR(C_PAN));*/
    262 
    263 	/*attron(A_BOLD | COLOR_PAIR(C_PTH));*/
    264 	str_print(p->curdir);
    265 	/*attroff(A_BOLD | COLOR_PAIR(C_PTH));*/
    266 
    267 	/*if ((n = MIN(YMAX - 4, p->nents - p->curscroll)) <= 0)*/
    268 		/*return;*/
    269 	if ((n = p->nents) <= 0)
    270 		return;
    271 
    272 	for (i = 0; i < n; i++) {
    273 		ep = &p->ents[i];
    274 		ind = ' ';
    275 		attrs = 0;
    276 		/*color = 0;*/
    277 
    278 		move(i + 2, 0);
    279 		
    280 		/* FIXME: no. */
    281 		if (i == p->sel)
    282 			attrs |= A_REVERSE;
    283 
    284 		if (f_info) {
    285 			/*attron(COLOR_PAIR(C_INF));*/
    286 			printw("%s  %c%c%c  %7s  ",
    287 			    ep->date,
    288 			    ((ep->mode >> 6) & 7) + '0',
    289 			    ((ep->mode >> 3) & 7) + '0',
    290 			    (ep->mode & 7) + '0',
    291 			    size_human(ep->size));
    292 			/*attroff(COLOR_PAIR(C_INF));*/
    293 		}
    294 
    295 		addch(ep->selected ? '+' : ' ');
    296 
    297 		switch (ep->mode & S_IFMT) {
    298 		case S_IFDIR:
    299 			ind = '/';
    300 			/*color = C_DIR;*/
    301 			attrs |= A_BOLD;
    302 			break;
    303 		case S_IFREG:
    304 			/*color = C_FIL;*/
    305 			if (ep->mode & 0100) {
    306 				ind = '*';
    307 				/*color = C_EXE;*/
    308 			}
    309 			break;
    310 		case S_IFLNK:
    311 			/* FIXME: @ is added to dirs too */
    312 			ind = (ep->flags & DT_DIR) ? '/' : '@';
    313 			/*color = C_LNK;*/
    314 			if (S_ISDIR(ep->mode))
    315 				attrs |= A_BOLD;
    316 			break;
    317 		case S_IFSOCK:
    318 			ind = '=';
    319 			/*color = C_SOC;*/
    320 			break;
    321 		case S_IFIFO:
    322 			ind = '|';
    323 			/*color = C_PIP;*/
    324 			break;
    325 		case S_IFBLK:
    326 			/*color = C_BLK;*/
    327 			break;
    328 		case S_IFCHR:
    329 			/*color = C_CHR;*/
    330 			break;
    331 		default:
    332 			ind = '?';
    333 			/*color = C_UND;*/
    334 			break;
    335 		}
    336 
    337 		/*attrs |= COLOR_PAIR(color);*/
    338 		attron(attrs);
    339 		str_print(ep->name);
    340 		attroff(attrs);
    341 		addch(ind);
    342 	}
    343 
    344 	ep = &p->ents[p->sel];
    345 	switch (ep->mode & S_IFMT) {
    346 	case S_IFREG:
    347 		n = '-';
    348 		break;
    349 	case S_IFDIR:
    350 		n = 'd';
    351 		break;
    352 	case S_IFLNK:
    353 		n = 'l';
    354 		break;
    355 	case S_IFSOCK:
    356 		n = 's';
    357 		break;
    358 	case S_IFIFO:
    359 		n = 'p';
    360 		break;
    361 	case S_IFBLK:
    362 		n = 'b';
    363 		break;
    364 	case S_IFCHR:
    365 		n = 'c';
    366 		break;
    367 	default:
    368 		n = '?';
    369 		break;
    370 	}
    371 
    372 	/*attron(COLOR_PAIR(C_STA));*/
    373 	mvprintw(YMAX - 1, 0, "%ld/%ld %c%c%c%c%c%c%c%c%c%c %s %s",
    374 	    p->sel + 1, p->nents, n,
    375 	    ep->mode & S_IRUSR ? 'r' : '-',
    376 	    ep->mode & S_IWUSR ? 'w' : '-',
    377 	    ep->mode & S_IXUSR ? 'x' : '-',
    378 	    ep->mode & S_IRGRP ? 'r' : '-',
    379 	    ep->mode & S_IWGRP ? 'w' : '-',
    380 	    ep->mode & S_IXGRP ? 'x' : '-',
    381 	    ep->mode & S_IROTH ? 'r' : '-',
    382 	    ep->mode & S_IWOTH ? 'w' : '-',
    383 	    ep->mode & S_IXOTH ? 'x' : '-',
    384 	    size_human(ep->size),
    385 	    ep->date);
    386 	/*attroff(COLOR_PAIR(C_STA));*/
    387 }
    388 
    389 static void
    390 notify(const char *fmt, ...)
    391 {
    392 	va_list args;
    393 	char buf[XMAX]; /* Line shouldn't get longer than `XMAX`. */
    394 
    395 	move(YMAX - 1, 0);
    396 	clrtoeol();
    397 	va_start(args, fmt);
    398 	(void)vsnprintf(buf, XMAX, fmt, args);
    399 	va_end(args);
    400 	addstr(buf);
    401 	f_redraw = 1;
    402 }
    403 
    404 static int
    405 confirmact(const char *str)
    406 {
    407 	char c;
    408 
    409 	if (f_noconfirm)
    410 		return (1);
    411 	TIMEOUT_CLEAR();
    412 	/*notify(msgs[MSG_EXEC], str);*/
    413 	c = getch();
    414 	TIMEOUT_SET();
    415 
    416 	return (c == 'y');
    417 }
    418 
    419 static char *
    420 escape(const char *str)
    421 {
    422 	char *buf;
    423 	size_t sz;
    424 	int i = 0;
    425 
    426 	sz = strlen(str);
    427 	buf = emalloc(sz + 1);
    428 
    429 	for (; *str; str++) {
    430 		switch (*str) {
    431 		case ' ':  /* FALLTHROUGH */
    432 		case '\'':
    433 		case '(':
    434 		case ')':
    435 			buf = erealloc(buf, ++sz + 1); 
    436 			buf[i++] = '\\';
    437 		}
    438 		buf[i++] = *str;
    439 	}
    440 	buf[i] = '\0';
    441 
    442 	return (buf);
    443 }
    444 
    445 static char *
    446 size_human(size_t sz)
    447 {
    448 	static char buf[12];
    449 	int i = 0;
    450 
    451 	for (; sz > 1024; i++)
    452 		sz >>= 10;
    453 	(void)snprintf(buf, sizeof(buf), "%ld%c", sz, "BKMGTPEZY"[i]);
    454 
    455 	return (buf);
    456 }
    457 
    458 static void
    459 str_print(char *str)
    460 {
    461 	char *s;
    462 
    463 	for (s = str; *s != '\0' && (s - str) < XMAX; s++) {
    464 		/* Print a ~ if we have no more space left. */
    465 		/* FIXME: put it outside the loop? */
    466 		if (getcurx(stdscr) == XMAX - 1) {
    467 			addch('~');
    468 			break;
    469 		}
    470 		addch(*s);
    471 	}
    472 }
    473 
    474 static char *
    475 prompt_read(const char *msg)
    476 {
    477 	char buf[BUFSIZ], *str;
    478 	int len = 0, c;
    479 
    480 	notify(msg);
    481 	echo();
    482 	curs_set(1);
    483 	TIMEOUT_CLEAR();
    484 
    485 	while ((c = getch()) != '\n') {
    486 		switch (c) {
    487 		case KEY_BACKSPACE:	/* FALLTHROUGH */
    488 		case KEY_DC:
    489 		case '\b':
    490 			if (len > 0)
    491 				len--;
    492 			break;
    493 		case ESC:
    494 			str = NULL;
    495 			goto exit;
    496 		case ERR:
    497 			break;
    498 		default:
    499 			buf[len++] = c;
    500 		}
    501 	}
    502 
    503 	buf[len] = '\0';
    504 	str = estrdup(buf);
    505 exit:
    506 	curs_set(0);
    507 	noecho();
    508 	TIMEOUT_SET();
    509 
    510 	return (str);
    511 }
    512 
    513 /* TODO: use nanosleep(2) */
    514 static void
    515 xdelay(int delay)
    516 {
    517 	refresh();
    518 	(void)usleep(delay * 1000);
    519 }
    520 
    521 static void
    522 sel_clump(void)
    523 {
    524 	struct pane *p;
    525 
    526 	p = &panes[selpane];
    527 	if (p->sel < 0)
    528 		p->sel = 0;
    529 	else if (p->sel > p->nents - 1)
    530 		p->sel = p->nents - 1;
    531 }
    532 
    533 static void *
    534 emalloc(size_t nb)
    535 {
    536 	void *p;
    537 
    538 	if ((p = malloc(nb)) == NULL)
    539 		err(1, "malloc");
    540 	return (p);
    541 }
    542 
    543 static void *
    544 erealloc(void *p, size_t nb)
    545 {
    546 	if ((p = realloc(p, nb)) == NULL)
    547 		err(1, "realloc");
    548 	return (p);
    549 }
    550 
    551 static char *
    552 estrdup(const char *s)
    553 {
    554 	char *p;
    555 
    556 	if ((p = strdup(s)) == NULL)
    557 		err(1, "strdup");
    558 	return (p);
    559 }
    560 
    561 static void
    562 sighandler(int sig)
    563 {
    564 	switch (sig) {
    565 	case SIGINT: /* FALLTHROUGH */
    566 	case SIGTERM:
    567 	case SIGUSR1:
    568 		f_running = 0;
    569 	case SIGWINCH:
    570 		f_redraw = 1;
    571 		break;
    572 	}
    573 }
    574 
    575 static void
    576 cleanup(void)
    577 {
    578 	int i = 0;
    579 
    580 	for (i = 0; i < npanes; i++)
    581 		free(panes[i].ents);
    582 	free(panes);
    583 	endwin();
    584 }
    585 
    586 static void
    587 chlevel(const arg *arg)
    588 {
    589 	struct pane *p;
    590 	union arg a;
    591 
    592 	p = &panes[selpane];
    593 	switch (arg->i) {
    594 	case S_LEFT:
    595 		a.s = "..";
    596 		break;
    597 	case S_RIGHT:
    598 		if (p->ents[p->sel].flags & DT_DIR) {
    599 			a.s = p->ents[p->sel].name;
    600 		} else {
    601 			/* TODO: file */
    602 			return;
    603 		}
    604 		break;
    605 	default:
    606 		return;
    607 	}
    608 	cd(&a);
    609 }
    610 
    611 static void
    612 scrolldir(const arg *arg)
    613 {
    614 	struct pane *p;
    615 
    616 	p = &panes[selpane];
    617 	switch (arg->i) {
    618 	case S_UP:
    619 		p->sel--;
    620 		break;
    621 	case S_DOWN:
    622 		p->sel++;
    623 		break;
    624 	/*case S_PGUP:*/
    625 		/*p->sel >>= 1;*/
    626 		/*break;*/
    627 	/*case S_PGDOWN:*/
    628 		/*p->sel <<= 1;*/
    629 		/*break;*/
    630 	case S_TOP:
    631 		p->sel = 0;
    632 		break;
    633 	case S_BOTTOM:
    634 		p->sel = p->nents - 1;
    635 		break;
    636 	}
    637 	sel_clump();
    638 }
    639 
    640 static void
    641 cyclepane(const arg *arg)
    642 {
    643 	UNUSED(arg);
    644 	selpane++;
    645 	selpane %= npanes;
    646 	f_redraw = 1;
    647 }
    648 
    649 static void
    650 setflag(const arg *arg)
    651 {
    652 	if (arg->i & F_SHOWALL)
    653 		f_showall ^= 1;
    654 	if (arg->i & F_INFO)
    655 		f_info ^= 1;
    656 	if (arg->i & F_REDRAW)
    657 		;	/* nothing */
    658 	if (arg->i & F_QUIT)
    659 		f_running = 0;
    660 	f_redraw = 1;
    661 }
    662 
    663 /* FIXME: have to wait until xdelay returns */
    664 static void
    665 cd(const arg *arg)
    666 {
    667 	char *path;
    668 	char apath[PATH_MAX];
    669 
    670 	if ((path = getenv(arg->s)) == NULL)
    671 		path = (char *)arg->s;
    672 	/* 
    673 	 * Using this `apath` temporary variable we avoid `entries_read`
    674 	 * crashing when the path is wrong.
    675 	 */
    676 	if (realpath(path, apath) == NULL)
    677 		goto fail;
    678 	if (chdir(path) < 0)
    679 		goto fail;
    680 	(void)strlcpy(panes[selpane].curdir, apath, sizeof(apath));
    681 	f_redraw = 1;
    682 	return;
    683 fail:
    684 	notify(msgs[MSG_FAIL], strerror(errno));
    685 	xdelay(DELAY_MS << 2);
    686 }
    687 
    688 static void
    689 run(const arg *arg)
    690 {
    691 }
    692 
    693 static void
    694 sort(const arg *arg)
    695 {
    696 	struct pane *p;
    697 
    698 	UNUSED(arg);
    699 	p = &panes[selpane];
    700 	notify(msgs[MSG_SORT]);
    701 
    702 	switch (getch()) {
    703 	case 'n':
    704 		f_namesort = 1;
    705 		f_sizesort = f_datesort = 0;
    706 		break;
    707 	case 's':
    708 		f_sizesort = 1;
    709 		f_namesort = f_datesort = 0;
    710 		break;
    711 	case 'd':
    712 		f_datesort = 1;
    713 		f_namesort = f_sizesort = 0;
    714 	case 'r':
    715 		f_revsort ^= 1;
    716 		break;
    717 	}
    718 
    719 	if (!f_revsort) {
    720 		if (f_namesort)
    721 			p->sortfn = namecmp;
    722 		else if (f_sizesort)
    723 			p->sortfn = sizecmp;
    724 		else if (f_datesort)
    725 			p->sortfn = datecmp;
    726 	} else {
    727 		if (f_namesort)
    728 			p->sortfn = revnamecmp;
    729 		else if (f_sizesort)
    730 			p->sortfn = revsizecmp;
    731 		else if (f_datesort)
    732 			p->sortfn = revdatecmp;
    733 	}
    734 	ENTSORT(p->ents, p->nents, p->sortfn);
    735 }
    736 
    737 int
    738 main(int argc, char *argv[])
    739 {
    740 	struct pane *p;
    741 	struct sigaction sa;
    742 	char cwd[PATH_MAX] = {0};
    743 	int ch, i;
    744 
    745 	argv0 = *argv;
    746 	(void)setlocale(LC_ALL, "");
    747 	if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO))
    748 		err(1, "isatty");
    749 
    750 	while ((ch = getopt(argc, argv, "aiv")) != -1) {
    751 		switch (ch) {
    752 		case 'a':
    753 			f_showall = 1;
    754 			break;
    755 		case 'i':
    756 			f_info = 1;
    757 			break;
    758 		case 'v':
    759 			fprintf(stderr, "%s-"VERSION"\n", argv0);
    760 			exit(1);
    761 		case '?': /* FALLTHROUGH */
    762 		default:
    763 			fprintf(stderr, "usage: %s [-aiv]\n", argv0);
    764 			exit(1);
    765 		}
    766 	}
    767 	argc -= optind;
    768 	argv += optind;
    769 	/* TODO: add [dir] */
    770 
    771 	(void)memset(&sa, 0, sizeof(sa));
    772 	(void)sigfillset(&sa.sa_mask);
    773 	sa.sa_handler = sighandler;
    774 	if (sigaction(SIGINT, &sa, NULL) < 0)
    775 		err(1, "sigaction(SIGINT)");
    776 	if (sigaction(SIGTERM, &sa, NULL) < 0)
    777 		err(1, "sigaction(SIGTERM)");
    778 	if (sigaction(SIGWINCH, &sa, NULL) < 0)
    779 		err(1, "sigaction(SIGWINCH)");
    780 
    781 	curses_init();
    782 
    783 	if (npanes < 1)
    784 		npanes = 1;
    785 	else if (npanes > 9)
    786 		npanes = 9;
    787 
    788 	f_redraw = 1;
    789 	f_namesort = 1;
    790 	panes = emalloc(npanes * sizeof(struct pane));
    791 	for (i = 0; i < npanes; i++) {
    792 		panes[i].ents = NULL;
    793 		panes[i].nents = 0;
    794 		panes[i].sel = 0;
    795 		panes[i].sortfn = namecmp;
    796 		(void)strlcpy(panes[i].curdir, getcwd(cwd, sizeof(cwd)),
    797 		    sizeof(panes[i].curdir));
    798 	}
    799 
    800 	while (f_running) {
    801 		p = &panes[selpane];
    802 		erase();
    803 		if (f_redraw) {
    804 			free(p->ents);
    805 			if ((p->ents = entries_read(p->curdir)) == NULL)
    806 				errx(1, "entries_read: %s", p->curdir);
    807 			sel_clump();
    808 			f_redraw = 0;
    809 			/* XXX: what? */
    810 			refresh();
    811 		}
    812 		pane_print(p);
    813 		/* TODO: use kqueue(2) and inotify */
    814 		if ((ch = getch()) != ERR) {
    815 			for (i = 0; i < LEN(keys); i++)
    816 				if (ch == keys[i].key)
    817 					keys[i].func(&keys[i].arg);
    818 			ch -= '0';
    819 			if (ch > 0 && ch <= npanes) {
    820 				selpane = ch - 1;
    821 				f_redraw = 1;
    822 			}
    823 		}
    824 	}
    825 	cleanup();
    826 
    827 	return (0);
    828 }