snownews/uiutil.c

392 lines
10 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 "uiutil.h"
#include <ncurses.h>
//----------------------------------------------------------------------
enum clear_line { NORMAL, INVERSE };
//----------------------------------------------------------------------
static const char* SnowSortIgnore (const char* title);
static void SmartFeedNewitems (struct feed* smart_feed);
static void clearLine (unsigned line, enum clear_line how);
//----------------------------------------------------------------------
// Init the ncurses library.
void InitCurses (void)
{
initscr();
keypad (stdscr, TRUE); // Activate keypad so we can read function keys with getch.
cbreak(); // No buffering.
noecho(); // Do not echo typed chars onto the screen.
ESCDELAY = 10; // Only wait 10ms after ESC
clear();
curs_set (_settings.cursor_always_visible);
}
// Print text in statusbar.
void UIStatus (const char* text, int delay, int warning)
{
int attr = WA_REVERSE;
if (warning)
attr |= COLOR_PAIR (10);
attron (attr);
clearLine (LINES - 1, INVERSE);
attron (WA_REVERSE);
mvaddn_utf8 (LINES - 1, 1, text, COLS - 2);
attroff (attr);
refresh();
if (delay)
sleep (delay);
}
// Swap all pointers inside a feed struct except next and prev
void SwapPointers (struct feed* one, struct feed* two)
{
struct feed* two_next = two->next, *two_prev = two->prev;
struct feed tmp = *one;
*one = *two;
*two = tmp;
one->next = two->next;
one->prev = two->prev;
two->next = two_next;
two->prev = two_prev;
}
// Ignore "A", "The", etc. prefixes when sorting feeds.
static const char* SnowSortIgnore (const char* title)
{
if (strncasecmp (title, "a ", strlen ("a ")) == 0)
return title + strlen ("a ");
else if (strncasecmp (title, "the ", strlen ("the ")) == 0)
return title + strlen ("the ");
return title;
}
// Sort the struct list alphabetically.
// Sorting criteria is struct feed->title
void SnowSort (void)
{
// If there is no element do not run sort or it'll crash!
if (!_feed_list)
return;
UIStatus (_("Sorting, please wait..."), 0, 0);
unsigned elements = 0;
for (const struct feed* f = _feed_list; f; f = f->next)
++elements;
for (unsigned i = 0; i <= elements; ++i) {
for (struct feed* f = _feed_list; f->next; f = f->next) {
const char* one = SnowSortIgnore (f->title);
const char* two = SnowSortIgnore (f->next->title);
if (strcasecmp (one, two) > 0)
SwapPointers (f, f->next);
}
}
}
// Draw a filled box with WA_REVERSE at coordinates x1y1/x2y2
void UISupportDrawBox (unsigned x1, unsigned y1, unsigned x2, unsigned y2)
{
attron (WA_REVERSE);
for (unsigned y = y1; y <= y2; ++y) {
move (y, x1);
for (unsigned x = x1; x <= x2; ++x) {
int c = ' ';
if (y == y1) {
if (x == x1)
c = ACS_ULCORNER;
else if (x == x2)
c = ACS_URCORNER;
else
c = ACS_HLINE;
} else if (y == y2) {
if (x == x1)
c = ACS_LLCORNER;
else if (x == x2)
c = ACS_LRCORNER;
else
c = ACS_HLINE;
} else if (x == x1 || x == x2)
c = ACS_VLINE;
addch (c);
}
}
attroff (WA_REVERSE);
}
// Draw main program header.
void UISupportDrawHeader (const char* headerstring)
{
if (!headerstring)
headerstring = "* " SNOWNEWS_NAME " " SNOWNEWS_VERSTRING;
clearLine (0, INVERSE);
attron (WA_REVERSE);
mvadd_utf8 (0, 1, headerstring);
attroff (WA_REVERSE);
}
// Take a URL and execute in default browser.
// Apply all security restrictions before running system()!
void UISupportURLJump (const char* url)
{
if (!url)
return; // Should not happen. Nope, really should not happen.
if (strncmp (url, "smartfeed", strlen ("smartfeed")) == 0)
return; // Smartfeeds cannot be opened (for now).
if (strchr (_settings.browser, '\'')) {
UIStatus (_("Unsafe browser string (contains quotes)! See snownews.kcore.de/faq#toc2.4"), 5, 1);
return; // Complain loudly if browser string contains a single quote.
}
if (strchr (url, '\'')) {
UIStatus (_("Invalid URL passed. Possible shell exploit attempted!"), 3, 1);
return;
}
char escapedurl[PATH_MAX];
snprintf (escapedurl, sizeof (escapedurl), "'%s' 2>/dev/null", url);
char browcall[PATH_MAX];
snprintf (browcall, sizeof (browcall), _settings.browser, escapedurl);
char execmsg[sizeof (browcall) + strlen ("Executing ")];
snprintf (execmsg, sizeof (execmsg), _("Executing %s"), browcall);
UIStatus (execmsg, 0, 0);
endwin();
system (browcall);
}
void SmartFeedsUpdate (void)
{
for (struct feed* f = _feed_list; f; f = f->next)
if (f->smartfeed)
SmartFeedNewitems (f);
}
static void SmartFeedNewitems (struct feed* smart_feed)
{
// Be smart and don't leak the smart feed.
// The items->data structures must not be freed, since a smart feed is only
// a pointer collection and does not contain allocated memory.
while (smart_feed->items) {
struct newsitem* i2f = smart_feed->items;
smart_feed->items = smart_feed->items->next;
free (i2f);
}
for (struct feed* f = _feed_list; f; f = f->next) {
// Do not add the smart feed recursively. 8)
if (f == smart_feed)
continue;
for (struct newsitem* item = f->items; item; item = item->next) {
if (item->data->readstatus)
continue;
// If item is unread, add to smart feed.
struct newsitem* new_item = calloc (1, sizeof (struct newsitem));
new_item->data = item->data;
// Find insertion point, sorted with latest date first
struct newsitem* prev = NULL;
if (smart_feed->items && smart_feed->items->data->date >= new_item->data->date) {
prev = smart_feed->items;
while (prev->next && prev->data->date >= new_item->data->date)
prev = prev->next;
}
// Add to data structure.
if (!prev) {
new_item->next = smart_feed->items;
smart_feed->items = new_item; // Add to beginning of the list
} else {
new_item->next = prev->next;
new_item->prev = prev;
prev->next = new_item;
}
}
}
// Only fill out once.
if (!smart_feed->title)
smart_feed->title = strdup (_("(New headlines)"));
if (!smart_feed->link)
smart_feed->link = strdup (smart_feed->feedurl);
}
bool SmartFeedExists (const char* smartfeed)
{
// Find our smart feed.
if (strcmp (smartfeed, "newitems") == 0)
for (const struct feed* f = _feed_list; f; f = f->next)
if (f->smartfeed == 1)
return true;
return false;
}
void DrawProgressBar (unsigned numobjects, unsigned titlestrlen)
{
attron (WA_REVERSE);
mvhline (LINES - 1, titlestrlen + 1, '=', numobjects);
mvaddch (LINES - 1, COLS - 3, ']');
attroff (WA_REVERSE);
refresh();
}
static void clearLine (unsigned line, enum clear_line how)
{
if (how == INVERSE)
attron (WA_REVERSE);
mvhline (line, 0, ' ', COLS);
if (how == INVERSE)
attron (WA_REVERSE);
}
// Returns the number of bytes in the character whose first char is c
static unsigned utf8_ibytes (char c)
{
// Count the leading bits. Header bits are 1 * nBytes followed by a 0.
// 0 - single byte character. Take 7 bits (0xff >> 1)
// 1 - error, in the middle of the character. Take 6 bits (0xff >> 2)
// so you will keep reading invalid entries until you hit the next character.
// >2 - multibyte character. Take remaining bits, and get the next bytes.
//
unsigned n = 0u;
for (uint8_t mask = 0x80; c & mask; ++n)
mask >>= 1;
return n+!n; // A sequence is always at least 1 byte.
}
static wchar_t utf8_next (const char** pt)
{
const char* i = *pt;
unsigned n = utf8_ibytes (*i);
wchar_t v = *i & (0xff >> n); // First byte contains bits after the header.
while (--n && *++i) // Each subsequent byte has 6 bits.
v = (v << 6) | (*i & 0x3f);
*pt = ++i;
return v;
}
unsigned utf8_length (const char* s)
{
unsigned l = 0;
while (utf8_next (&s))
++l;
return l;
}
unsigned utf8_range_length (const char* f, const char* l)
{
unsigned rl = 0;
while (f < l && utf8_next (&f))
++rl;
return rl;
}
void addn_utf8 (const char* s, unsigned n)
{
#if NCURSES_WIDECHAR
attr_t attr = 0;
short cpair = 0;
attr_get (&attr, &cpair, NULL);
wchar_t wchzs[2] = { 0, 0 };
cchar_t ch = {};
while (n-- && (wchzs[0] = utf8_next (&s))) {
setcchar (&ch, wchzs, attr, cpair, NULL);
add_wch (&ch);
}
#else
wchar_t wc;
while (n-- && (wc = utf8_next (&s)))
addch (wc < CHAR_MAX ? (char) wc : '?');
#endif
}
void add_utf8 (const char* s)
{
int x = getcurx (stdscr);
unsigned w = getmaxx (stdscr);
addn_utf8 (s, w-x);
}
void mvaddn_utf8 (int y, int x, const char* s, unsigned n)
{
move (y, x);
addn_utf8 (s, n);
}
void mvadd_utf8 (int y, int x, const char* s)
{
move (y, x);
add_utf8 (s);
}
unsigned utf8_osize (wchar_t c)
{
if (c > 0x1fffff)
return 0;
else if (c > 0xffff)
return 4;
else if (c > 0x7ff)
return 3;
else if (c > 0x7f)
return 2;
return 1;
}
char* utf8_write (wchar_t c, char* o)
{
unsigned n = utf8_osize (c);
if (!n)
return o;
if (n <= 1)
*o++ = c;
else {
unsigned btw = n * 6;
*o++ = ((c >> (btw -= 6)) & 0x3f) | (0xff << (8 - n));
while (btw)
*o++ = ((c >> (btw -= 6)) & 0x3f) | 0x80;
}
return o;
}
void wrap_text (char* text, unsigned width)
{
unsigned textlen = strlen (text);
char* t = text;
char* tend = t + textlen;
char* sp = NULL;
unsigned linelen = 0;
while (t < tend) {
char* pc = t;
wchar_t c = utf8_next ((const char**) &t);
if (c == '\n') {
linelen = 0;
sp = NULL;
continue;
} else if (c == ' ')
sp = pc;
if (++linelen >= width && sp) {
*sp = '\n';
linelen = pc - sp;
}
}
}