2389 lines
51 KiB
C
2389 lines
51 KiB
C
#include <sys/ioctl.h>
|
|
#include <sys/select.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <locale.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <termios.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <wchar.h>
|
|
|
|
#include "util.h"
|
|
|
|
/* curses */
|
|
#ifndef SFEED_MINICURSES
|
|
#include <curses.h>
|
|
#include <term.h>
|
|
#else
|
|
#include "minicurses.h"
|
|
#endif
|
|
|
|
#define LEN(a) sizeof((a))/sizeof((a)[0])
|
|
#define MAX(a,b) ((a) > (b) ? (a) : (b))
|
|
#define MIN(a,b) ((a) < (b) ? (a) : (b))
|
|
|
|
#ifndef SFEED_DUMBTERM
|
|
#define SCROLLBAR_SYMBOL_BAR "\xe2\x94\x82" /* symbol: "light vertical" */
|
|
#define SCROLLBAR_SYMBOL_TICK " "
|
|
#define LINEBAR_SYMBOL_BAR "\xe2\x94\x80" /* symbol: "light horizontal" */
|
|
#define LINEBAR_SYMBOL_RIGHT "\xe2\x94\xa4" /* symbol: "light vertical and left" */
|
|
#else
|
|
#define SCROLLBAR_SYMBOL_BAR "|" /* symbol: "light vertical" */
|
|
#define SCROLLBAR_SYMBOL_TICK " "
|
|
#define LINEBAR_SYMBOL_BAR "-" /* symbol: "light horizontal" */
|
|
#define LINEBAR_SYMBOL_RIGHT "|" /* symbol: "light vertical and left" */
|
|
#endif
|
|
|
|
/* color-theme */
|
|
#ifndef SFEED_THEME
|
|
#define SFEED_THEME "themes/mono.h"
|
|
#endif
|
|
#include SFEED_THEME
|
|
|
|
enum {
|
|
ATTR_RESET = 0, ATTR_BOLD_ON = 1, ATTR_FAINT_ON = 2, ATTR_REVERSE_ON = 7
|
|
};
|
|
|
|
enum Layout {
|
|
LayoutVertical = 0, LayoutHorizontal, LayoutMonocle, LayoutLast
|
|
};
|
|
|
|
enum Pane { PaneFeeds, PaneItems, PaneLast };
|
|
|
|
struct win {
|
|
int width; /* absolute width of the window */
|
|
int height; /* absolute height of the window */
|
|
int dirty; /* needs draw update: clears screen */
|
|
};
|
|
|
|
struct row {
|
|
char *text; /* text string, optional if using row_format() callback */
|
|
int bold;
|
|
void *data; /* data binding */
|
|
};
|
|
|
|
struct pane {
|
|
int x; /* absolute x position on the screen */
|
|
int y; /* absolute y position on the screen */
|
|
int width; /* absolute width of the pane */
|
|
int height; /* absolute height of the pane, should be > 0 */
|
|
off_t pos; /* focused row position */
|
|
struct row *rows;
|
|
size_t nrows; /* total amount of rows */
|
|
int focused; /* has focus or not */
|
|
int hidden; /* is visible or not */
|
|
int dirty; /* needs draw update */
|
|
/* (optional) callback functions */
|
|
struct row *(*row_get)(struct pane *, off_t);
|
|
char *(*row_format)(struct pane *, struct row *);
|
|
int (*row_match)(struct pane *, struct row *, const char *);
|
|
};
|
|
|
|
struct scrollbar {
|
|
int tickpos;
|
|
int ticksize;
|
|
int x; /* absolute x position on the screen */
|
|
int y; /* absolute y position on the screen */
|
|
int size; /* absolute size of the bar, should be > 0 */
|
|
int focused; /* has focus or not */
|
|
int hidden; /* is visible or not */
|
|
int dirty; /* needs draw update */
|
|
};
|
|
|
|
struct statusbar {
|
|
int x; /* absolute x position on the screen */
|
|
int y; /* absolute y position on the screen */
|
|
int width; /* absolute width of the bar */
|
|
char *text; /* data */
|
|
int hidden; /* is visible or not */
|
|
int dirty; /* needs draw update */
|
|
};
|
|
|
|
struct linebar {
|
|
int x; /* absolute x position on the screen */
|
|
int y; /* absolute y position on the screen */
|
|
int width; /* absolute width of the line */
|
|
int hidden; /* is visible or not */
|
|
int dirty; /* needs draw update */
|
|
};
|
|
|
|
/* /UI */
|
|
|
|
struct item {
|
|
char *fields[FieldLast];
|
|
char *line; /* allocated split line */
|
|
/* field to match new items, if link is set match on link, else on id */
|
|
char *matchnew;
|
|
time_t timestamp;
|
|
int timeok;
|
|
int isnew;
|
|
off_t offset; /* line offset in file for lazyload */
|
|
};
|
|
|
|
struct urls {
|
|
char **items; /* array of URLs */
|
|
size_t len; /* amount of items */
|
|
size_t cap; /* available capacity */
|
|
};
|
|
|
|
struct items {
|
|
struct item *items; /* array of items */
|
|
size_t len; /* amount of items */
|
|
size_t cap; /* available capacity */
|
|
};
|
|
|
|
void alldirty(void);
|
|
void cleanup(void);
|
|
void draw(void);
|
|
int getsidebarsize(void);
|
|
void markread(struct pane *, off_t, off_t, int);
|
|
void pane_draw(struct pane *);
|
|
void sighandler(int);
|
|
void updategeom(void);
|
|
void updatesidebar(void);
|
|
void urls_free(struct urls *);
|
|
int urls_hasmatch(struct urls *, const char *);
|
|
void urls_read(struct urls *, const char *);
|
|
|
|
static struct linebar linebar;
|
|
static struct statusbar statusbar;
|
|
static struct pane panes[PaneLast];
|
|
static struct scrollbar scrollbars[PaneLast]; /* each pane has a scrollbar */
|
|
static struct win win;
|
|
static size_t selpane;
|
|
/* fixed sidebar size, < 0 is automatic */
|
|
static int fixedsidebarsizes[LayoutLast] = { -1, -1, -1 };
|
|
static int layout = LayoutVertical, prevlayout = LayoutVertical;
|
|
static int onlynew = 0; /* show only new in sidebar */
|
|
static int usemouse = 1; /* use xterm mouse tracking */
|
|
|
|
static struct termios tsave; /* terminal state at startup */
|
|
static struct termios tcur;
|
|
static int devnullfd;
|
|
static int istermsetup, needcleanup;
|
|
|
|
static struct feed *feeds;
|
|
static struct feed *curfeed;
|
|
static size_t nfeeds; /* amount of feeds */
|
|
static time_t comparetime;
|
|
struct urls urls;
|
|
static char *urlfile;
|
|
|
|
volatile sig_atomic_t state_sigchld = 0, state_sighup = 0, state_sigint = 0;
|
|
volatile sig_atomic_t state_sigterm = 0, state_sigwinch = 0;
|
|
|
|
static char *plumbercmd = "xdg-open"; /* env variable: $SFEED_PLUMBER */
|
|
static char *pipercmd = "sfeed_content"; /* env variable: $SFEED_PIPER */
|
|
static char *yankercmd = "xclip -r"; /* env variable: $SFEED_YANKER */
|
|
static char *markreadcmd = "sfeed_markread read"; /* env variable: $SFEED_MARK_READ */
|
|
static char *markunreadcmd = "sfeed_markread unread"; /* env variable: $SFEED_MARK_UNREAD */
|
|
static char *cmdenv; /* env variable: $SFEED_AUTOCMD */
|
|
static int plumberia = 0; /* env variable: $SFEED_PLUMBER_INTERACTIVE */
|
|
static int piperia = 1; /* env variable: $SFEED_PIPER_INTERACTIVE */
|
|
static int yankeria = 0; /* env variable: $SFEED_YANKER_INTERACTIVE */
|
|
static int lazyload = 0; /* env variable: $SFEED_LAZYLOAD */
|
|
|
|
int
|
|
ttywritef(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
int n;
|
|
|
|
va_start(ap, fmt);
|
|
n = vfprintf(stdout, fmt, ap);
|
|
va_end(ap);
|
|
fflush(stdout);
|
|
|
|
return n;
|
|
}
|
|
|
|
int
|
|
ttywrite(const char *s)
|
|
{
|
|
if (!s)
|
|
return 0; /* for tparm() returning NULL */
|
|
return write(1, s, strlen(s));
|
|
}
|
|
|
|
/* Print to stderr, call cleanup() and _exit(). */
|
|
__dead void
|
|
die(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
int saved_errno;
|
|
|
|
saved_errno = errno;
|
|
cleanup();
|
|
|
|
va_start(ap, fmt);
|
|
vfprintf(stderr, fmt, ap);
|
|
va_end(ap);
|
|
|
|
if (saved_errno)
|
|
fprintf(stderr, ": %s", strerror(saved_errno));
|
|
putc('\n', stderr);
|
|
fflush(stderr);
|
|
|
|
_exit(1);
|
|
}
|
|
|
|
void *
|
|
erealloc(void *ptr, size_t size)
|
|
{
|
|
void *p;
|
|
|
|
if (!(p = realloc(ptr, size)))
|
|
die("realloc");
|
|
return p;
|
|
}
|
|
|
|
void *
|
|
ecalloc(size_t nmemb, size_t size)
|
|
{
|
|
void *p;
|
|
|
|
if (!(p = calloc(nmemb, size)))
|
|
die("calloc");
|
|
return p;
|
|
}
|
|
|
|
char *
|
|
estrdup(const char *s)
|
|
{
|
|
char *p;
|
|
|
|
if (!(p = strdup(s)))
|
|
die("strdup");
|
|
return p;
|
|
}
|
|
|
|
/* Wrapper for tparm() which allows NULL parameter for str. */
|
|
char *
|
|
tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6,
|
|
long p7, long p8, long p9)
|
|
{
|
|
if (!str)
|
|
return NULL;
|
|
/* some tparm() implementations have char *, some have const char * */
|
|
return tparm((char *)str, p1, p2, p3, p4, p5, p6, p7, p8, p9);
|
|
}
|
|
|
|
/* Counts column width of character string. */
|
|
size_t
|
|
colw(const char *s)
|
|
{
|
|
wchar_t wc;
|
|
size_t col = 0, i, slen;
|
|
int inc, rl, w;
|
|
|
|
slen = strlen(s);
|
|
for (i = 0; i < slen; i += inc) {
|
|
inc = 1; /* next byte */
|
|
if ((unsigned char)s[i] < 32) {
|
|
continue;
|
|
} else if ((unsigned char)s[i] >= 127) {
|
|
rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
|
|
inc = rl;
|
|
if (rl < 0) {
|
|
mbtowc(NULL, NULL, 0); /* reset state */
|
|
inc = 1; /* invalid, seek next byte */
|
|
w = 1; /* replacement char is one width */
|
|
} else if ((w = wcwidth(wc)) == -1) {
|
|
continue;
|
|
}
|
|
col += w;
|
|
} else {
|
|
col++;
|
|
}
|
|
}
|
|
return col;
|
|
}
|
|
|
|
/* Format `len` columns of characters. If string is shorter pad the rest
|
|
with characters `pad`. */
|
|
int
|
|
utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad)
|
|
{
|
|
wchar_t wc;
|
|
size_t col = 0, i, slen, siz = 0;
|
|
int inc, rl, w;
|
|
|
|
if (!bufsiz)
|
|
return -1;
|
|
if (!len) {
|
|
buf[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
slen = strlen(s);
|
|
for (i = 0; i < slen; i += inc) {
|
|
inc = 1; /* next byte */
|
|
if ((unsigned char)s[i] < 32)
|
|
continue;
|
|
|
|
rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
|
|
inc = rl;
|
|
if (rl < 0) {
|
|
mbtowc(NULL, NULL, 0); /* reset state */
|
|
inc = 1; /* invalid, seek next byte */
|
|
w = 1; /* replacement char is one width */
|
|
} else if ((w = wcwidth(wc)) == -1) {
|
|
continue;
|
|
}
|
|
|
|
if (col + w > len || (col + w == len && s[i + inc])) {
|
|
if (siz + 4 >= bufsiz)
|
|
return -1;
|
|
memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1);
|
|
siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1;
|
|
buf[siz] = '\0';
|
|
col++;
|
|
break;
|
|
} else if (rl < 0) {
|
|
if (siz + 4 >= bufsiz)
|
|
return -1;
|
|
memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1);
|
|
siz += sizeof(UTF_INVALID_SYMBOL) - 1;
|
|
buf[siz] = '\0';
|
|
col++;
|
|
continue;
|
|
}
|
|
if (siz + inc + 1 >= bufsiz)
|
|
return -1;
|
|
memcpy(&buf[siz], &s[i], inc);
|
|
siz += inc;
|
|
buf[siz] = '\0';
|
|
col += w;
|
|
}
|
|
|
|
len -= col;
|
|
if (siz + len + 1 >= bufsiz)
|
|
return -1;
|
|
memset(&buf[siz], pad, len);
|
|
siz += len;
|
|
buf[siz] = '\0';
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
resetstate(void)
|
|
{
|
|
ttywrite("\x1b""c"); /* rs1: reset title and state */
|
|
}
|
|
|
|
void
|
|
updatetitle(void)
|
|
{
|
|
unsigned long totalnew = 0, total = 0;
|
|
size_t i;
|
|
|
|
for (i = 0; i < nfeeds; i++) {
|
|
totalnew += feeds[i].totalnew;
|
|
total += feeds[i].total;
|
|
}
|
|
ttywritef("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, total);
|
|
}
|
|
|
|
void
|
|
appmode(int on)
|
|
{
|
|
ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
mousemode(int on)
|
|
{
|
|
ttywrite(on ? "\x1b[?1000h" : "\x1b[?1000l"); /* xterm X10 mouse mode */
|
|
ttywrite(on ? "\x1b[?1006h" : "\x1b[?1006l"); /* extended SGR mouse mode */
|
|
}
|
|
|
|
void
|
|
cursormode(int on)
|
|
{
|
|
ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
cursormove(int x, int y)
|
|
{
|
|
ttywrite(tparmnull(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
cursorsave(void)
|
|
{
|
|
/* do not save the cursor if it won't be restored anyway */
|
|
if (cursor_invisible)
|
|
ttywrite(tparmnull(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
cursorrestore(void)
|
|
{
|
|
/* if the cursor cannot be hidden then move to a consistent position */
|
|
if (cursor_invisible)
|
|
ttywrite(tparmnull(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
else
|
|
cursormove(0, 0);
|
|
}
|
|
|
|
void
|
|
attrmode(int mode)
|
|
{
|
|
switch (mode) {
|
|
case ATTR_RESET:
|
|
ttywrite(tparmnull(exit_attribute_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
break;
|
|
case ATTR_BOLD_ON:
|
|
ttywrite(tparmnull(enter_bold_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
break;
|
|
case ATTR_FAINT_ON:
|
|
ttywrite(tparmnull(enter_dim_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
break;
|
|
case ATTR_REVERSE_ON:
|
|
ttywrite(tparmnull(enter_reverse_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
cleareol(void)
|
|
{
|
|
ttywrite(tparmnull(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
clearscreen(void)
|
|
{
|
|
ttywrite(tparmnull(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
void
|
|
cleanup(void)
|
|
{
|
|
struct sigaction sa;
|
|
|
|
if (!needcleanup)
|
|
return;
|
|
needcleanup = 0;
|
|
|
|
if (istermsetup) {
|
|
resetstate();
|
|
cursormode(1);
|
|
appmode(0);
|
|
clearscreen();
|
|
|
|
if (usemouse)
|
|
mousemode(0);
|
|
}
|
|
|
|
/* restore terminal settings */
|
|
tcsetattr(0, TCSANOW, &tsave);
|
|
|
|
memset(&sa, 0, sizeof(sa));
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
|
|
sa.sa_handler = SIG_DFL;
|
|
sigaction(SIGWINCH, &sa, NULL);
|
|
}
|
|
|
|
void
|
|
win_update(struct win *w, int width, int height)
|
|
{
|
|
if (width != w->width || height != w->height)
|
|
w->dirty = 1;
|
|
w->width = width;
|
|
w->height = height;
|
|
}
|
|
|
|
void
|
|
resizewin(void)
|
|
{
|
|
struct winsize winsz;
|
|
int width, height;
|
|
|
|
if (ioctl(1, TIOCGWINSZ, &winsz) != -1) {
|
|
width = winsz.ws_col > 0 ? winsz.ws_col : 80;
|
|
height = winsz.ws_row > 0 ? winsz.ws_row : 24;
|
|
win_update(&win, width, height);
|
|
}
|
|
if (win.dirty)
|
|
alldirty();
|
|
}
|
|
|
|
void
|
|
init(void)
|
|
{
|
|
struct sigaction sa;
|
|
int errret = 1;
|
|
|
|
needcleanup = 1;
|
|
|
|
tcgetattr(0, &tsave);
|
|
memcpy(&tcur, &tsave, sizeof(tcur));
|
|
tcur.c_lflag &= ~(ECHO|ICANON);
|
|
tcur.c_cc[VMIN] = 1;
|
|
tcur.c_cc[VTIME] = 0;
|
|
tcsetattr(0, TCSANOW, &tcur);
|
|
|
|
if (!istermsetup &&
|
|
(setupterm(NULL, 1, &errret) != OK || errret != 1)) {
|
|
errno = 0;
|
|
die("setupterm: terminfo database or entry for $TERM not found");
|
|
}
|
|
istermsetup = 1;
|
|
resizewin();
|
|
|
|
appmode(1);
|
|
cursormode(0);
|
|
|
|
if (usemouse)
|
|
mousemode(1);
|
|
|
|
memset(&sa, 0, sizeof(sa));
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
|
|
sa.sa_handler = sighandler;
|
|
sigaction(SIGCHLD, &sa, NULL);
|
|
sigaction(SIGHUP, &sa, NULL);
|
|
sigaction(SIGINT, &sa, NULL);
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
sigaction(SIGWINCH, &sa, NULL);
|
|
}
|
|
|
|
void
|
|
processexit(pid_t pid, int interactive)
|
|
{
|
|
struct sigaction sa;
|
|
|
|
if (interactive) {
|
|
memset(&sa, 0, sizeof(sa));
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
|
|
|
|
/* ignore SIGINT (^C) in parent for interactive applications */
|
|
sa.sa_handler = SIG_IGN;
|
|
sigaction(SIGINT, &sa, NULL);
|
|
|
|
sa.sa_flags = 0; /* SIGTERM: interrupt waitpid(), no SA_RESTART */
|
|
sa.sa_handler = sighandler;
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
|
|
/* wait for process to change state, ignore errors */
|
|
waitpid(pid, NULL, 0);
|
|
|
|
init();
|
|
updatesidebar();
|
|
updategeom();
|
|
updatetitle();
|
|
}
|
|
}
|
|
|
|
/* Pipe item line or item field to a program.
|
|
If `field` is -1 then pipe the TSV line, else a specified field.
|
|
if `interactive` is 1 then cleanup and restore the tty and wait on the
|
|
process.
|
|
if 0 then don't do that and also write stdout and stderr to /dev/null. */
|
|
void
|
|
pipeitem(const char *cmd, struct item *item, int field, int interactive)
|
|
{
|
|
FILE *fp;
|
|
pid_t pid;
|
|
int i, status;
|
|
|
|
if (interactive)
|
|
cleanup();
|
|
|
|
switch ((pid = fork())) {
|
|
case -1:
|
|
die("fork");
|
|
case 0:
|
|
if (!interactive) {
|
|
dup2(devnullfd, 1); /* stdout */
|
|
dup2(devnullfd, 2); /* stderr */
|
|
}
|
|
|
|
errno = 0;
|
|
if (!(fp = popen(cmd, "w")))
|
|
die("popen: %s", cmd);
|
|
if (field == -1) {
|
|
for (i = 0; i < FieldLast; i++) {
|
|
if (i)
|
|
putc('\t', fp);
|
|
fputs(item->fields[i], fp);
|
|
}
|
|
} else {
|
|
fputs(item->fields[field], fp);
|
|
}
|
|
putc('\n', fp);
|
|
status = pclose(fp);
|
|
status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
|
|
_exit(status);
|
|
default:
|
|
processexit(pid, interactive);
|
|
}
|
|
}
|
|
|
|
void
|
|
forkexec(char *argv[], int interactive)
|
|
{
|
|
pid_t pid;
|
|
|
|
if (interactive)
|
|
cleanup();
|
|
|
|
switch ((pid = fork())) {
|
|
case -1:
|
|
die("fork");
|
|
case 0:
|
|
if (!interactive) {
|
|
dup2(devnullfd, 0); /* stdin */
|
|
dup2(devnullfd, 1); /* stdout */
|
|
dup2(devnullfd, 2); /* stderr */
|
|
}
|
|
if (execvp(argv[0], argv) == -1)
|
|
_exit(1);
|
|
default:
|
|
processexit(pid, interactive);
|
|
}
|
|
}
|
|
|
|
struct row *
|
|
pane_row_get(struct pane *p, off_t pos)
|
|
{
|
|
if (pos < 0 || pos >= p->nrows)
|
|
return NULL;
|
|
|
|
if (p->row_get)
|
|
return p->row_get(p, pos);
|
|
return p->rows + pos;
|
|
}
|
|
|
|
char *
|
|
pane_row_text(struct pane *p, struct row *row)
|
|
{
|
|
/* custom formatter */
|
|
if (p->row_format)
|
|
return p->row_format(p, row);
|
|
return row->text;
|
|
}
|
|
|
|
int
|
|
pane_row_match(struct pane *p, struct row *row, const char *s)
|
|
{
|
|
if (p->row_match)
|
|
return p->row_match(p, row, s);
|
|
return (strcasestr(pane_row_text(p, row), s) != NULL);
|
|
}
|
|
|
|
void
|
|
pane_row_draw(struct pane *p, off_t pos, int selected)
|
|
{
|
|
struct row *row;
|
|
|
|
if (p->hidden || !p->width || !p->height ||
|
|
p->x >= win.width || p->y + (pos % p->height) >= win.height)
|
|
return;
|
|
|
|
row = pane_row_get(p, pos);
|
|
|
|
cursorsave();
|
|
cursormove(p->x, p->y + (pos % p->height));
|
|
|
|
if (p->focused)
|
|
THEME_ITEM_FOCUS();
|
|
else
|
|
THEME_ITEM_NORMAL();
|
|
if (row && row->bold)
|
|
THEME_ITEM_BOLD();
|
|
if (selected)
|
|
THEME_ITEM_SELECTED();
|
|
if (row) {
|
|
printutf8pad(stdout, pane_row_text(p, row), p->width, ' ');
|
|
fflush(stdout);
|
|
} else {
|
|
ttywritef("%-*.*s", p->width, p->width, "");
|
|
}
|
|
|
|
attrmode(ATTR_RESET);
|
|
cursorrestore();
|
|
}
|
|
|
|
void
|
|
pane_setpos(struct pane *p, off_t pos)
|
|
{
|
|
if (pos < 0)
|
|
pos = 0; /* clamp */
|
|
if (!p->nrows)
|
|
return; /* invalid */
|
|
if (pos >= p->nrows)
|
|
pos = p->nrows - 1; /* clamp */
|
|
if (pos == p->pos)
|
|
return; /* no change */
|
|
|
|
/* is on different scroll region? mark whole pane dirty */
|
|
if (((p->pos - (p->pos % p->height)) / p->height) !=
|
|
((pos - (pos % p->height)) / p->height)) {
|
|
p->dirty = 1;
|
|
} else {
|
|
/* only redraw the 2 dirty rows */
|
|
pane_row_draw(p, p->pos, 0);
|
|
pane_row_draw(p, pos, 1);
|
|
}
|
|
p->pos = pos;
|
|
}
|
|
|
|
void
|
|
pane_scrollpage(struct pane *p, int pages)
|
|
{
|
|
off_t pos;
|
|
|
|
if (pages < 0) {
|
|
pos = p->pos - (-pages * p->height);
|
|
pos -= (p->pos % p->height);
|
|
pos += p->height - 1;
|
|
pane_setpos(p, pos);
|
|
} else if (pages > 0) {
|
|
pos = p->pos + (pages * p->height);
|
|
if ((p->pos % p->height))
|
|
pos -= (p->pos % p->height);
|
|
pane_setpos(p, pos);
|
|
}
|
|
}
|
|
|
|
void
|
|
pane_scrolln(struct pane *p, int n)
|
|
{
|
|
pane_setpos(p, p->pos + n);
|
|
}
|
|
|
|
void
|
|
pane_setfocus(struct pane *p, int on)
|
|
{
|
|
if (p->focused != on) {
|
|
p->focused = on;
|
|
p->dirty = 1;
|
|
}
|
|
}
|
|
|
|
void
|
|
pane_draw(struct pane *p)
|
|
{
|
|
off_t pos, y;
|
|
|
|
if (!p->dirty)
|
|
return;
|
|
p->dirty = 0;
|
|
if (p->hidden || !p->width || !p->height)
|
|
return;
|
|
|
|
/* draw visible rows */
|
|
pos = p->pos - (p->pos % p->height);
|
|
for (y = 0; y < p->height; y++)
|
|
pane_row_draw(p, y + pos, (y + pos) == p->pos);
|
|
}
|
|
|
|
void
|
|
setlayout(int n)
|
|
{
|
|
if (layout != LayoutMonocle)
|
|
prevlayout = layout; /* previous non-monocle layout */
|
|
layout = n;
|
|
}
|
|
|
|
void
|
|
updategeom(void)
|
|
{
|
|
int h, w, x = 0, y = 0;
|
|
|
|
panes[PaneFeeds].hidden = layout == LayoutMonocle && (selpane != PaneFeeds);
|
|
panes[PaneItems].hidden = layout == LayoutMonocle && (selpane != PaneItems);
|
|
linebar.hidden = layout != LayoutHorizontal;
|
|
|
|
w = win.width;
|
|
/* always reserve space for statusbar */
|
|
h = MAX(win.height - 1, 1);
|
|
|
|
panes[PaneFeeds].x = x;
|
|
panes[PaneFeeds].y = y;
|
|
|
|
switch (layout) {
|
|
case LayoutVertical:
|
|
panes[PaneFeeds].width = getsidebarsize();
|
|
|
|
x += panes[PaneFeeds].width;
|
|
w -= panes[PaneFeeds].width;
|
|
|
|
/* space for scrollbar if sidebar is visible */
|
|
w--;
|
|
x++;
|
|
|
|
panes[PaneFeeds].height = MAX(h, 1);
|
|
break;
|
|
case LayoutHorizontal:
|
|
panes[PaneFeeds].height = getsidebarsize();
|
|
|
|
h -= panes[PaneFeeds].height;
|
|
y += panes[PaneFeeds].height;
|
|
|
|
linebar.x = 0;
|
|
linebar.y = y;
|
|
linebar.width = win.width;
|
|
|
|
h--;
|
|
y++;
|
|
|
|
panes[PaneFeeds].width = MAX(w - 1, 0);
|
|
break;
|
|
case LayoutMonocle:
|
|
panes[PaneFeeds].height = MAX(h, 1);
|
|
panes[PaneFeeds].width = MAX(w - 1, 0);
|
|
break;
|
|
}
|
|
|
|
panes[PaneItems].x = x;
|
|
panes[PaneItems].y = y;
|
|
panes[PaneItems].width = MAX(w - 1, 0);
|
|
panes[PaneItems].height = MAX(h, 1);
|
|
if (x >= win.width || y + 1 >= win.height)
|
|
panes[PaneItems].hidden = 1;
|
|
|
|
scrollbars[PaneFeeds].x = panes[PaneFeeds].x + panes[PaneFeeds].width;
|
|
scrollbars[PaneFeeds].y = panes[PaneFeeds].y;
|
|
scrollbars[PaneFeeds].size = panes[PaneFeeds].height;
|
|
scrollbars[PaneFeeds].hidden = panes[PaneFeeds].hidden;
|
|
|
|
scrollbars[PaneItems].x = panes[PaneItems].x + panes[PaneItems].width;
|
|
scrollbars[PaneItems].y = panes[PaneItems].y;
|
|
scrollbars[PaneItems].size = panes[PaneItems].height;
|
|
scrollbars[PaneItems].hidden = panes[PaneItems].hidden;
|
|
|
|
statusbar.width = win.width;
|
|
statusbar.x = 0;
|
|
statusbar.y = MAX(win.height - 1, 0);
|
|
|
|
alldirty();
|
|
}
|
|
|
|
void
|
|
scrollbar_setfocus(struct scrollbar *s, int on)
|
|
{
|
|
if (s->focused != on) {
|
|
s->focused = on;
|
|
s->dirty = 1;
|
|
}
|
|
}
|
|
|
|
void
|
|
scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight)
|
|
{
|
|
int tickpos = 0, ticksize = 0;
|
|
|
|
/* do not show a scrollbar if all items fit on the page */
|
|
if (nrows > pageheight) {
|
|
ticksize = s->size / ((double)nrows / (double)pageheight);
|
|
if (ticksize == 0)
|
|
ticksize = 1;
|
|
|
|
tickpos = (pos / (double)nrows) * (double)s->size;
|
|
|
|
/* fixup due to cell precision */
|
|
if (pos + pageheight >= nrows ||
|
|
tickpos + ticksize >= s->size)
|
|
tickpos = s->size - ticksize;
|
|
}
|
|
|
|
if (s->tickpos != tickpos || s->ticksize != ticksize)
|
|
s->dirty = 1;
|
|
s->tickpos = tickpos;
|
|
s->ticksize = ticksize;
|
|
}
|
|
|
|
void
|
|
scrollbar_draw(struct scrollbar *s)
|
|
{
|
|
off_t y;
|
|
|
|
if (!s->dirty)
|
|
return;
|
|
s->dirty = 0;
|
|
if (s->hidden || !s->size || s->x >= win.width || s->y >= win.height)
|
|
return;
|
|
|
|
cursorsave();
|
|
|
|
/* draw bar (not tick) */
|
|
if (s->focused)
|
|
THEME_SCROLLBAR_FOCUS();
|
|
else
|
|
THEME_SCROLLBAR_NORMAL();
|
|
for (y = 0; y < s->size; y++) {
|
|
if (y >= s->tickpos && y < s->tickpos + s->ticksize)
|
|
continue; /* skip tick */
|
|
cursormove(s->x, s->y + y);
|
|
ttywrite(SCROLLBAR_SYMBOL_BAR);
|
|
}
|
|
|
|
/* draw tick */
|
|
if (s->focused)
|
|
THEME_SCROLLBAR_TICK_FOCUS();
|
|
else
|
|
THEME_SCROLLBAR_TICK_NORMAL();
|
|
for (y = s->tickpos; y < s->size && y < s->tickpos + s->ticksize; y++) {
|
|
cursormove(s->x, s->y + y);
|
|
ttywrite(SCROLLBAR_SYMBOL_TICK);
|
|
}
|
|
|
|
attrmode(ATTR_RESET);
|
|
cursorrestore();
|
|
}
|
|
|
|
int
|
|
readch(void)
|
|
{
|
|
unsigned char b;
|
|
fd_set readfds;
|
|
struct timeval tv;
|
|
|
|
if (cmdenv && *cmdenv) {
|
|
b = *(cmdenv++); /* $SFEED_AUTOCMD */
|
|
return (int)b;
|
|
}
|
|
|
|
for (;;) {
|
|
FD_ZERO(&readfds);
|
|
FD_SET(0, &readfds);
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 250000; /* 250ms */
|
|
switch (select(1, &readfds, NULL, NULL, &tv)) {
|
|
case -1:
|
|
if (errno != EINTR)
|
|
die("select");
|
|
return -2; /* EINTR: like a signal */
|
|
case 0:
|
|
return -3; /* time-out */
|
|
}
|
|
|
|
switch (read(0, &b, 1)) {
|
|
case -1: die("read");
|
|
case 0: return EOF;
|
|
default: return (int)b;
|
|
}
|
|
}
|
|
}
|
|
|
|
char *
|
|
lineeditor(void)
|
|
{
|
|
char *input = NULL;
|
|
size_t cap = 0, nchars = 0;
|
|
int ch;
|
|
|
|
if (usemouse)
|
|
mousemode(0);
|
|
for (;;) {
|
|
if (nchars + 2 >= cap) {
|
|
cap = cap ? cap * 2 : 32;
|
|
input = erealloc(input, cap);
|
|
}
|
|
|
|
ch = readch();
|
|
if (ch == EOF || ch == '\r' || ch == '\n') {
|
|
input[nchars] = '\0';
|
|
break;
|
|
} else if (ch == '\b' || ch == 0x7f) {
|
|
if (!nchars)
|
|
continue;
|
|
input[--nchars] = '\0';
|
|
ttywrite("\b \b"); /* back, blank, back */
|
|
} else if (ch >= ' ') {
|
|
input[nchars] = ch;
|
|
input[nchars + 1] = '\0';
|
|
ttywrite(&input[nchars]);
|
|
nchars++;
|
|
} else if (ch < 0) {
|
|
if (state_sigchld) {
|
|
state_sigchld = 0;
|
|
/* wait on child processes so they don't become a zombie */
|
|
while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
|
|
;
|
|
}
|
|
if (state_sigint)
|
|
state_sigint = 0; /* cancel prompt and don't handle this signal */
|
|
else if (state_sighup || state_sigterm)
|
|
; /* cancel prompt and handle these signals */
|
|
else /* no signal, time-out or SIGCHLD or SIGWINCH */
|
|
continue; /* do not cancel: process signal later */
|
|
|
|
free(input);
|
|
input = NULL;
|
|
break; /* cancel prompt */
|
|
}
|
|
}
|
|
if (usemouse)
|
|
mousemode(1);
|
|
return input;
|
|
}
|
|
|
|
char *
|
|
uiprompt(int x, int y, char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *input, buf[32];
|
|
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
|
|
cursorsave();
|
|
cursormove(x, y);
|
|
THEME_INPUT_LABEL();
|
|
ttywrite(buf);
|
|
attrmode(ATTR_RESET);
|
|
|
|
THEME_INPUT_NORMAL();
|
|
cleareol();
|
|
cursormode(1);
|
|
cursormove(x + colw(buf) + 1, y);
|
|
|
|
input = lineeditor();
|
|
attrmode(ATTR_RESET);
|
|
|
|
cursormode(0);
|
|
cursorrestore();
|
|
|
|
return input;
|
|
}
|
|
|
|
void
|
|
linebar_draw(struct linebar *b)
|
|
{
|
|
int i;
|
|
|
|
if (!b->dirty)
|
|
return;
|
|
b->dirty = 0;
|
|
if (b->hidden || !b->width)
|
|
return;
|
|
|
|
cursorsave();
|
|
cursormove(b->x, b->y);
|
|
THEME_LINEBAR();
|
|
for (i = 0; i < b->width - 1; i++)
|
|
ttywrite(LINEBAR_SYMBOL_BAR);
|
|
ttywrite(LINEBAR_SYMBOL_RIGHT);
|
|
attrmode(ATTR_RESET);
|
|
cursorrestore();
|
|
}
|
|
|
|
void
|
|
statusbar_draw(struct statusbar *s)
|
|
{
|
|
if (!s->dirty)
|
|
return;
|
|
s->dirty = 0;
|
|
if (s->hidden || !s->width || s->x >= win.width || s->y >= win.height)
|
|
return;
|
|
|
|
cursorsave();
|
|
cursormove(s->x, s->y);
|
|
THEME_STATUSBAR();
|
|
/* terminals without xenl (eat newline glitch) mess up scrolling when
|
|
using the last cell on the last line on the screen. */
|
|
printutf8pad(stdout, s->text, s->width - (!eat_newline_glitch), ' ');
|
|
fflush(stdout);
|
|
attrmode(ATTR_RESET);
|
|
cursorrestore();
|
|
}
|
|
|
|
void
|
|
statusbar_update(struct statusbar *s, const char *text)
|
|
{
|
|
if (s->text && !strcmp(s->text, text))
|
|
return;
|
|
|
|
free(s->text);
|
|
s->text = estrdup(text);
|
|
s->dirty = 1;
|
|
}
|
|
|
|
/* Line to item, modifies and splits line in-place. */
|
|
int
|
|
linetoitem(char *line, struct item *item)
|
|
{
|
|
char *fields[FieldLast];
|
|
time_t parsedtime;
|
|
|
|
item->line = line;
|
|
parseline(line, fields);
|
|
memcpy(item->fields, fields, sizeof(fields));
|
|
if (urlfile)
|
|
item->matchnew = estrdup(fields[fields[FieldLink][0] ? FieldLink : FieldId]);
|
|
else
|
|
item->matchnew = NULL;
|
|
|
|
parsedtime = 0;
|
|
if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) {
|
|
item->timestamp = parsedtime;
|
|
item->timeok = 1;
|
|
} else {
|
|
item->timestamp = 0;
|
|
item->timeok = 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
feed_items_free(struct items *items)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < items->len; i++) {
|
|
free(items->items[i].line);
|
|
free(items->items[i].matchnew);
|
|
}
|
|
free(items->items);
|
|
items->items = NULL;
|
|
items->len = 0;
|
|
items->cap = 0;
|
|
}
|
|
|
|
void
|
|
feed_items_get(struct feed *f, FILE *fp, struct items *itemsret)
|
|
{
|
|
struct item *item, *items = NULL;
|
|
char *line = NULL;
|
|
size_t cap, i, linesize = 0, nitems;
|
|
ssize_t linelen, n;
|
|
off_t offset;
|
|
|
|
cap = nitems = 0;
|
|
offset = 0;
|
|
for (i = 0; ; i++) {
|
|
if (i + 1 >= cap) {
|
|
cap = cap ? cap * 2 : 16;
|
|
items = erealloc(items, cap * sizeof(struct item));
|
|
}
|
|
if ((n = linelen = getline(&line, &linesize, fp)) > 0) {
|
|
item = &items[i];
|
|
|
|
item->offset = offset;
|
|
offset += linelen;
|
|
|
|
if (line[linelen - 1] == '\n')
|
|
line[--linelen] = '\0';
|
|
|
|
if (lazyload && f->path) {
|
|
linetoitem(line, item);
|
|
|
|
/* data is ignored here, will be lazy-loaded later. */
|
|
item->line = NULL;
|
|
memset(item->fields, 0, sizeof(item->fields));
|
|
} else {
|
|
linetoitem(estrdup(line), item);
|
|
}
|
|
|
|
nitems++;
|
|
}
|
|
if (ferror(fp))
|
|
die("getline: %s", f->name);
|
|
if (n <= 0 || feof(fp))
|
|
break;
|
|
}
|
|
itemsret->items = items;
|
|
itemsret->len = nitems;
|
|
itemsret->cap = cap;
|
|
free(line);
|
|
}
|
|
|
|
void
|
|
updatenewitems(struct feed *f)
|
|
{
|
|
struct pane *p;
|
|
struct row *row;
|
|
struct item *item;
|
|
size_t i;
|
|
|
|
p = &panes[PaneItems];
|
|
p->dirty = 1;
|
|
f->totalnew = 0;
|
|
for (i = 0; i < p->nrows; i++) {
|
|
row = &(p->rows[i]); /* do not use pane_row_get() */
|
|
item = row->data;
|
|
if (urlfile)
|
|
item->isnew = !urls_hasmatch(&urls, item->matchnew);
|
|
else
|
|
item->isnew = (item->timeok && item->timestamp >= comparetime);
|
|
row->bold = item->isnew;
|
|
f->totalnew += item->isnew;
|
|
}
|
|
f->total = p->nrows;
|
|
}
|
|
|
|
void
|
|
feed_load(struct feed *f, FILE *fp)
|
|
{
|
|
/* static, reuse local buffers */
|
|
static struct items items;
|
|
struct pane *p;
|
|
size_t i;
|
|
|
|
feed_items_free(&items);
|
|
feed_items_get(f, fp, &items);
|
|
p = &panes[PaneItems];
|
|
p->pos = 0;
|
|
p->nrows = items.len;
|
|
free(p->rows);
|
|
p->rows = ecalloc(sizeof(p->rows[0]), items.len + 1);
|
|
for (i = 0; i < items.len; i++)
|
|
p->rows[i].data = &(items.items[i]); /* do not use pane_row_get() */
|
|
|
|
updatenewitems(f);
|
|
}
|
|
|
|
void
|
|
feed_count(struct feed *f, FILE *fp)
|
|
{
|
|
char *fields[FieldLast];
|
|
char *line = NULL;
|
|
size_t linesize = 0;
|
|
ssize_t linelen;
|
|
time_t parsedtime;
|
|
|
|
f->totalnew = f->total = 0;
|
|
while ((linelen = getline(&line, &linesize, fp)) > 0) {
|
|
if (line[linelen - 1] == '\n')
|
|
line[--linelen] = '\0';
|
|
parseline(line, fields);
|
|
|
|
if (urlfile) {
|
|
f->totalnew += !urls_hasmatch(&urls, fields[fields[FieldLink][0] ? FieldLink : FieldId]);
|
|
} else {
|
|
parsedtime = 0;
|
|
if (!strtotime(fields[FieldUnixTimestamp], &parsedtime))
|
|
f->totalnew += (parsedtime >= comparetime);
|
|
}
|
|
f->total++;
|
|
}
|
|
if (ferror(fp))
|
|
die("getline: %s", f->name);
|
|
free(line);
|
|
}
|
|
|
|
void
|
|
feed_setenv(struct feed *f)
|
|
{
|
|
if (f && f->path)
|
|
setenv("SFEED_FEED_PATH", f->path, 1);
|
|
else
|
|
unsetenv("SFEED_FEED_PATH");
|
|
}
|
|
|
|
/* Change feed, have one file open, reopen file if needed. */
|
|
void
|
|
feeds_set(struct feed *f)
|
|
{
|
|
if (curfeed) {
|
|
if (curfeed->path && curfeed->fp) {
|
|
fclose(curfeed->fp);
|
|
curfeed->fp = NULL;
|
|
}
|
|
}
|
|
|
|
if (f && f->path) {
|
|
if (!f->fp && !(f->fp = fopen(f->path, "rb")))
|
|
die("fopen: %s", f->path);
|
|
}
|
|
|
|
feed_setenv(f);
|
|
|
|
curfeed = f;
|
|
}
|
|
|
|
void
|
|
feeds_load(struct feed *feeds, size_t nfeeds)
|
|
{
|
|
struct feed *f;
|
|
size_t i;
|
|
|
|
errno = 0;
|
|
if ((comparetime = time(NULL)) == (time_t)-1)
|
|
die("time");
|
|
/* 1 day is old news */
|
|
comparetime -= 86400;
|
|
|
|
for (i = 0; i < nfeeds; i++) {
|
|
f = &feeds[i];
|
|
|
|
if (f->path) {
|
|
if (f->fp) {
|
|
if (fseek(f->fp, 0, SEEK_SET))
|
|
die("fseek: %s", f->path);
|
|
} else {
|
|
if (!(f->fp = fopen(f->path, "rb")))
|
|
die("fopen: %s", f->path);
|
|
}
|
|
}
|
|
if (!f->fp) {
|
|
/* reading from stdin, just recount new */
|
|
if (f == curfeed)
|
|
updatenewitems(f);
|
|
continue;
|
|
}
|
|
|
|
/* load first items, because of first selection or stdin. */
|
|
if (f == curfeed) {
|
|
feed_load(f, f->fp);
|
|
} else {
|
|
feed_count(f, f->fp);
|
|
if (f->path && f->fp) {
|
|
fclose(f->fp);
|
|
f->fp = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* find row position of the feed if visible, else return -1 */
|
|
off_t
|
|
feeds_row_get(struct pane *p, struct feed *f)
|
|
{
|
|
struct row *row;
|
|
struct feed *fr;
|
|
off_t pos;
|
|
|
|
for (pos = 0; pos < p->nrows; pos++) {
|
|
if (!(row = pane_row_get(p, pos)))
|
|
continue;
|
|
fr = row->data;
|
|
if (!strcmp(fr->name, f->name))
|
|
return pos;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void
|
|
feeds_reloadall(void)
|
|
{
|
|
struct pane *p;
|
|
struct feed *f = NULL;
|
|
struct row *row;
|
|
off_t pos;
|
|
|
|
p = &panes[PaneFeeds];
|
|
if ((row = pane_row_get(p, p->pos)))
|
|
f = row->data;
|
|
|
|
pos = panes[PaneItems].pos; /* store numeric item position */
|
|
feeds_set(curfeed); /* close and reopen feed if possible */
|
|
urls_read(&urls, urlfile);
|
|
feeds_load(feeds, nfeeds);
|
|
urls_free(&urls);
|
|
/* restore numeric item position */
|
|
pane_setpos(&panes[PaneItems], pos);
|
|
updatesidebar();
|
|
updatetitle();
|
|
|
|
/* try to find the same feed in the pane */
|
|
if (f && (pos = feeds_row_get(p, f)) != -1)
|
|
pane_setpos(p, pos);
|
|
else
|
|
pane_setpos(p, 0);
|
|
}
|
|
|
|
void
|
|
feed_open_selected(struct pane *p)
|
|
{
|
|
struct feed *f;
|
|
struct row *row;
|
|
|
|
if (!(row = pane_row_get(p, p->pos)))
|
|
return;
|
|
f = row->data;
|
|
feeds_set(f);
|
|
urls_read(&urls, urlfile);
|
|
if (f->fp)
|
|
feed_load(f, f->fp);
|
|
urls_free(&urls);
|
|
/* redraw row: counts could be changed */
|
|
updatesidebar();
|
|
updatetitle();
|
|
|
|
if (layout == LayoutMonocle) {
|
|
selpane = PaneItems;
|
|
updategeom();
|
|
}
|
|
}
|
|
|
|
void
|
|
feed_plumb_selected_item(struct pane *p, int field)
|
|
{
|
|
struct row *row;
|
|
struct item *item;
|
|
char *cmd[3]; /* will have: { plumbercmd, arg, NULL } */
|
|
|
|
if (!(row = pane_row_get(p, p->pos)))
|
|
return;
|
|
markread(p, p->pos, p->pos, 1);
|
|
item = row->data;
|
|
cmd[0] = plumbercmd;
|
|
cmd[1] = item->fields[field]; /* set first argument for plumber */
|
|
cmd[2] = NULL;
|
|
forkexec(cmd, plumberia);
|
|
}
|
|
|
|
void
|
|
feed_pipe_selected_item(struct pane *p)
|
|
{
|
|
struct row *row;
|
|
struct item *item;
|
|
|
|
if (!(row = pane_row_get(p, p->pos)))
|
|
return;
|
|
item = row->data;
|
|
markread(p, p->pos, p->pos, 1);
|
|
pipeitem(pipercmd, item, -1, piperia);
|
|
}
|
|
|
|
void
|
|
feed_yank_selected_item(struct pane *p, int field)
|
|
{
|
|
struct row *row;
|
|
struct item *item;
|
|
|
|
if (!(row = pane_row_get(p, p->pos)))
|
|
return;
|
|
item = row->data;
|
|
pipeitem(yankercmd, item, field, yankeria);
|
|
}
|
|
|
|
/* calculate optimal (default) size */
|
|
int
|
|
getsidebarsizedefault(void)
|
|
{
|
|
struct feed *feed;
|
|
size_t i;
|
|
int len, size;
|
|
|
|
switch (layout) {
|
|
case LayoutVertical:
|
|
for (i = 0, size = 0; i < nfeeds; i++) {
|
|
feed = &feeds[i];
|
|
len = snprintf(NULL, 0, " (%lu/%lu)",
|
|
feed->totalnew, feed->total) +
|
|
colw(feed->name);
|
|
if (len > size)
|
|
size = len;
|
|
|
|
if (onlynew && feed->totalnew == 0)
|
|
continue;
|
|
}
|
|
return MAX(MIN(win.width - 1, size), 0);
|
|
case LayoutHorizontal:
|
|
for (i = 0, size = 0; i < nfeeds; i++) {
|
|
feed = &feeds[i];
|
|
if (onlynew && feed->totalnew == 0)
|
|
continue;
|
|
size++;
|
|
}
|
|
return MAX(MIN((win.height - 1) / 2, size), 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
getsidebarsize(void)
|
|
{
|
|
int size;
|
|
|
|
if ((size = fixedsidebarsizes[layout]) < 0)
|
|
size = getsidebarsizedefault();
|
|
return size;
|
|
}
|
|
|
|
void
|
|
adjustsidebarsize(int n)
|
|
{
|
|
int size;
|
|
|
|
if ((size = fixedsidebarsizes[layout]) < 0)
|
|
size = getsidebarsizedefault();
|
|
if (n > 0) {
|
|
if ((layout == LayoutVertical && size + 1 < win.width) ||
|
|
(layout == LayoutHorizontal && size + 1 < win.height))
|
|
size++;
|
|
} else if (n < 0) {
|
|
if ((layout == LayoutVertical && size > 0) ||
|
|
(layout == LayoutHorizontal && size > 1))
|
|
size--;
|
|
}
|
|
|
|
if (size != fixedsidebarsizes[layout]) {
|
|
fixedsidebarsizes[layout] = size;
|
|
updategeom();
|
|
}
|
|
}
|
|
|
|
void
|
|
updatesidebar(void)
|
|
{
|
|
struct pane *p;
|
|
struct row *row;
|
|
struct feed *feed;
|
|
size_t i, nrows;
|
|
int oldvalue = 0, newvalue = 0;
|
|
|
|
p = &panes[PaneFeeds];
|
|
if (!p->rows)
|
|
p->rows = ecalloc(sizeof(p->rows[0]), nfeeds + 1);
|
|
|
|
switch (layout) {
|
|
case LayoutVertical:
|
|
oldvalue = p->width;
|
|
newvalue = getsidebarsize();
|
|
p->width = newvalue;
|
|
break;
|
|
case LayoutHorizontal:
|
|
oldvalue = p->height;
|
|
newvalue = getsidebarsize();
|
|
p->height = newvalue;
|
|
break;
|
|
}
|
|
|
|
nrows = 0;
|
|
for (i = 0; i < nfeeds; i++) {
|
|
feed = &feeds[i];
|
|
|
|
row = &(p->rows[nrows]);
|
|
row->bold = (feed->totalnew > 0);
|
|
row->data = feed;
|
|
|
|
if (onlynew && feed->totalnew == 0)
|
|
continue;
|
|
|
|
nrows++;
|
|
}
|
|
p->nrows = nrows;
|
|
|
|
if (oldvalue != newvalue)
|
|
updategeom();
|
|
else
|
|
p->dirty = 1;
|
|
|
|
if (!p->nrows)
|
|
p->pos = 0;
|
|
else if (p->pos >= p->nrows)
|
|
p->pos = p->nrows - 1;
|
|
}
|
|
|
|
void
|
|
sighandler(int signo)
|
|
{
|
|
switch (signo) {
|
|
case SIGCHLD: state_sigchld = 1; break;
|
|
case SIGHUP: state_sighup = 1; break;
|
|
case SIGINT: state_sigint = 1; break;
|
|
case SIGTERM: state_sigterm = 1; break;
|
|
case SIGWINCH: state_sigwinch = 1; break;
|
|
}
|
|
}
|
|
|
|
void
|
|
alldirty(void)
|
|
{
|
|
win.dirty = 1;
|
|
panes[PaneFeeds].dirty = 1;
|
|
panes[PaneItems].dirty = 1;
|
|
scrollbars[PaneFeeds].dirty = 1;
|
|
scrollbars[PaneItems].dirty = 1;
|
|
linebar.dirty = 1;
|
|
statusbar.dirty = 1;
|
|
}
|
|
|
|
void
|
|
draw(void)
|
|
{
|
|
struct row *row;
|
|
struct item *item;
|
|
size_t i;
|
|
|
|
if (win.dirty)
|
|
win.dirty = 0;
|
|
|
|
for (i = 0; i < LEN(panes); i++) {
|
|
pane_setfocus(&panes[i], i == selpane);
|
|
pane_draw(&panes[i]);
|
|
|
|
/* each pane has a scrollbar */
|
|
scrollbar_setfocus(&scrollbars[i], i == selpane);
|
|
scrollbar_update(&scrollbars[i],
|
|
panes[i].pos - (panes[i].pos % panes[i].height),
|
|
panes[i].nrows, panes[i].height);
|
|
scrollbar_draw(&scrollbars[i]);
|
|
}
|
|
|
|
linebar_draw(&linebar);
|
|
|
|
/* if item selection text changed then update the status text */
|
|
if ((row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) {
|
|
item = row->data;
|
|
statusbar_update(&statusbar, item->fields[FieldLink]);
|
|
} else {
|
|
statusbar_update(&statusbar, "");
|
|
}
|
|
statusbar_draw(&statusbar);
|
|
}
|
|
|
|
void
|
|
mousereport(int button, int release, int keymask, int x, int y)
|
|
{
|
|
struct pane *p;
|
|
size_t i;
|
|
off_t pos;
|
|
int changedpane, dblclick;
|
|
|
|
if (!usemouse || release || button == -1)
|
|
return;
|
|
|
|
for (i = 0; i < LEN(panes); i++) {
|
|
p = &panes[i];
|
|
if (p->hidden || !p->width || !p->height)
|
|
continue;
|
|
|
|
/* these button actions are done regardless of the position */
|
|
switch (button) {
|
|
case 7: /* side-button: backward */
|
|
if (selpane == PaneFeeds)
|
|
return;
|
|
selpane = PaneFeeds;
|
|
if (layout == LayoutMonocle)
|
|
updategeom();
|
|
return;
|
|
case 8: /* side-button: forward */
|
|
if (selpane == PaneItems)
|
|
return;
|
|
selpane = PaneItems;
|
|
if (layout == LayoutMonocle)
|
|
updategeom();
|
|
return;
|
|
}
|
|
|
|
/* check if mouse position is in pane or in its scrollbar */
|
|
if (!(x >= p->x && x < p->x + p->width + (!scrollbars[i].hidden) &&
|
|
y >= p->y && y < p->y + p->height))
|
|
continue;
|
|
|
|
changedpane = (selpane != i);
|
|
selpane = i;
|
|
/* relative position on screen */
|
|
pos = y - p->y + p->pos - (p->pos % p->height);
|
|
dblclick = (pos == p->pos); /* clicking the already selected row */
|
|
|
|
switch (button) {
|
|
case 0: /* left-click */
|
|
if (!p->nrows || pos >= p->nrows)
|
|
break;
|
|
pane_setpos(p, pos);
|
|
if (i == PaneFeeds)
|
|
feed_open_selected(&panes[PaneFeeds]);
|
|
else if (i == PaneItems && dblclick && !changedpane)
|
|
feed_plumb_selected_item(&panes[PaneItems], FieldLink);
|
|
break;
|
|
case 2: /* right-click */
|
|
if (!p->nrows || pos >= p->nrows)
|
|
break;
|
|
pane_setpos(p, pos);
|
|
if (i == PaneItems)
|
|
feed_pipe_selected_item(&panes[PaneItems]);
|
|
break;
|
|
case 3: /* scroll up */
|
|
case 4: /* scroll down */
|
|
pane_scrollpage(p, button == 3 ? -1 : +1);
|
|
break;
|
|
}
|
|
return; /* do not bubble events */
|
|
}
|
|
}
|
|
|
|
/* Custom formatter for feed row. */
|
|
char *
|
|
feed_row_format(struct pane *p, struct row *row)
|
|
{
|
|
/* static, reuse local buffers */
|
|
static char *bufw, *text;
|
|
static size_t bufwsize, textsize;
|
|
struct feed *feed;
|
|
size_t needsize;
|
|
char counts[128];
|
|
int len, w;
|
|
|
|
feed = row->data;
|
|
|
|
/* align counts to the right and pad the rest with spaces */
|
|
len = snprintf(counts, sizeof(counts), "(%lu/%lu)",
|
|
feed->totalnew, feed->total);
|
|
if (len > p->width)
|
|
w = p->width;
|
|
else
|
|
w = p->width - len;
|
|
|
|
needsize = (w + 1) * 4;
|
|
if (needsize > bufwsize) {
|
|
bufw = erealloc(bufw, needsize);
|
|
bufwsize = needsize;
|
|
}
|
|
|
|
needsize = bufwsize + sizeof(counts) + 1;
|
|
if (needsize > textsize) {
|
|
text = erealloc(text, needsize);
|
|
textsize = needsize;
|
|
}
|
|
|
|
if (utf8pad(bufw, bufwsize, feed->name, w, ' ') != -1)
|
|
snprintf(text, textsize, "%s%s", bufw, counts);
|
|
else
|
|
text[0] = '\0';
|
|
|
|
return text;
|
|
}
|
|
|
|
int
|
|
feed_row_match(struct pane *p, struct row *row, const char *s)
|
|
{
|
|
struct feed *feed;
|
|
|
|
feed = row->data;
|
|
|
|
return (strcasestr(feed->name, s) != NULL);
|
|
}
|
|
|
|
struct row *
|
|
item_row_get(struct pane *p, off_t pos)
|
|
{
|
|
struct row *itemrow;
|
|
struct item *item;
|
|
struct feed *f;
|
|
char *line = NULL;
|
|
size_t linesize = 0;
|
|
ssize_t linelen;
|
|
|
|
itemrow = p->rows + pos;
|
|
item = itemrow->data;
|
|
|
|
f = curfeed;
|
|
if (f && f->path && f->fp && !item->line) {
|
|
if (fseek(f->fp, item->offset, SEEK_SET))
|
|
die("fseek: %s", f->path);
|
|
|
|
if ((linelen = getline(&line, &linesize, f->fp)) <= 0) {
|
|
if (ferror(f->fp))
|
|
die("getline: %s", f->path);
|
|
return NULL;
|
|
}
|
|
|
|
if (line[linelen - 1] == '\n')
|
|
line[--linelen] = '\0';
|
|
|
|
linetoitem(estrdup(line), item);
|
|
free(line);
|
|
|
|
itemrow->data = item;
|
|
}
|
|
return itemrow;
|
|
}
|
|
|
|
/* Custom formatter for item row. */
|
|
char *
|
|
item_row_format(struct pane *p, struct row *row)
|
|
{
|
|
/* static, reuse local buffers */
|
|
static char *text;
|
|
static size_t textsize;
|
|
struct item *item;
|
|
struct tm tm;
|
|
size_t needsize;
|
|
|
|
item = row->data;
|
|
|
|
needsize = strlen(item->fields[FieldTitle]) + 21;
|
|
if (needsize > textsize) {
|
|
text = erealloc(text, needsize);
|
|
textsize = needsize;
|
|
}
|
|
|
|
if (item->timeok && localtime_r(&(item->timestamp), &tm)) {
|
|
snprintf(text, textsize, "%c %04d-%02d-%02d %02d:%02d %s",
|
|
item->fields[FieldEnclosure][0] ? '@' : ' ',
|
|
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
|
tm.tm_hour, tm.tm_min, item->fields[FieldTitle]);
|
|
} else {
|
|
snprintf(text, textsize, "%c %s",
|
|
item->fields[FieldEnclosure][0] ? '@' : ' ',
|
|
item->fields[FieldTitle]);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
void
|
|
markread(struct pane *p, off_t from, off_t to, int isread)
|
|
{
|
|
struct row *row;
|
|
struct item *item;
|
|
FILE *fp;
|
|
off_t i;
|
|
const char *cmd;
|
|
int isnew = !isread, pid, status = -1, visstart;
|
|
|
|
if (!urlfile || !p->nrows)
|
|
return;
|
|
|
|
cmd = isread ? markreadcmd : markunreadcmd;
|
|
|
|
switch ((pid = fork())) {
|
|
case -1:
|
|
die("fork");
|
|
case 0:
|
|
dup2(devnullfd, 1); /* stdout */
|
|
dup2(devnullfd, 2); /* stderr */
|
|
|
|
errno = 0;
|
|
if (!(fp = popen(cmd, "w")))
|
|
die("popen: %s", cmd);
|
|
|
|
for (i = from; i <= to && i < p->nrows; i++) {
|
|
/* do not use pane_row_get(): no need for lazyload */
|
|
row = &(p->rows[i]);
|
|
item = row->data;
|
|
if (item->isnew != isnew) {
|
|
fputs(item->matchnew, fp);
|
|
putc('\n', fp);
|
|
}
|
|
}
|
|
status = pclose(fp);
|
|
status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
|
|
_exit(status);
|
|
default:
|
|
/* waitpid() and block on process status change,
|
|
fail if exit statuscode was unavailable or non-zero */
|
|
if (waitpid(pid, &status, 0) <= 0 || status)
|
|
break;
|
|
|
|
visstart = p->pos - (p->pos % p->height); /* visible start */
|
|
for (i = from; i <= to && i < p->nrows; i++) {
|
|
row = &(p->rows[i]);
|
|
item = row->data;
|
|
if (item->isnew == isnew)
|
|
continue;
|
|
|
|
row->bold = item->isnew = isnew;
|
|
curfeed->totalnew += isnew ? 1 : -1;
|
|
|
|
/* draw if visible on screen */
|
|
if (i >= visstart && i < visstart + p->height)
|
|
pane_row_draw(p, i, i == p->pos);
|
|
}
|
|
updatesidebar();
|
|
updatetitle();
|
|
}
|
|
}
|
|
|
|
int
|
|
urls_cmp(const void *v1, const void *v2)
|
|
{
|
|
return strcmp(*((char **)v1), *((char **)v2));
|
|
}
|
|
|
|
void
|
|
urls_free(struct urls *urls)
|
|
{
|
|
while (urls->len > 0) {
|
|
urls->len--;
|
|
free(urls->items[urls->len]);
|
|
}
|
|
free(urls->items);
|
|
urls->items = NULL;
|
|
urls->len = 0;
|
|
urls->cap = 0;
|
|
}
|
|
|
|
int
|
|
urls_hasmatch(struct urls *urls, const char *url)
|
|
{
|
|
return (urls->len &&
|
|
bsearch(&url, urls->items, urls->len, sizeof(char *), urls_cmp));
|
|
}
|
|
|
|
void
|
|
urls_read(struct urls *urls, const char *urlfile)
|
|
{
|
|
FILE *fp;
|
|
char *line = NULL;
|
|
size_t linesiz = 0;
|
|
ssize_t n;
|
|
|
|
urls_free(urls);
|
|
|
|
if (!urlfile)
|
|
return;
|
|
if (!(fp = fopen(urlfile, "rb")))
|
|
die("fopen: %s", urlfile);
|
|
|
|
while ((n = getline(&line, &linesiz, fp)) > 0) {
|
|
if (line[n - 1] == '\n')
|
|
line[--n] = '\0';
|
|
if (urls->len + 1 >= urls->cap) {
|
|
urls->cap = urls->cap ? urls->cap * 2 : 16;
|
|
urls->items = erealloc(urls->items, urls->cap * sizeof(char *));
|
|
}
|
|
urls->items[urls->len++] = estrdup(line);
|
|
}
|
|
if (ferror(fp))
|
|
die("getline: %s", urlfile);
|
|
fclose(fp);
|
|
free(line);
|
|
|
|
if (urls->len > 0)
|
|
qsort(urls->items, urls->len, sizeof(char *), urls_cmp);
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
struct pane *p;
|
|
struct feed *f;
|
|
struct row *row;
|
|
char *name, *tmp;
|
|
char *search = NULL; /* search text */
|
|
int button, ch, fd, i, keymask, release, x, y;
|
|
off_t pos;
|
|
|
|
#ifdef __OpenBSD__
|
|
if (pledge("stdio rpath tty proc exec", NULL) == -1)
|
|
die("pledge");
|
|
#endif
|
|
|
|
setlocale(LC_CTYPE, "");
|
|
|
|
if ((tmp = getenv("SFEED_PLUMBER")))
|
|
plumbercmd = tmp;
|
|
if ((tmp = getenv("SFEED_PIPER")))
|
|
pipercmd = tmp;
|
|
if ((tmp = getenv("SFEED_YANKER")))
|
|
yankercmd = tmp;
|
|
if ((tmp = getenv("SFEED_PLUMBER_INTERACTIVE")))
|
|
plumberia = !strcmp(tmp, "1");
|
|
if ((tmp = getenv("SFEED_PIPER_INTERACTIVE")))
|
|
piperia = !strcmp(tmp, "1");
|
|
if ((tmp = getenv("SFEED_YANKER_INTERACTIVE")))
|
|
yankeria = !strcmp(tmp, "1");
|
|
if ((tmp = getenv("SFEED_MARK_READ")))
|
|
markreadcmd = tmp;
|
|
if ((tmp = getenv("SFEED_MARK_UNREAD")))
|
|
markunreadcmd = tmp;
|
|
if ((tmp = getenv("SFEED_LAZYLOAD")))
|
|
lazyload = !strcmp(tmp, "1");
|
|
urlfile = getenv("SFEED_URL_FILE"); /* can be NULL */
|
|
cmdenv = getenv("SFEED_AUTOCMD"); /* can be NULL */
|
|
|
|
setlayout(argc <= 1 ? LayoutMonocle : LayoutVertical);
|
|
selpane = layout == LayoutMonocle ? PaneItems : PaneFeeds;
|
|
|
|
panes[PaneFeeds].row_format = feed_row_format;
|
|
panes[PaneFeeds].row_match = feed_row_match;
|
|
panes[PaneItems].row_format = item_row_format;
|
|
if (lazyload)
|
|
panes[PaneItems].row_get = item_row_get;
|
|
|
|
feeds = ecalloc(argc, sizeof(struct feed));
|
|
if (argc == 1) {
|
|
nfeeds = 1;
|
|
f = &feeds[0];
|
|
f->name = "stdin";
|
|
if (!(f->fp = fdopen(0, "rb")))
|
|
die("fdopen");
|
|
} else {
|
|
for (i = 1; i < argc; i++) {
|
|
f = &feeds[i - 1];
|
|
f->path = argv[i];
|
|
name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i];
|
|
f->name = name;
|
|
}
|
|
nfeeds = argc - 1;
|
|
}
|
|
feeds_set(&feeds[0]);
|
|
urls_read(&urls, urlfile);
|
|
feeds_load(feeds, nfeeds);
|
|
urls_free(&urls);
|
|
|
|
if (!isatty(0)) {
|
|
if ((fd = open("/dev/tty", O_RDONLY)) == -1)
|
|
die("open: /dev/tty");
|
|
if (dup2(fd, 0) == -1)
|
|
die("dup2(%d, 0): /dev/tty -> stdin", fd);
|
|
close(fd);
|
|
}
|
|
if (argc == 1)
|
|
feeds[0].fp = NULL;
|
|
|
|
if ((devnullfd = open("/dev/null", O_WRONLY)) == -1)
|
|
die("open: /dev/null");
|
|
|
|
init();
|
|
updatesidebar();
|
|
updategeom();
|
|
updatetitle();
|
|
draw();
|
|
|
|
while (1) {
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
switch (ch) {
|
|
case '\x1b':
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
if (ch != '[' && ch != 'O')
|
|
continue; /* unhandled */
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
switch (ch) {
|
|
case 'M': /* mouse: X10 encoding */
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
button = ch - 32;
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
x = ch - 32;
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
y = ch - 32;
|
|
|
|
keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
|
|
button &= ~keymask; /* unset key mask */
|
|
|
|
/* button numbers (0 - 2) encoded in lowest 2 bits
|
|
release does not indicate which button (so set to 0).
|
|
Handle extended buttons like scrollwheels
|
|
and side-buttons by each range. */
|
|
release = 0;
|
|
if (button == 3) {
|
|
button = -1;
|
|
release = 1;
|
|
} else if (button >= 128) {
|
|
button -= 121;
|
|
} else if (button >= 64) {
|
|
button -= 61;
|
|
}
|
|
mousereport(button, release, keymask, x - 1, y - 1);
|
|
break;
|
|
case '<': /* mouse: SGR encoding */
|
|
for (button = 0; ; button *= 10, button += ch - '0') {
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
else if (ch == ';')
|
|
break;
|
|
}
|
|
for (x = 0; ; x *= 10, x += ch - '0') {
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
else if (ch == ';')
|
|
break;
|
|
}
|
|
for (y = 0; ; y *= 10, y += ch - '0') {
|
|
if ((ch = readch()) < 0)
|
|
goto event;
|
|
else if (ch == 'm' || ch == 'M')
|
|
break; /* release or press */
|
|
}
|
|
release = ch == 'm';
|
|
keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
|
|
button &= ~keymask; /* unset key mask */
|
|
|
|
if (button >= 128)
|
|
button -= 121;
|
|
else if (button >= 64)
|
|
button -= 61;
|
|
|
|
mousereport(button, release, keymask, x - 1, y - 1);
|
|
break;
|
|
case 'A': goto keyup; /* arrow up */
|
|
case 'B': goto keydown; /* arrow down */
|
|
case 'C': goto keyright; /* arrow right */
|
|
case 'D': goto keyleft; /* arrow left */
|
|
case 'F': goto endpos; /* end */
|
|
case 'H': goto startpos; /* home */
|
|
default:
|
|
if (!(ch >= '0' && ch <= '9'))
|
|
break;
|
|
for (i = ch - '0'; ;) {
|
|
if ((ch = readch()) < 0) {
|
|
goto event;
|
|
} else if (ch >= '0' && ch <= '9') {
|
|
i = (i * 10) + (ch - '0');
|
|
continue;
|
|
} else if (ch == '~') { /* DEC: ESC [ num ~ */
|
|
switch (i) {
|
|
case 1: goto startpos; /* home */
|
|
case 4: goto endpos; /* end */
|
|
case 5: goto prevpage; /* page up */
|
|
case 6: goto nextpage; /* page down */
|
|
case 7: goto startpos; /* home: urxvt */
|
|
case 8: goto endpos; /* end: urxvt */
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
keyup:
|
|
case 'k':
|
|
pane_scrolln(&panes[selpane], -1);
|
|
break;
|
|
keydown:
|
|
case 'j':
|
|
pane_scrolln(&panes[selpane], +1);
|
|
break;
|
|
keyleft:
|
|
case 'h':
|
|
if (selpane == PaneFeeds)
|
|
break;
|
|
selpane = PaneFeeds;
|
|
if (layout == LayoutMonocle)
|
|
updategeom();
|
|
break;
|
|
keyright:
|
|
case 'l':
|
|
if (selpane == PaneItems)
|
|
break;
|
|
selpane = PaneItems;
|
|
if (layout == LayoutMonocle)
|
|
updategeom();
|
|
break;
|
|
case 'K':
|
|
p = &panes[selpane];
|
|
if (!p->nrows)
|
|
break;
|
|
for (pos = p->pos - 1; pos >= 0; pos--) {
|
|
if ((row = pane_row_get(p, pos)) && row->bold) {
|
|
pane_setpos(p, pos);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'J':
|
|
p = &panes[selpane];
|
|
if (!p->nrows)
|
|
break;
|
|
for (pos = p->pos + 1; pos < p->nrows; pos++) {
|
|
if ((row = pane_row_get(p, pos)) && row->bold) {
|
|
pane_setpos(p, pos);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case '\t':
|
|
selpane = selpane == PaneFeeds ? PaneItems : PaneFeeds;
|
|
if (layout == LayoutMonocle)
|
|
updategeom();
|
|
break;
|
|
startpos:
|
|
case 'g':
|
|
pane_setpos(&panes[selpane], 0);
|
|
break;
|
|
endpos:
|
|
case 'G':
|
|
p = &panes[selpane];
|
|
if (p->nrows)
|
|
pane_setpos(p, p->nrows - 1);
|
|
break;
|
|
prevpage:
|
|
case 2: /* ^B */
|
|
pane_scrollpage(&panes[selpane], -1);
|
|
break;
|
|
nextpage:
|
|
case ' ':
|
|
case 6: /* ^F */
|
|
pane_scrollpage(&panes[selpane], +1);
|
|
break;
|
|
case '[':
|
|
case ']':
|
|
pane_scrolln(&panes[PaneFeeds], ch == '[' ? -1 : +1);
|
|
feed_open_selected(&panes[PaneFeeds]);
|
|
break;
|
|
case '/': /* new search (forward) */
|
|
case '?': /* new search (backward) */
|
|
case 'n': /* search again (forward) */
|
|
case 'N': /* search again (backward) */
|
|
p = &panes[selpane];
|
|
|
|
/* prompt for new input */
|
|
if (ch == '?' || ch == '/') {
|
|
tmp = ch == '?' ? "backward" : "forward";
|
|
free(search);
|
|
search = uiprompt(statusbar.x, statusbar.y,
|
|
"Search (%s):", tmp);
|
|
statusbar.dirty = 1;
|
|
}
|
|
if (!search || !p->nrows)
|
|
break;
|
|
|
|
if (ch == '/' || ch == 'n') {
|
|
/* forward */
|
|
for (pos = p->pos + 1; pos < p->nrows; pos++) {
|
|
if (pane_row_match(p, pane_row_get(p, pos), search)) {
|
|
pane_setpos(p, pos);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
/* backward */
|
|
for (pos = p->pos - 1; pos >= 0; pos--) {
|
|
if (pane_row_match(p, pane_row_get(p, pos), search)) {
|
|
pane_setpos(p, pos);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 12: /* ^L, redraw */
|
|
alldirty();
|
|
break;
|
|
case 'R': /* reload all files */
|
|
feeds_reloadall();
|
|
break;
|
|
case 'a': /* attachment */
|
|
case 'e': /* enclosure */
|
|
case '@':
|
|
if (selpane == PaneItems)
|
|
feed_plumb_selected_item(&panes[selpane], FieldEnclosure);
|
|
break;
|
|
case 'm': /* toggle mouse mode */
|
|
usemouse = !usemouse;
|
|
mousemode(usemouse);
|
|
break;
|
|
case '<': /* decrease fixed sidebar width */
|
|
case '>': /* increase fixed sidebar width */
|
|
adjustsidebarsize(ch == '<' ? -1 : +1);
|
|
break;
|
|
case '=': /* reset fixed sidebar to automatic size */
|
|
fixedsidebarsizes[layout] = -1;
|
|
updategeom();
|
|
break;
|
|
case 't': /* toggle showing only new in sidebar */
|
|
p = &panes[PaneFeeds];
|
|
if ((row = pane_row_get(p, p->pos)))
|
|
f = row->data;
|
|
else
|
|
f = NULL;
|
|
|
|
onlynew = !onlynew;
|
|
updatesidebar();
|
|
|
|
/* try to find the same feed in the pane */
|
|
if (f && f->totalnew &&
|
|
(pos = feeds_row_get(p, f)) != -1)
|
|
pane_setpos(p, pos);
|
|
else
|
|
pane_setpos(p, 0);
|
|
break;
|
|
case 'o': /* feeds: load, items: plumb URL */
|
|
case '\n':
|
|
if (selpane == PaneFeeds && panes[selpane].nrows)
|
|
feed_open_selected(&panes[selpane]);
|
|
else if (selpane == PaneItems && panes[selpane].nrows)
|
|
feed_plumb_selected_item(&panes[selpane], FieldLink);
|
|
break;
|
|
case 'c': /* items: pipe TSV line to program */
|
|
case 'p':
|
|
case '|':
|
|
if (selpane == PaneItems)
|
|
feed_pipe_selected_item(&panes[selpane]);
|
|
break;
|
|
case 'y': /* yank: pipe TSV field to yank URL to clipboard */
|
|
case 'E': /* yank: pipe TSV field to yank enclosure to clipboard */
|
|
if (selpane == PaneItems)
|
|
feed_yank_selected_item(&panes[selpane],
|
|
ch == 'y' ? FieldLink : FieldEnclosure);
|
|
break;
|
|
case 'f': /* mark all read */
|
|
case 'F': /* mark all unread */
|
|
if (panes[PaneItems].nrows) {
|
|
p = &panes[PaneItems];
|
|
markread(p, 0, p->nrows - 1, ch == 'f');
|
|
}
|
|
break;
|
|
case 'r': /* mark item as read */
|
|
case 'u': /* mark item as unread */
|
|
if (selpane == PaneItems && panes[selpane].nrows) {
|
|
p = &panes[selpane];
|
|
markread(p, p->pos, p->pos, ch == 'r');
|
|
}
|
|
break;
|
|
case 's': /* toggle layout between monocle or non-monocle */
|
|
setlayout(layout == LayoutMonocle ? prevlayout : LayoutMonocle);
|
|
updategeom();
|
|
break;
|
|
case '1': /* vertical layout */
|
|
case '2': /* horizontal layout */
|
|
case '3': /* monocle layout */
|
|
setlayout(ch - '1');
|
|
updategeom();
|
|
break;
|
|
case 4: /* EOT */
|
|
case 'q': goto end;
|
|
}
|
|
event:
|
|
if (ch == EOF)
|
|
goto end;
|
|
else if (ch == -3 && !state_sigchld && !state_sighup &&
|
|
!state_sigint && !state_sigterm && !state_sigwinch)
|
|
continue; /* just a time-out, nothing to do */
|
|
|
|
/* handle signals in a particular order */
|
|
if (state_sigchld) {
|
|
state_sigchld = 0;
|
|
/* wait on child processes so they don't become a zombie,
|
|
do not block the parent process if there is no status,
|
|
ignore errors */
|
|
while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
|
|
;
|
|
}
|
|
if (state_sigterm) {
|
|
cleanup();
|
|
_exit(128 + SIGTERM);
|
|
}
|
|
if (state_sigint) {
|
|
cleanup();
|
|
_exit(128 + SIGINT);
|
|
}
|
|
if (state_sighup) {
|
|
state_sighup = 0;
|
|
feeds_reloadall();
|
|
}
|
|
if (state_sigwinch) {
|
|
state_sigwinch = 0;
|
|
resizewin();
|
|
updategeom();
|
|
}
|
|
|
|
draw();
|
|
}
|
|
end:
|
|
cleanup();
|
|
|
|
return 0;
|
|
}
|