diff --git a/Makefile b/Makefile index 05f8bb8..213ecb5 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/catgirl.1 b/catgirl.1 index fb031c2..91c4ff4 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -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 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 diff --git a/chat.c b/chat.c index 1ad2833..c67d8a9 100644 --- a/chat.c +++ b/chat.c @@ -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; diff --git a/chat.h b/chat.h index 9317843..57d4ba6 100644 --- a/chat.h +++ b/chat.h @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -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 + * + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include + +#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; + } +}