snownews/ui.c

1167 lines
38 KiB
C

// This file is part of Snownews - A lightweight console RSS newsreader
//
// Copyright (c) 2003-2004 Oliver Feiler <kiza@kcore.de>
// Copyright (c) 2021 Mike Sharov <msharov@users.sourceforge.net>
//
// Snownews is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3
// as published by the Free Software Foundation.
//
// Snownews is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Snownews. If not, see http://www.gnu.org/licenses/.
#include "ui.h"
#include "main.h"
#include "about.h"
#include "cat.h"
#include "conv.h"
#include "dialog.h"
#include "feedio.h"
#include "setup.h"
#include "uiutil.h"
#include <ncurses.h>
#include <libxml/parser.h>
#ifdef UTF_8
#define xmlStrlen(s) xmlUTF8Strlen(s)
#endif
//----------------------------------------------------------------------
static bool resize_dirty = false;
//----------------------------------------------------------------------
void sig_winch (int p __attribute__((unused)))
{
resize_dirty = true;
}
// View newsitem in scrollable window.
// Speed of this code has been greatly increased in 1.2.1.
static void UIDisplayItem (const struct newsitem* current_item, const struct feed* current_feed)
{
unsigned topline = 0; // First line on screen
while (1) {
const unsigned pagesz = LINES-4;
const unsigned pagew = COLS-2;
erase();
const char* feed_title = current_feed->description;
if (!feed_title || !feed_title[0])
feed_title = current_feed->title;
if (!feed_title || !feed_title[0])
feed_title = "* " SNOWNEWS_NAME " " SNOWNEWS_VERSTRING;
// Print feed title
UISupportDrawHeader (feed_title);
// Print publishing date if we have one.
if (current_item->data->date) {
char* date_str = unixToPostDateString (current_item->data->date);
if (date_str) {
move (0, COLS - strlen(date_str) - 2);
attron (WA_REVERSE);
addch (' ');
addstr (date_str);
addch (' ');
attroff (WA_REVERSE);
free (date_str);
}
}
// Print item title
unsigned ydesc = 1, xdesc = 1;
if (current_item->data->title) {
unsigned titlelen = xmlStrlen ((xmlChar*) current_item->data->title);
unsigned xtitle = xdesc;
if (titlelen < COLS - xdesc*2)
xtitle = (COLS - titlelen) / 2u;
move (ydesc, xtitle);
attron (WA_BOLD);
add_utf8 (current_item->data->title);
attroff (WA_BOLD);
mvhline (++ydesc, 0, 0, COLS);
++ydesc;
}
// Print item text
unsigned desclines = 0; // and count lines
if (!current_item->data->description || !current_item->data->description[0])
mvadd_utf8 (ydesc, xdesc, _("No description available."));
else {
char* wrapped_dtext = strdup (current_item->data->description);
wrap_text (wrapped_dtext, pagew);
const char* dtext = wrapped_dtext;
const char* dtextend = dtext + strlen (dtext);
unsigned y = ydesc;
while (dtext < dtextend) {
const char* pnl = strchr (dtext, '\n');
if (!pnl)
pnl = dtextend;
if (desclines >= topline && desclines < topline+pagesz) {
unsigned linelen = utf8_range_length (dtext, pnl);
if (linelen > pagew)
linelen = pagew;
mvaddn_utf8 (y++, xdesc, dtext, linelen);
}
++desclines;
dtext = pnl+strlen("\n");
}
free (wrapped_dtext);
}
char keyinfostr [256];
if (current_item->data->link)
snprintf (keyinfostr, sizeof (keyinfostr), "-> %s", current_item->data->link);
else
snprintf (keyinfostr, sizeof (keyinfostr), _("Press '%c' or Enter to return to previous screen. Hit '%c' for help screen."), _settings.keybindings.prevmenu, _settings.keybindings.help);
UIStatus (keyinfostr, 0, 0);
int uiinput = getch();
if (uiinput == _settings.keybindings.help || uiinput == '?')
UIDisplayItemHelp();
else if (uiinput == '\n' || uiinput == _settings.keybindings.prevmenu || uiinput == _settings.keybindings.enter)
return;
else if (uiinput == _settings.keybindings.urljump)
UISupportURLJump (current_item->data->link);
else if (uiinput == _settings.keybindings.next || uiinput == KEY_RIGHT) {
if (current_item->next != NULL) {
current_item = current_item->next;
topline = 0;
} else
uiinput = ungetch (_settings.keybindings.prevmenu);
} else if (uiinput == _settings.keybindings.prev || uiinput == KEY_LEFT) {
if (current_item->prev != NULL) {
current_item = current_item->prev;
topline = 0;
} else
uiinput = ungetch (_settings.keybindings.prevmenu);
} else if (uiinput == KEY_NPAGE || uiinput == ' ' || uiinput == _settings.keybindings.pdown) {
// Scroll by one page.
for (unsigned i = 0; i < pagesz; ++i)
if (topline + pagesz < desclines)
++topline;
} else if (uiinput == KEY_PPAGE || uiinput == _settings.keybindings.pup) {
for (unsigned i = 0; i < pagesz; ++i)
if (topline > 0)
--topline;
} else if (uiinput == KEY_UP && topline > 0)
--topline;
else if (uiinput == KEY_DOWN && topline + pagesz < desclines)
++topline;
else if (uiinput == KEY_HOME || uiinput == 'H')
topline = 0;
else if (uiinput == KEY_END || uiinput == 'G')
topline = (desclines > pagesz ? desclines - pagesz : 0);
else if (resize_dirty || uiinput == KEY_RESIZE) {
clear();
resize_dirty = false;
} else if (uiinput == _settings.keybindings.about)
UIAbout();
else if (uiinput == 12) // Redraw screen on ^L
clear();
}
}
static int UIDisplayFeed (struct feed* current_feed)
{
const unsigned ymax = LINES-1;
const unsigned pagesz = LINES-3;
// Set highlighted to _feed_list at the beginning.
// Otherwise bad things (tm) will happen.
const struct newsitem* first_scr_ptr = current_feed->items;
const struct newsitem* tmp_first = first_scr_ptr;
// Select first unread item if we enter feed view or
// leave first item active if there is no unread.
const struct newsitem* highlighted = current_feed->items;
const struct newsitem* tmp_highlighted = highlighted;
unsigned highlightline = ymax+1;
// Moves highlight to next unread item.
unsigned highlightnum = 1; // Index of the highlighted item, 1 = first visible
while (highlighted && highlighted->next && highlighted->data->readstatus) {
highlighted = highlighted->next;
if (++highlightnum > ymax) {
--highlightnum;
if (first_scr_ptr->next)
first_scr_ptr = first_scr_ptr->next;
}
}
// If *highlighted points to a read entry now we have hit the last
// entry and there are no unread items anymore. Restore the original
// location in this case.
// Check if we have no items at all!
if (highlighted && highlighted->data->readstatus) {
highlighted = tmp_highlighted;
first_scr_ptr = tmp_first;
}
// Typeahead enabled?
bool typeahead = false;
unsigned typeaheadskip = 0; // # of lines to skip (for typeahead)
char* searchstr = NULL; // Typeahead searchstr string
unsigned searchstrlen = 0;
// Save first starting position. For typeahead.
const struct newsitem* savestart = first_scr_ptr; // Typeahead internal usage (tmp position save)
const struct newsitem* savestart_first = NULL;
// Put all categories of the current feed into a comma seperated list.
char* categories = GetCategoryList (current_feed);
bool reloaded = false; // We need to signal the main function if we changed feed contents.
while (1) {
erase();
// Print a title or something
const char* title = current_feed->description;
if (!title || !title[0])
title = current_feed->title;
if (!title || !title[0])
title = _("No title");
UISupportDrawHeader (title);
// We start the item list below the header
unsigned ypos = 2, itemnum = 1;
if (typeahead) {
// This resets the offset for every typeahead loop.
first_scr_ptr = current_feed->items;
unsigned count = 0, skipper = 0; // # of lines already skipped
bool found = false;
for (const struct newsitem * i = current_feed->items; i; ++count, i = i->next) {
// count+1 > ymax:
// If the _next_ line would go over the boundary.
if (count + 1 > ymax)
if (first_scr_ptr->next)
first_scr_ptr = first_scr_ptr->next;
if (!searchstrlen)
continue;
// Substring match.
if (i->data->title && s_strcasestr (i->data->title, searchstr)) {
found = true;
highlighted = i;
if (typeaheadskip >= 1 && skipper != typeaheadskip) {
++skipper;
continue;
}
break;
}
}
if (!found) {
// Restore original position on no match.
highlighted = savestart;
first_scr_ptr = savestart_first;
}
}
// Print unread entries in bold.
const struct newsitem* current_item = NULL;
for (const struct newsitem* item = first_scr_ptr; item; item = item->next) {
// Set cursor to start of current line and clear it.
move (ypos, 0);
clrtoeol();
if (!item->data->readstatus)
attron (WA_BOLD);
if (item == highlighted) {
current_item = item;
highlightline = ypos;
highlightnum = itemnum;
attron (WA_REVERSE);
mvhline (ypos, 0, ' ', COLS);
}
// Check for empty <title>
if (item->data->title) {
const char* item_title = item->data->title;
char tmpstr[256];
if (current_feed->smartfeed == 1) {
snprintf (tmpstr, sizeof (tmpstr), "(%s) %s", item->data->parent->title, item->data->title);
item_title = tmpstr;
}
int columns = COLS - 6; // Cut max item length.
mvaddn_utf8 (ypos, 1, item_title, columns);
if (xmlStrlen ((xmlChar*) item_title) > columns)
mvaddstr (ypos, columns + 1, "...");
} else {
if (current_feed->smartfeed == 1) {
char tmpstr[256];
snprintf (tmpstr, sizeof (tmpstr), "(%s) %s", item->data->parent->title, _("No title"));
mvadd_utf8 (ypos, 1, tmpstr);
} else
mvadd_utf8 (ypos, 1, _("No title"));
}
++ypos;
if (item == highlighted)
attroff (WA_REVERSE);
if (!item->data->readstatus)
attroff (WA_BOLD);
if (itemnum >= ymax)
break;
++itemnum;
}
char tmpstr [256];
if (typeahead)
snprintf (tmpstr, sizeof (tmpstr), "-> %s", searchstr);
else if (current_item && current_item->data->link)
snprintf (tmpstr, sizeof (tmpstr), "-> %s", current_item->data->link);
else
snprintf (tmpstr, sizeof (tmpstr), _("Press '%c' to return to main menu, '%c' to show help."), _settings.keybindings.prevmenu, _settings.keybindings.help);
UIStatus (tmpstr, 0, 0);
move (highlightline, 0);
int uiinput = getch();
time_t curtime = time (NULL);
if (typeahead) {
// Only match real characters.
if (uiinput >= ' ' && uiinput <= '~') {
searchstr[searchstrlen] = uiinput;
searchstr[searchstrlen + 1] = 0;
++searchstrlen;
searchstr = realloc (searchstr, searchstrlen + 2);
// ASCII 127 is DEL, 263 is... actually I have
// no idea, but it's what the text console returns.
} else if (uiinput == 127 || uiinput == 263) {
// Don't let searchstrlen go below 0!
if (searchstrlen > 0) {
--searchstrlen;
searchstr[searchstrlen] = 0;
searchstr = realloc (searchstr, searchstrlen + 2);
} else {
typeahead = false;
free (searchstr);
}
}
} else {
if (uiinput == _settings.keybindings.help || uiinput == '?')
UIDisplayFeedHelp();
else if (uiinput == _settings.keybindings.prevmenu) {
free (categories);
return reloaded;
} else if ((uiinput == KEY_UP || uiinput == _settings.keybindings.prev) && highlighted && highlighted->prev) {
// Check if we have no items at all!
highlighted = highlighted->prev;
// Adjust first visible entry.
if (--highlightnum < 1 && first_scr_ptr->prev) {
++highlightnum;
first_scr_ptr = first_scr_ptr->prev;
}
} else if ((uiinput == KEY_DOWN || uiinput == _settings.keybindings.next) && highlighted && highlighted->next) {
highlighted = highlighted->next;
// Adjust first visible entry.
if (++highlightnum > pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
} else if ((uiinput == KEY_NPAGE || uiinput == ' ' || uiinput == _settings.keybindings.pdown) && highlighted) {
// Move highlight one page up/down == pagesz
for (unsigned i = 0; i < pagesz && highlighted->next; ++i) {
highlighted = highlighted->next;
if (++highlightnum > pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
}
} else if ((uiinput == KEY_PPAGE || uiinput == _settings.keybindings.pup) && highlighted) {
for (unsigned i = 0; i < pagesz && highlighted->prev; ++i) {
highlighted = highlighted->prev;
if (--highlightnum < 1 && first_scr_ptr->prev) {
++highlightnum;
first_scr_ptr = first_scr_ptr->prev;
}
}
} else if (uiinput == KEY_HOME || uiinput == _settings.keybindings.home) {
highlighted = first_scr_ptr = current_feed->items;
highlightnum = 0;
} else if (uiinput == KEY_END || uiinput == _settings.keybindings.end) {
highlighted = first_scr_ptr = current_feed->items;
highlightnum = 0;
while (highlighted && highlighted->next) {
highlighted = highlighted->next;
if (++highlightnum >= pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
}
} else if (uiinput == _settings.keybindings.reload || uiinput == _settings.keybindings.forcereload) {
if (_unfiltered_feed_list) {
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
continue;
}
if (current_feed->smartfeed == 1)
continue;
if (uiinput == _settings.keybindings.forcereload) {
free (current_feed->lasterror);
current_feed->lasterror = NULL;
}
UpdateFeed (current_feed);
highlighted = current_feed->items;
// Reset first_scr_ptr if reloading.
first_scr_ptr = current_feed->items;
reloaded = true;
} else if (uiinput == _settings.keybindings.urljump)
UISupportURLJump (current_feed->link);
else if (uiinput == _settings.keybindings.urljump2 && highlighted)
UISupportURLJump (highlighted->data->link);
else if (uiinput == _settings.keybindings.markread) { // Mark everything read.
for (struct newsitem* i = current_feed->items; i; i = i->next) {
if (!i->data->readstatus) {
i->data->readstatus = true;
current_feed->mtime = curtime;
}
}
} else if (uiinput == _settings.keybindings.markunread && highlighted) {
highlighted->data->readstatus = !highlighted->data->readstatus;
current_feed->mtime = curtime;
reloaded = true;
} else if (uiinput == _settings.keybindings.about)
UIAbout();
else if (uiinput == _settings.keybindings.feedinfo)
FeedInfo (current_feed);
else if (resize_dirty || uiinput == KEY_RESIZE) {
clear();
resize_dirty = false;
} else if (uiinput == 12) // Redraw screen on ^L
clear();
}
if (uiinput == '\n' || (uiinput == _settings.keybindings.enter && !typeahead)) {
// If typeahead is active clear it's state and free the structure.
if (typeahead) {
free (searchstr);
typeahead = false;
if (!_settings.cursor_always_visible)
curs_set (0);
typeaheadskip = 0;
}
// Check if we have no items at all!
// Don't even try to view a non existant item.
if (highlighted) {
UIDisplayItem (highlighted, current_feed);
if (!highlighted->data->readstatus) {
highlighted->data->readstatus = true;
current_feed->mtime = curtime;
}
tmp_highlighted = highlighted;
tmp_first = first_scr_ptr;
// highlighted = first_scr_ptr;
// Moves highlight to next unread item.
while (highlighted->next && highlighted->data->readstatus) {
highlighted = highlighted->next;
if (++highlightnum > ymax && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
}
// If *highlighted points to a read entry now we have hit the last
// entry and there are no unread items anymore. Restore the original
// location in this case.
if (highlighted->data->readstatus) {
highlighted = tmp_highlighted;
first_scr_ptr = tmp_first;
}
}
}
// TAB key is decimal 9.
if (uiinput == 9 || uiinput == _settings.keybindings.typeahead) {
if (typeahead) {
if (searchstrlen == 0) {
typeahead = false;
// Typeahead now off.
if (!_settings.cursor_always_visible)
curs_set (0);
free (searchstr);
typeaheadskip = 0;
highlighted = savestart;
first_scr_ptr = savestart_first;
} else {
unsigned found = 0;
for (const struct newsitem * i = current_feed->items; i; i = i->next)
if (i->data->title && s_strcasestr (i->data->title, searchstr))
++found; // Substring match.
if (typeaheadskip == found - 1)
typeaheadskip = 0;
else if (found > 1)
++typeaheadskip; // If more than one match was found and user presses tab we will skip matches.
}
} else {
typeahead = true;
// Typeahead now on.
curs_set (1);
searchstr = malloc (2);
memset (searchstr, 0, 2);
searchstrlen = 0;
// Save all start positions.
savestart = highlighted;
savestart_first = first_scr_ptr;
}
}
// ctrl+g clears typeahead.
if (uiinput == 7) {
// But only if it was switched on previously.
if (typeahead) {
typeahead = false;
free (searchstr);
typeaheadskip = 0;
highlighted = savestart;
first_scr_ptr = savestart_first;
}
}
// ctrl+u clears line.
if (uiinput == 21) {
searchstrlen = 0;
searchstr = realloc (searchstr, 2);
memset (searchstr, 0, 2);
}
}
}
void UIMainInterface (void)
{
struct feed* first_scr_ptr = _feed_list; // First pointer on top of screen. Used for scrolling.
struct feed* savestart = first_scr_ptr;
struct feed* savestart_first = NULL;
struct feed* highlighted = _feed_list;
unsigned highlightnum = 1;
unsigned highlightline = LINES-1; // Line with current selected item cursor
const unsigned pagesz = LINES-2;
// will be moved to this line to have it in
// the same line as the highlight. Visually
// impaired users with screen readers seem
// to need this.
bool typeahead = false;
unsigned typeaheadskip = 0; // Number of typeahead matches to skip.
char* search = NULL; // Typeahead search string.
unsigned searchlen = 0; // " length.
char* filters[8] = { }; // Category filters. Must be NULL when not used!
unsigned numfilters = 0; // Number of active filters.
bool filteractivated = false;
bool update_smartfeeds = true;
bool andxor = false; // Toggle for AND/OR combinations of filters.
while (1) {
if (update_smartfeeds) {
// This only needs to be done if new items are added, old removed.
// Reload, add, delete.
SmartFeedsUpdate();
update_smartfeeds = false;
}
erase();
char* filterstring = NULL;
if (filters[0]) {
numfilters = 0;
size_t len = 0;
for (unsigned i = 0; i < sizeof(filters)/sizeof(filters[0]) && filters[i]; ++i, ++numfilters)
len += strlen (filters[i]) + strlen (", ");
filterstring = calloc (len, 1);
for (unsigned i = 0; i < numfilters; ++i) {
if (i)
strcat (filterstring, ", ");
strcat (filterstring, filters[i]);
}
}
UISupportDrawHeader (filterstring);
if (filterstring)
free (filterstring);
filterstring = NULL;
// If a filter is defined we need to make copy of the pointers in
// struct feed and work on that.
// Build a new list only with items matching current filter.
//
// Never EVER set filteractivated=true if there is no filter defined!
// This should be moved to its own function in ui-support.c!
if (filteractivated) {
if (!_unfiltered_feed_list)
_unfiltered_feed_list = _feed_list;
_feed_list = NULL;
for (const struct feed * cur_ptr = _unfiltered_feed_list; cur_ptr; cur_ptr = cur_ptr->next) {
unsigned found = 0;
if (!cur_ptr->feedcategories)
continue;
for (const struct feedcategories * c = cur_ptr->feedcategories; c; c = c->next)
for (unsigned i = 0; i < 8 && filters[i]; ++i)
if (strcasecmp (c->name, filters[i]) == 0) // Match found
++found;
if (!found)
continue;
bool addfilteritem = false;
if (!andxor) { // AND
if (found == numfilters)
addfilteritem = true;
} else if (found) // OR
addfilteritem = true;
if (addfilteritem) {
struct feed* new_feed = newFeedStruct();
// These structs will get leaked!
// Yeah, yeah, it is on my todo list...
// Though valgrind doesn't report them as leaked.
// Duplicate pointers
new_feed->feedurl = cur_ptr->feedurl;
new_feed->xmltext = cur_ptr->xmltext;
new_feed->title = cur_ptr->title;
new_feed->link = cur_ptr->link;
new_feed->description = cur_ptr->description;
new_feed->lasterror = cur_ptr->lasterror;
new_feed->items = cur_ptr->items;
new_feed->problem = cur_ptr->problem;
new_feed->custom_title = cur_ptr->custom_title;
new_feed->original = cur_ptr->original;
new_feed->perfeedfilter = cur_ptr->perfeedfilter;
new_feed->execurl = cur_ptr->execurl;
new_feed->feedcategories = cur_ptr->feedcategories;
// Add to bottom of pointer chain.
AddFeedToList (new_feed);
}
}
first_scr_ptr = _feed_list;
highlighted = first_scr_ptr;
filteractivated = false;
}
unsigned ypos = 2, itemnum = 1;
if (typeahead) {
// This resets the offset for every typeahead loop.
first_scr_ptr = _feed_list;
unsigned count = 0, skipper = 0;
bool found = false;
for (struct feed* cur_ptr = _feed_list; cur_ptr; ++count, cur_ptr = cur_ptr->next) {
// count+1 >= pagesz: if the _next_ line would go over the boundary.
if (count + 1 > pagesz && first_scr_ptr->next)
first_scr_ptr = first_scr_ptr->next;
// Exact match from beginning of line.
if (searchlen > 0 && s_strcasestr (cur_ptr->title, search)) {
found = true;
highlighted = cur_ptr;
if (typeaheadskip >= 1 && skipper != typeaheadskip)
++skipper;
break;
}
}
if (!found) {
// Restore original position on no match.
highlighted = savestart;
first_scr_ptr = savestart_first;
}
}
for (struct feed * cur_ptr = first_scr_ptr; cur_ptr; cur_ptr = cur_ptr->next) {
// Set cursor to start of current line and clear it.
move (ypos, 0);
clrtoeol();
// Determine number of new items in feed.
unsigned newcount = 0;
for (const struct newsitem * i = cur_ptr->items; i; i = i->next)
if (!i->data->readstatus)
++newcount;
// Make highlight if we are the highlighted feed
if (cur_ptr == highlighted) {
highlightline = ypos;
highlightnum = itemnum;
attron (WA_REVERSE);
mvhline (ypos, 0, ' ', COLS);
}
int columns;
if (cur_ptr->feedcategories != NULL)
columns = COLS - 26 - strlen ("new");
else
columns = COLS - 9 - strlen ("new");
mvaddn_utf8 (ypos, 1, cur_ptr->title, columns);
if (xmlStrlen ((xmlChar*) cur_ptr->title) > columns)
mvaddstr (ypos, columns + 1, "...");
if (cur_ptr->problem)
mvaddch (ypos, 0, '!');
// Execute this _after_ the for loop. Otherwise it'll suck CPU like crazy!
if (newcount) {
const char* localized_msg = ngettext ("%3u new", "%3u new", newcount);
char msgbuf[16];
snprintf (msgbuf, sizeof (msgbuf), localized_msg, newcount);
mvadd_utf8 (ypos, COLS - 1 - utf8_length (localized_msg), msgbuf);
}
if (cur_ptr->feedcategories != NULL)
mvaddn_utf8 (ypos, COLS - 21 - strlen ("new"), cur_ptr->feedcategories->name, 15);
++ypos;
if (cur_ptr == highlighted)
attroff (WA_REVERSE);
if (itemnum >= pagesz)
break;
++itemnum;
}
if (typeahead) {
char msgbuf[128];
snprintf (msgbuf, sizeof (msgbuf), "-> %s", search);
UIStatus (msgbuf, 0, 0);
} else {
char msgbuf[128];
snprintf (msgbuf, sizeof (msgbuf), _("Press '%c' for help window."), _settings.keybindings.help);
if (easterEgg())
snprintf (msgbuf, sizeof (msgbuf), _("Press '%c' for help window. (Press '%c' to play Santa Hunta!)"), _settings.keybindings.help, _settings.keybindings.about);
UIStatus (msgbuf, 0, 0);
}
move (highlightline, 0);
int uiinput = getch();
if (typeahead) {
// Only match real characters.
if (uiinput >= ' ' && uiinput <= '~') {
search[searchlen] = uiinput;
search[++searchlen] = 0;
search = realloc (search, searchlen + 1);
} else if (uiinput == 127 || uiinput == 263) {
// Don't let searchlen go below 0!
if (searchlen > 0)
search[--searchlen] = 0;
else {
typeahead = false;
free (search);
}
}
} else {
if (uiinput == _settings.keybindings.quit) {
// Restore original _feed_list if filter is defined!
if (filters[0])
_feed_list = _unfiltered_feed_list;
MainQuit (NULL, NULL);
} else if (uiinput == _settings.keybindings.reload || uiinput == _settings.keybindings.forcereload) {
if (filters[0])
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
if (highlighted && uiinput == _settings.keybindings.forcereload) {
free (highlighted->lasterror);
highlighted->lasterror = NULL;
}
UpdateFeed (highlighted);
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.reloadall) {
if (filters[0])
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
UpdateAllFeeds();
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.addfeed || uiinput == _settings.keybindings.newheadlines) {
if (filters[0])
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else if (uiinput == _settings.keybindings.addfeed) {
switch (UIAddFeed (NULL)) {
case 0:
UIStatus (_("Successfully added new item..."), 1, 0);
_feed_list_changed = true;
break;
case 2:
UIStatus (_("Invalid URL! Please add http:// if you forgot this."), 2, 1);
break;
case -1:
UIStatus (_("There was a problem adding the feed!"), 2, 1);
break;
default:
break;
}
} else if (!SmartFeedExists ("newitems")) {
UIAddFeed ("smartfeed:/newitems");
_feed_list_changed = true;
}
// Scroll to top of screen and redraw everything.
highlighted = _feed_list;
first_scr_ptr = _feed_list;
update_smartfeeds = true;
} else if (uiinput == _settings.keybindings.help || uiinput == '?')
UIHelpScreen();
else if (uiinput == _settings.keybindings.deletefeed) {
// This should be moved to its own function in ui-support.c!
if (filters[0]) {
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
continue;
}
// Move this code into its own function!
// If the deleted feed was the last one of a specific category,
// remove this category from the global list.
if (highlighted) {
if (UIDeleteFeed (highlighted->title) == 1) {
// Do it!
struct feed* removed = highlighted;
// Save current highlight position.
struct feed* saved_highlighted = highlighted;
// Remove cachefile from filesystem.
char* hashme = Hashify (highlighted->feedurl);
char cachefilename[PATH_MAX];
CacheFilePath (hashme, cachefilename, sizeof(cachefilename));
free (hashme);
// Errors from unlink can be ignored. Worst thing that happens is that
// we delete a file that doesn't exist.
unlink (cachefilename);
// Unlink pointer from chain.
if (highlighted == _feed_list) {
// first element
if (_feed_list->next) {
_feed_list = _feed_list->next;
first_scr_ptr = _feed_list;
highlighted->next->prev = NULL;
} else {
_feed_list = NULL;
first_scr_ptr = NULL;
}
// Set new highlighted to _feed_list again.
saved_highlighted = _feed_list;
} else if (highlighted->next == NULL) {
// last element
// Set new highlighted to element before deleted one.
saved_highlighted = highlighted->prev;
// If highlighted was first line move first line upward in pointer chain.
if (highlighted == first_scr_ptr)
first_scr_ptr = first_scr_ptr->prev;
highlighted->prev->next = NULL;
} else {
// element inside list */
// Set new highlighted to element after deleted one.
saved_highlighted = highlighted->next;
// If highlighted was last line, move first line downward in pointer chain.
if (highlighted == first_scr_ptr)
first_scr_ptr = first_scr_ptr->next;
highlighted->prev->next = highlighted->next;
highlighted->next->prev = highlighted->prev;
}
// Put highlight to new highlight position.
highlighted = saved_highlighted;
// free (removed) pointer
if (!removed->smartfeed) {
if (removed->items) {
while (removed->items->next) {
removed->items = removed->items->next;
free (removed->items->prev->data->title);
free (removed->items->prev->data->link);
free (removed->items->prev->data->description);
free (removed->items->prev);
}
free (removed->items->data->title);
free (removed->items->data->link);
free (removed->items->data->description);
free (removed->items);
}
free (removed->feedurl);
free (removed->xmltext);
removed->xmltext = NULL;
removed->content_length = 0;
free (removed->title);
free (removed->link);
free (removed->description);
free (removed->lasterror);
free (removed->custom_title);
free (removed->original);
free (removed);
}
_feed_list_changed = true;
update_smartfeeds = true;
}
}
} else if ((uiinput == KEY_UP || uiinput == _settings.keybindings.prev) && highlighted && highlighted->prev) {
highlighted = highlighted->prev;
if (--highlightnum < 1 && first_scr_ptr->prev) { // Reached first onscreen entry.
++highlightnum;
first_scr_ptr = first_scr_ptr->prev;
}
} else if ((uiinput == KEY_DOWN || uiinput == _settings.keybindings.next) && highlighted && highlighted->next) {
highlighted = highlighted->next;
if (++highlightnum > pagesz && first_scr_ptr->next) { // If we fall off the screen, advance first_scr_ptr to next entry.
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
} else if (uiinput == KEY_NPAGE || uiinput == ' ' || uiinput == _settings.keybindings.pdown) {
// Move highlight one page up/down == pagesz
for (unsigned i = 0; i < pagesz && highlighted->next; ++i) {
highlighted = highlighted->next;
if (++highlightnum > pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
}
} else if (uiinput == KEY_PPAGE || uiinput == _settings.keybindings.pup) {
for (unsigned i = 0; i < pagesz && highlighted->prev; ++i) {
highlighted = highlighted->prev;
if (--highlightnum < 1 && first_scr_ptr->prev) {
++highlightnum;
first_scr_ptr = first_scr_ptr->prev;
}
}
} else if (uiinput == KEY_HOME || uiinput == _settings.keybindings.home) {
highlighted = first_scr_ptr = _feed_list;
highlightnum = 0;
} else if (uiinput == KEY_END || uiinput == _settings.keybindings.end) {
highlighted = first_scr_ptr = _feed_list;
highlightnum = 0;
while (highlighted && highlighted->next) {
highlighted = highlighted->next;
if (++highlightnum >= pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
}
} else if (uiinput == _settings.keybindings.moveup) {
// This function is deactivated when a filter is active.
if (filters[0] != NULL) {
UIStatus (_("You cannot move items while a category filter is defined!"), 2, 0);
continue;
}
// Move item up.
if (highlighted) {
if (highlighted->prev != NULL) {
SwapPointers (highlighted, highlighted->prev);
highlighted = highlighted->prev;
if (highlightnum - 1 < 1 && first_scr_ptr->prev)
first_scr_ptr = first_scr_ptr->prev;
_feed_list_changed = true;
}
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.movedown) {
// This function is deactivated when a filter is active.
if (filters[0]) {
UIStatus (_("You cannot move items while a category filter is defined!"), 2, 0);
continue;
}
// Move item down.
if (highlighted) {
if (highlighted->next) {
SwapPointers (highlighted, highlighted->next);
highlighted = highlighted->next;
if (++highlightnum >= pagesz && first_scr_ptr->next) {
--highlightnum;
first_scr_ptr = first_scr_ptr->next;
}
_feed_list_changed = true;
}
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.dfltbrowser) {
UIChangeBrowser();
SaveBrowserSetting();
} else if (uiinput == _settings.keybindings.markallread) {
// This function is safe for using in filter mode, because it only
// changes int values. It automatically marks the correct ones read
// if a filter is applied since we are using a copy of the main data.
time_t curtime = time (NULL);
for (struct feed* f = _feed_list; f; f = f->next) {
for (struct newsitem* i = f->items; i; i = i->next) {
if (!i->data->readstatus) {
i->data->readstatus = true;
f->mtime = curtime;
}
}
}
} else if (uiinput == _settings.keybindings.about)
UIAbout();
else if (uiinput == _settings.keybindings.changefeedname && highlighted) {
if (filters[0]) // This needs to be worked on before it works while a filter is applied!
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
UIChangeFeedName (highlighted);
_feed_list_changed = true;
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.perfeedfilter && highlighted) {
if (filters[0])
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
UIPerFeedFilter (highlighted);
_feed_list_changed = true;
}
} else if (uiinput == _settings.keybindings.sortfeeds && highlighted) {
if (filters[0]) // Deactivate sorting function if filter is applied.
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
SnowSort();
_feed_list_changed = true;
update_smartfeeds = true;
}
} else if (uiinput == _settings.keybindings.categorize && highlighted && !highlighted->smartfeed) {
if (filters[0]) // This needs to be worked on before it works while a filter is applied!
UIStatus (_("Please deactivate the category filter before using this function."), 2, 0);
else {
CategorizeFeed (highlighted);
_feed_list_changed = true;
}
} else if (uiinput == _settings.keybindings.filter) {
// GUI to set a filter
char* catfilter = DialogGetCategoryFilter();
if (catfilter) {
unsigned iunusedfilter = 0;
while (iunusedfilter < 8 && filters[iunusedfilter])
++iunusedfilter;
if (iunusedfilter < 8) {
filters[iunusedfilter] = strdup (catfilter);
filteractivated = true;
}
} else {
ResetFilters (filters);
// If DialogGetCategoryFilter() was used to switch off filter
// Restore _feed_list here.
if (_unfiltered_feed_list) {
// Restore _feed_list
_feed_list = _unfiltered_feed_list;
first_scr_ptr = _feed_list;
highlighted = _feed_list;
_unfiltered_feed_list = NULL;
}
}
free (catfilter);
_feed_list_changed = true;
} else if (uiinput == _settings.keybindings.filtercurrent) {
// Set filter to primary filter of this feed.
// Free filter if it's not empty to avoid memory leaks!
if (filters[0]) {
ResetFilters (filters);
// Set new filter to primary category.
// highlighted can be null if there is no feed highlighted.
// First start, no feeds ever added, multiple filters applied that no feed matches.
if (highlighted && highlighted->feedcategories) {
filters[0] = strdup (highlighted->feedcategories->name);
filteractivated = true;
}
// Restore _feed_list
_feed_list = _unfiltered_feed_list;
first_scr_ptr = _feed_list;
highlighted = _feed_list;
_unfiltered_feed_list = NULL;
} else if (highlighted && highlighted->feedcategories) {
filters[0] = strdup (highlighted->feedcategories->name);
filteractivated = true;
}
} else if (uiinput == _settings.keybindings.nofilter) {
// Remove all filters.
if (filters[0]) {
ResetFilters (filters);
// Restore _feed_list
_feed_list = _unfiltered_feed_list;
first_scr_ptr = _feed_list;
highlighted = _feed_list;
_unfiltered_feed_list = NULL;
}
} else if (uiinput == 'X' && filters[0]) { // AND or OR matching for the filer.
andxor = !andxor;
filteractivated = true; // Reconstruct filter.
} else if (resize_dirty || uiinput == KEY_RESIZE) {
clear();
resize_dirty = false;
} else if (uiinput == 12) // Redraw screen on ^L
clear();
}
if (uiinput == '\n' || (uiinput == _settings.keybindings.enter && !typeahead)) {
// If typeahead is active clear it's state and free the structure.
if (typeahead) {
free (search);
typeahead = false;
// Clear typeaheadskip flag, otherwise type ahead breaks, because we
// have a typeaheadskip "offset" when using it again.
typeaheadskip = 0;
}
// Select this feed, open and view entries.
// If contents of the feed was changed during display (reload),
// regenerate the smartfeed.
if (highlighted && UIDisplayFeed (highlighted))
update_smartfeeds = true;
// Clear screen after we return from here.
erase();
}
// TAB key is decimal 9.
if (uiinput == 9 || uiinput == _settings.keybindings.typeahead) {
if (typeahead) {
if (searchlen == 0) {
typeahead = false;
// Typeahead now off.
free (search);
typeaheadskip = 0;
highlighted = savestart;
first_scr_ptr = savestart_first;
} else {
unsigned found = 0;
for (const struct feed* f = _feed_list; f; f = f->next)
if (s_strcasestr (f->title, search)) // Substring match.
++found;
if (typeaheadskip == found - 1) // found-1 to avoid empty tab cycle.
typeaheadskip = 0;
else if (found > 1) // If more than one match was found and user presses tab we will typeaheadskip matches.
++typeaheadskip;
}
} else {
typeahead = true;
// Typeahead now on.
search = malloc (2);
memset (search, 0, 2);
searchlen = 0;
// Save all start positions.
savestart = highlighted;
savestart_first = first_scr_ptr;
}
}
// ctrl+g clears typeahead.
if (uiinput == 7) {
// But only if it was switched on previously.
if (typeahead) {
typeahead = false;
free (search);
typeaheadskip = 0;
highlighted = savestart;
first_scr_ptr = savestart_first;
}
}
// ctrl+u clears line.
if (uiinput == 21) {
searchlen = 0;
search = realloc (search, 2);
memset (search, 0, 2);
}
}
}