diff options
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | catgirl.1 | 41 | ||||
| -rw-r--r-- | chat.c | 20 | ||||
| -rw-r--r-- | chat.h | 7 | ||||
| -rw-r--r-- | config.c | 178 | 
5 files changed, 234 insertions, 13 deletions
@@ -7,6 +7,7 @@ LDLIBS = -lcurses -lcrypto -ltls  OBJS += chat.o  OBJS += command.o +OBJS += config.o  OBJS += edit.o  OBJS += handle.o  OBJS += irc.o @@ -19,6 +19,7 @@  .Op Fl r Ar real  .Op Fl u Ar user  .Op Fl w Ar pass +.Op Ar config ...  .  .Sh DESCRIPTION  The @@ -27,9 +28,25 @@ program is a curses  TLS-only IRC client.  .  .Pp +Options can be loaded from files +listed on the command line. +Files are searched for in +.Pa $XDG_CONFIG_DIRS/catgirl +unless the path starts with +.Ql / +or +.Ql \&. . +Each option is placed on a line, +and lines beginning with +.Ql # +are ignored. +The options are listed below +following their corresponding flags. +. +.Pp  The arguments are as follows:  .Bl -tag -width Ds -.It Fl a Ar user Ns : Ns Ar pass +.It Fl a Ar user Ns : Ns Ar pass , Cm sasl-plain = Ar user Ns : Ns Ar pass  Authenticate as  .Ar user  with @@ -40,7 +57,7 @@ in plain text,  it is recommended to use SASL EXTERNAL instead with  .Fl e .  . -.It Fl c Ar path +.It Fl c Ar path , Cm cert = Ar path  Load the TLS client certificate from  .Ar path .  If the private key is in a separate file, @@ -50,52 +67,52 @@ With  .Fl e ,  authenticate using SASL EXTERNAL.  . -.It Fl e +.It Fl e , Cm sasl-external  Authenticate using SASL EXTERNAL,  also known as CertFP.  The TLS client certificate is loaded with  .Fl c .  . -.It Fl h Ar host +.It Fl h Ar host , Cm host = Ar host  Connect to  .Ar host .  . -.It Fl j Ar join +.It Fl j Ar join , Cm join = Ar join  Join the comma-separated list of channels  .Ar join .  . -.It Fl k Ar path +.It Fl k Ar path , Cm priv = Ar priv  Load the TLS client private key from  .Ar path .  . -.It Fl n Ar nick +.It Fl n Ar nick , Cm nick = Ar nick  Set nickname to  .Ar nick .  The default nickname is the user's name.  . -.It Fl p Ar port +.It Fl p Ar port , Cm port = Ar port  Connect to  .Ar port .  The default port is 6697.  . -.It Fl r Ar real +.It Fl r Ar real , Cm real = Ar real  Set realname to  .Ar real .  The default realname is the same as the nickname.  . -.It Fl u Ar user +.It Fl u Ar user , Cm user = Ar user  Set username to  .Ar user .  The default username is the same as the nickname.  . -.It Fl v +.It Fl v , Cm debug  Log raw IRC messages to the  .Sy <debug>  window  as well as standard error  if it is not a terminal.  . -.It Fl w Ar pass +.It Fl w Ar pass , Cm pass = Ar pass  Log in with the server password  .Ar pass .  .El @@ -63,8 +63,26 @@ int main(int argc, char *argv[]) {  	const char *user = NULL;  	const char *real = NULL; +	const char *Opts = "!a:c:eh:j:k:n:p:r:u:vw:"; +	const struct option LongOpts[] = { +		{ "insecure", no_argument, NULL, '!' }, +		{ "sasl-plain", required_argument, NULL, 'a' }, +		{ "cert", required_argument, NULL, 'c' }, +		{ "sasl-external", no_argument, NULL, 'e' }, +		{ "host", required_argument, NULL, 'h' }, +		{ "join", required_argument, NULL, 'j' }, +		{ "priv", required_argument, NULL, 'k' }, +		{ "nick", required_argument, NULL, 'n' }, +		{ "port", required_argument, NULL, 'p' }, +		{ "real", required_argument, NULL, 'r' }, +		{ "user", required_argument, NULL, 'u' }, +		{ "debug", no_argument, NULL, 'v' }, +		{ "pass", required_argument, NULL, 'w' }, +		{0}, +	}; +  	int opt; -	while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:vw:"))) { +	while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) {  		switch (opt) {  			break; case '!': insecure = true;  			break; case 'a': sasl = true; self.plain = optarg; @@ -15,6 +15,7 @@   */  #include <err.h> +#include <getopt.h>  #include <stdbool.h>  #include <stdint.h>  #include <string.h> @@ -139,6 +140,12 @@ void edit(size_t id, enum Edit op, wchar_t ch);  char *editHead(void);  char *editTail(void); +FILE *configOpen(const char *path, const char *mode); +int getopt_config( +	int argc, char *const *argv, +	const char *optstring, const struct option *longopts, int *longindex +); +  static inline enum Color hash(const char *str) {  	if (*str == '~') str++;  	uint32_t hash = 0; diff --git a/config.c b/config.c new file mode 100644 index 0000000..b3e42f9 --- /dev/null +++ b/config.c @@ -0,0 +1,178 @@ +/* Copyright (C) 2019  C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <errno.h> +#include <getopt.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "chat.h" + +#define CONFIG_DIR "catgirl" + +FILE *configOpen(const char *path, const char *mode) { +	if (path[0] == '/' || path[0] == '.') goto local; + +	const char *home = getenv("HOME"); +	const char *configHome = getenv("XDG_CONFIG_HOME"); +	const char *configDirs = getenv("XDG_CONFIG_DIRS"); + +	char buf[PATH_MAX]; +	if (configHome) { +		snprintf(buf, sizeof(buf), "%s/" CONFIG_DIR "/%s", configHome, path); +	} else { +		if (!home) goto local; +		snprintf(buf, sizeof(buf), "%s/.config/" CONFIG_DIR "/%s", home, path); +	} +	FILE *file = fopen(buf, mode); +	if (file) return file; +	if (errno != ENOENT) return NULL; + +	if (!configDirs) configDirs = "/etc/xdg"; +	while (*configDirs) { +		size_t len = strcspn(configDirs, ":"); +		snprintf( +			buf, sizeof(buf), "%.*s/" CONFIG_DIR "/%s", +			(int)len, configDirs, path +		); +		file = fopen(buf, mode); +		if (file) return file; +		if (errno != ENOENT) return NULL; +		configDirs += len; +		if (*configDirs) configDirs++; +	} + +local: +	return fopen(path, mode); +} + +#define WS "\t " + +static const char *path; +static FILE *file; +static size_t num; +static char *line; +static size_t cap; + +static int clean(int opt) { +	if (file) fclose(file); +	free(line); +	line = NULL; +	cap = 0; +	return opt; +} + +int getopt_config( +	int argc, char *const *argv, +	const char *optstring, const struct option *longopts, int *longindex +) { +	static int opt; +	if (opt >= 0) { +		opt = getopt_long(argc, argv, optstring, longopts, longindex); +	} +	if (opt >= 0) return opt; + +	for (;;) { +		if (!file) { +			if (optind < argc) { +				num = 0; +				path = argv[optind++]; +				file = configOpen(path, "r"); +				if (!file) { +					warn("%s", path); +					return clean('?'); +				} +			} else { +				return clean(-1); +			} +		} + +		for (;;) { +			ssize_t llen = getline(&line, &cap, file); +			if (ferror(file)) { +				warn("%s", path); +				return clean('?'); +			} +			if (llen <= 0) break; +			if (line[llen - 1] == '\n') line[llen - 1] = '\0'; +			num++; + +			char *name = line + strspn(line, WS); +			size_t len = strcspn(name, WS "="); +			if (!name[0] || name[0] == '#') continue; + +			const struct option *option; +			for (option = longopts; option->name; ++option) { +				if (strlen(option->name) != len) continue; +				if (!strncmp(option->name, name, len)) break; +			} +			if (!option->name) { +				warnx( +					"%s:%zu: unrecognized option `%.*s'", +					path, num, (int)len, name +				); +				return clean('?'); +			} + +			char *equal = &name[len] + strspn(&name[len], WS); +			if (*equal && *equal != '=') { +				warnx( +					"%s:%zu: option `%s' missing equals sign", +					path, num, option->name +				); +				return clean('?'); +			} +			if (option->has_arg == no_argument && *equal) { +				warnx( +					"%s:%zu: option `%s' doesn't allow an argument", +					path, num, option->name +				); +				return clean('?'); +			} +			if (option->has_arg == required_argument && !*equal) { +				warnx( +					"%s:%zu: option `%s' requires an argument", +					path, num, option->name +				); +				return clean(':'); +			} + +			optarg = NULL; +			if (*equal) { +				char *arg = &equal[1] + strspn(&equal[1], WS); +				optarg = strdup(arg); +				if (!optarg) { +					warn("getopt_config"); +					return clean('?'); +				} +			} + +			if (longindex) *longindex = option - longopts; +			if (option->flag) { +				*option->flag = option->val; +				return 0; +			} else { +				return option->val; +			} +		} + +		fclose(file); +		file = NULL; +	} +}  | 
