12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399 |
- #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 "|"
- #define SCROLLBAR_SYMBOL_TICK " "
- #define LINEBAR_SYMBOL_BAR "-"
- #define LINEBAR_SYMBOL_RIGHT "|"
- #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;
- /* DEC/SUN: ESC O char, HP: ESC char or SCO: ESC [ char */
- 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 'G': goto nextpage; /* page down */
- case 'H': goto startpos; /* home */
- case 'I': goto prevpage; /* page up */
- 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 */
- }
- } else if (ch == 'z') { /* SUN: ESC [ num z */
- switch (i) {
- case 214: goto startpos; /* home */
- case 216: goto prevpage; /* page up */
- case 220: goto endpos; /* end */
- case 222: goto nextpage; /* page down */
- }
- }
- 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;
- }
|