snownews/setup.c

471 lines
18 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 "setup.h"
#include "main.h"
#include "cat.h"
#include "feedio.h"
#include "uiutil.h"
#include "conv.h"
#include "parse.h"
#include <ctype.h>
#include <ncurses.h>
void ConfigFilePath (const char* filename, char* path, size_t pathsz)
{
const char* confhome = getenv ("XDG_CONFIG_HOME");
unsigned pathlen;
if (confhome)
pathlen = snprintf (path, pathsz, "%s/" SNOWNEWS_NAME "/%s", confhome, filename);
else
pathlen = snprintf (path, pathsz, "%s/.config/" SNOWNEWS_NAME "/%s", getenv("HOME"), filename);
if (pathlen >= pathsz)
MainQuit ("ConfigFilePath", _("configuration dir path is too long"));
if (path[pathlen-1] == '/')
path[pathlen-1] = 0;
}
void CacheFilePath (const char* filename, char* path, size_t pathsz)
{
const char* datahome = getenv ("XDG_DATA_HOME");
unsigned pathlen;
if (datahome)
pathlen = snprintf (path, pathsz, "%s/" SNOWNEWS_NAME "/%s", datahome, filename);
else
pathlen = snprintf (path, pathsz, "%s/.local/share/" SNOWNEWS_NAME "/%s", getenv("HOME"), filename);
if (pathlen >= pathsz)
MainQuit ("CacheFilePath", _("cache dir path is too long"));
if (path[pathlen-1] == '/')
path[pathlen-1] = 0;
}
static char* LoadFile (const char* filename)
{
struct stat st;
if (0 != stat (filename, &st) || !S_ISREG(st.st_mode) || !st.st_size)
return NULL;
int fd = open (filename, O_RDONLY);
if (fd < 0)
return NULL;
char* r = calloc (1, st.st_size);
for (ssize_t br = 0; br < st.st_size;) {
ssize_t ec = read (fd, &r[br], st.st_size-br);
if (ec <= 0) {
if (errno == EINTR)
continue;
free (r);
return NULL;
}
br += ec;
}
return r;
}
// Load browser command from ~./snownews/browser.
static void SetupBrowser (const char* filename)
{
char* fbuf = LoadFile (filename);
if (!fbuf) {
_settings.browser = strdup ("lynx %s");
return;
}
size_t fbuflen = strlen(fbuf);
while (isspace (fbuf[fbuflen-1]))
fbuf[--fbuflen] = 0;
_settings.browser = fbuf;
}
void SaveBrowserSetting (void)
{
char browserfilename[PATH_MAX];
ConfigFilePath ("browser", browserfilename, sizeof(browserfilename));
FILE* browserfile = fopen (browserfilename, "w");
if (!browserfile)
MainQuit (_("Save settings (browser)"), strerror (errno));
fputs (_settings.browser, browserfile);
fclose (browserfile);
}
// Parse http_proxy environment variable and define proxyname and proxyport.
static void SetupProxy (void)
{
// Check for proxy environment variable.
const char* proxyenv = getenv ("http_proxy");
if (!proxyenv)
return;
// The pointer returned by getenv must not be altered.
// What about mentioning this in the manpage of getenv?
char* proxystring = strdup (proxyenv);
char* proxystrbase = proxystring;
strsep (&proxystring, "/");
if (proxystring) {
strsep (&proxystring, "/");
if (proxystring) {
_settings.proxyname = strdup (strsep (&proxystring, ":"));
if (proxystring)
_settings.proxyport = atoi (strsep (&proxystring, "/"));
}
}
free (proxystrbase);
}
// Define global keybindings and load user customized bindings.
static void SetupKeybindings (const char* filename)
{
FILE* configfile = fopen (filename, "r");
if (configfile) {
// Read keybindings and populate keybindings struct.
while (!feof (configfile)) {
char linebuf[128];
if (!fgets (linebuf, sizeof (linebuf), configfile))
break;
if (linebuf[0] == '#')
continue;
linebuf[strlen (linebuf) - 1] = 0; // chop newline
char* value = linebuf;
strsep (&value, ":");
if (strcmp (linebuf, "next item") == 0)
_settings.keybindings.next = value[0];
else if (strcmp (linebuf, "previous item") == 0)
_settings.keybindings.prev = value[0];
else if (strcmp (linebuf, "return to previous menu") == 0)
_settings.keybindings.prevmenu = value[0];
else if (strcmp (linebuf, "quit") == 0)
_settings.keybindings.quit = value[0];
else if (strcmp (linebuf, "add feed") == 0)
_settings.keybindings.addfeed = value[0];
else if (strcmp (linebuf, "delete feed") == 0)
_settings.keybindings.deletefeed = value[0];
else if (strcmp (linebuf, "mark feed as read") == 0)
_settings.keybindings.markread = value[0];
else if (strcmp (linebuf, "mark item unread") == 0)
_settings.keybindings.markunread = value[0];
else if (strcmp (linebuf, "mark all as read") == 0)
_settings.keybindings.markallread = value[0];
else if (strcmp (linebuf, "change default browser") == 0)
_settings.keybindings.dfltbrowser = value[0];
else if (strcmp (linebuf, "sort feeds") == 0)
_settings.keybindings.sortfeeds = value[0];
else if (strcmp (linebuf, "move item up") == 0)
_settings.keybindings.moveup = value[0];
else if (strcmp (linebuf, "move item down") == 0)
_settings.keybindings.movedown = value[0];
else if (strcmp (linebuf, "show feedinfo") == 0)
_settings.keybindings.feedinfo = value[0];
else if (strcmp (linebuf, "reload feed") == 0)
_settings.keybindings.reload = value[0];
else if (strcmp (linebuf, "force reload feed") == 0)
_settings.keybindings.forcereload = value[0];
else if (strcmp (linebuf, "reload all feeds") == 0)
_settings.keybindings.reloadall = value[0];
else if (strcmp (linebuf, "open url") == 0)
_settings.keybindings.urljump = value[0];
else if (strcmp (linebuf, "open item url in overview") == 0)
_settings.keybindings.urljump2 = value[0];
else if (strcmp (linebuf, "change feedname") == 0)
_settings.keybindings.changefeedname = value[0];
else if (strcmp (linebuf, "page up") == 0)
_settings.keybindings.pup = value[0];
else if (strcmp (linebuf, "page down") == 0)
_settings.keybindings.pdown = value[0];
else if (strcmp (linebuf, "top") == 0)
_settings.keybindings.home = value[0];
else if (strcmp (linebuf, "bottom") == 0)
_settings.keybindings.end = value[0];
else if (strcmp (linebuf, "categorize feed") == 0)
_settings.keybindings.categorize = value[0];
else if (strcmp (linebuf, "apply filter") == 0)
_settings.keybindings.filter = value[0];
else if (strcmp (linebuf, "only current category") == 0)
_settings.keybindings.filtercurrent = value[0];
else if (strcmp (linebuf, "remove filter") == 0)
_settings.keybindings.nofilter = value[0];
else if (strcmp (linebuf, "per feed filter") == 0)
_settings.keybindings.perfeedfilter = value[0];
else if (strcmp (linebuf, "help menu") == 0)
_settings.keybindings.help = value[0];
else if (strcmp (linebuf, "about") == 0)
_settings.keybindings.about = value[0];
else if (strcmp (linebuf, "toggle AND/OR filtering") == 0)
_settings.keybindings.andxor = value[0];
else if (strcmp (linebuf, "enter") == 0)
_settings.keybindings.enter = value[0];
else if (strcmp (linebuf, "show new headlines") == 0)
_settings.keybindings.newheadlines = value[0];
else if (strcmp (linebuf, "type ahead find") == 0)
_settings.keybindings.typeahead = value[0];
}
// Override old default settings and make sure there is no clash.
// Default browser is now B; b moved to page up.
if (_settings.keybindings.dfltbrowser == 'b')
_settings.keybindings.dfltbrowser = 'B';
fclose (configfile);
} else {
// Write default bindings if the file is not there
configfile = fopen (filename, "w");
fputs ("# Snownews keybindings configfile\n", configfile);
fputs ("# Main menu bindings\n", configfile);
fprintf (configfile, "add feed:%c\n", _settings.keybindings.addfeed);
fprintf (configfile, "delete feed:%c\n", _settings.keybindings.deletefeed);
fprintf (configfile, "reload all feeds:%c\n", _settings.keybindings.reloadall);
fprintf (configfile, "change default browser:%c\n", _settings.keybindings.dfltbrowser);
fprintf (configfile, "move item up:%c\n", _settings.keybindings.moveup);
fprintf (configfile, "move item down:%c\n", _settings.keybindings.movedown);
fprintf (configfile, "change feedname:%c\n", _settings.keybindings.changefeedname);
fprintf (configfile, "sort feeds:%c\n", _settings.keybindings.sortfeeds);
fprintf (configfile, "categorize feed:%c\n", _settings.keybindings.categorize);
fprintf (configfile, "apply filter:%c\n", _settings.keybindings.filter);
fprintf (configfile, "only current category:%c\n", _settings.keybindings.filtercurrent);
fprintf (configfile, "mark all as read:%c\n", _settings.keybindings.markallread);
fprintf (configfile, "remove filter:%c\n", _settings.keybindings.nofilter);
fprintf (configfile, "per feed filter:%c\n", _settings.keybindings.perfeedfilter);
fprintf (configfile, "toggle AND/OR filtering:%c\n", _settings.keybindings.andxor);
fprintf (configfile, "quit:%c\n", _settings.keybindings.quit);
fputs ("# Feed display menu bindings\n", configfile);
fprintf (configfile, "show feedinfo:%c\n", _settings.keybindings.feedinfo);
fprintf (configfile, "mark feed as read:%c\n", _settings.keybindings.markread);
fprintf (configfile, "mark item unread:%c\n", _settings.keybindings.markunread);
fputs ("# General keybindungs\n", configfile);
fprintf (configfile, "next item:%c\n", _settings.keybindings.next);
fprintf (configfile, "previous item:%c\n", _settings.keybindings.prev);
fprintf (configfile, "return to previous menu:%c\n", _settings.keybindings.prevmenu);
fprintf (configfile, "reload feed:%c\n", _settings.keybindings.reload);
fprintf (configfile, "force reload feed:%c\n", _settings.keybindings.forcereload);
fprintf (configfile, "open url:%c\n", _settings.keybindings.urljump);
fprintf (configfile, "open item url in overview:%c\n", _settings.keybindings.urljump2);
fprintf (configfile, "page up:%c\n", _settings.keybindings.pup);
fprintf (configfile, "page down:%c\n", _settings.keybindings.pdown);
fprintf (configfile, "top:%c\n", _settings.keybindings.home);
fprintf (configfile, "bottom:%c\n", _settings.keybindings.end);
fprintf (configfile, "enter:%c\n", _settings.keybindings.enter);
fprintf (configfile, "show new headlines:%c\n", _settings.keybindings.newheadlines);
fprintf (configfile, "help menu:%c\n", _settings.keybindings.help);
fprintf (configfile, "about:%c\n", _settings.keybindings.about);
fprintf (configfile, "type ahead find:%c\n", _settings.keybindings.typeahead);
fclose (configfile);
}
}
// Set up colors and load user customized colors.
static void SetupColors (const char* filename)
{
// If there is a config file, read it.
FILE* configfile = fopen (filename, "r");
if (configfile) {
while (!feof (configfile)) {
char linebuf[128];
if (!fgets (linebuf, sizeof (linebuf), configfile))
break;
if (linebuf[0] == '#')
continue;
linebuf[strlen (linebuf) - 1] = 0; // chop newline
char* value = linebuf;
strsep (&value, ":");
unsigned cval = atoi (value);
bool cvalbold = (cval > 7);
if (strcmp (linebuf, "enabled") == 0)
_settings.monochrome = !cval;
else if (strcmp (linebuf, "new item") == 0) {
_settings.color.newitems = cval;
_settings.color.newitemsbold = cvalbold;
} else if (strcmp (linebuf, "goto url") == 0) {
_settings.color.urljump = cval;
_settings.color.urljumpbold = cvalbold;
} else if (strcmp (linebuf, "feedtitle") == 0) {
_settings.color.feedtitle = cval;
_settings.color.feedtitlebold = cvalbold;
}
}
fclose (configfile);
} else {
// Write the file. This will write all setting with their
// values read before and new ones with the defaults.
configfile = fopen (filename, "w");
fputs ("# Snownews color definitons\n", configfile);
fputs ("# black:0\n", configfile);
fputs ("# red:1\n", configfile);
fputs ("# green:2\n", configfile);
fputs ("# orange:3\n", configfile);
fputs ("# blue:4\n", configfile);
fputs ("# magenta(tm):5\n", configfile);
fputs ("# cyan:6\n", configfile);
fputs ("# gray:7\n", configfile);
fputs ("# brightred:9\n", configfile);
fputs ("# brightgreen:10\n", configfile);
fputs ("# yellow:11\n", configfile);
fputs ("# brightblue:12\n", configfile);
fputs ("# brightmagenta:13\n", configfile);
fputs ("# brightcyan:14\n", configfile);
fputs ("# white:15\n", configfile);
fputs ("# no color:-1\n", configfile);
fprintf (configfile, "enabled:%d\n", !_settings.monochrome);
fprintf (configfile, "new item:%d\n", _settings.color.newitems);
fprintf (configfile, "goto url:%d\n", _settings.color.urljump);
fprintf (configfile, "feedtitle:%d\n", _settings.color.feedtitle);
fclose (configfile);
}
if (!_settings.monochrome) {
start_color();
// The following call will automagically implement -1 as the terminal's
// default color for fg/bg in init_pair.
use_default_colors();
// Internally used color pairs.
// Use with WA_BOLD to get bright color (orange->yellow, gray->white, etc).
init_pair (10, 1, -1); // red
init_pair (11, 2, -1); // green
init_pair (12, 3, -1); // orange
init_pair (13, 4, -1); // blue
init_pair (14, 5, -1); // magenta
init_pair (15, 6, -1); // cyan
init_pair (16, 7, -1); // gray
// Default terminal color color pair
init_pair (63, -1, -1);
// Initialize all color pairs we're gonna use.
init_pair (2, _settings.color.newitems - 8 * _settings.color.newitemsbold, -1); // New item.
init_pair (3, _settings.color.urljump - 8 * _settings.color.urljumpbold, -1); // Goto url line
init_pair (4, _settings.color.feedtitle - 8 * _settings.color.feedtitlebold, -1); // Feed title header
}
}
static unsigned ParseOldFeedListFile (char* flbuf)
{
unsigned numfeeds = 0;
for (char* lineiter = flbuf; lineiter;) {
char* line = strsep (&lineiter, "\n");
// File format is url|custom name|comma seperated categories|filters
const char* url = strsep (&line, "|");
if (!url || !url[0])
continue; // no url
const char* cname = strsep (&line, "|");
const char* categories = strsep (&line, "|");
const char* filters = line;
AddFeed (url, cname, categories, filters);
++numfeeds;
}
return numfeeds;
}
static unsigned SetupFeedList (const char* filename)
{
unsigned numfeeds = 0;
char* flbuf = LoadFile (filename);
if (!flbuf)
return numfeeds; // no feeds
// Check the format; old snownews style or OPML
if (0 != strncmp (flbuf, "<?xml", strlen("<?xml"))) {
numfeeds = ParseOldFeedListFile (flbuf);
_feed_list_changed = true; // force conversion to OPML
} else
numfeeds = ParseOPMLFile (flbuf);
free (flbuf);
return numfeeds;
}
// Migrates configuration from ~/.snownews to ~/.config/snownews and ~/.local/share/snownews
static void MigrateConfigToXDG (void)
{
char dirname [PATH_MAX];
const char* homedir = getenv ("HOME");
if (!homedir)
MainQuit ("Reading configuration", "you have no home directory");
// Migrate cache dir to ~/.local/snownews
snprintf (dirname, sizeof(dirname), "%s/.snownews/cache", homedir);
struct stat dirtest;
if (0 == stat (dirname, &dirtest) && S_ISDIR(dirtest.st_mode)) {
char cachedir [PATH_MAX];
CacheFilePath ("", cachedir, sizeof(cachedir));
if (0 != rename (dirname, cachedir))
MainQuit ("Migrating feed cache ~/.snownews/cache to ~/.local/share/snownews", strerror (errno));
}
// Migrate configuration dir to ~/.config/snownews
snprintf (dirname, sizeof(dirname), "%s/.snownews", homedir);
if (0 == stat (dirname, &dirtest) && S_ISDIR(dirtest.st_mode)) {
char configdir [PATH_MAX];
ConfigFilePath ("", configdir, sizeof(configdir));
if (0 != rename (dirname, configdir))
MainQuit ("Migrating configuration ~/.snownews to ~/.config/snownews", strerror (errno));
}
// Convert urls to urls.opml
char oldurls [PATH_MAX];
ConfigFilePath ("urls", oldurls, sizeof(oldurls));
if (0 == access (oldurls, F_OK)) {
char newurls [PATH_MAX];
ConfigFilePath ("urls.opml", newurls, sizeof(newurls));
if (0 != rename (oldurls, newurls))
MainQuit ("Migrating urls to urls.opml", strerror (errno));
_feed_list_changed = true; // force conversion to OPML
}
}
// Create snownews' config directories if they do not exist yet,
// load global configuration settings and caches.
//
// Returns number of feeds successfully loaded.
//
unsigned Config (void)
{
// Set umask to 077 for all created files.
umask (0077);
MigrateConfigToXDG();
SetupProxy();
// Setup config directories.
char dirname [PATH_MAX];
ConfigFilePath ("", dirname, sizeof(dirname));
struct stat dirtest;
if (0 > stat (dirname, &dirtest)) {
if (0 > mkdir (dirname, S_IRWXU))
MainQuit (dirname, strerror (errno));
} else if (!S_ISDIR (dirtest.st_mode))
MainQuit (dirname, "is a file");
CacheFilePath ("", dirname, sizeof(dirname));
if (0 > stat (dirname, &dirtest)) {
if (0 > mkdir (dirname, S_IRWXU))
MainQuit (dirname, strerror (errno));
} else if (!S_ISDIR (dirtest.st_mode))
MainQuit (dirname, "is a file");
UIStatus (_("Reading configuration settings..."), 0, 0);
char filename[PATH_MAX];
ConfigFilePath ("browser", filename, sizeof(filename));
SetupBrowser (filename);
ConfigFilePath ("keybindings", filename, sizeof(filename));
SetupKeybindings (filename);
ConfigFilePath ("colors", filename, sizeof(filename));
SetupColors (filename);
ConfigFilePath ("urls.opml", filename, sizeof(filename));
return SetupFeedList (filename);
}