From 2ce2f493e1a8af2ea59922439e0b52725018596b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 31 Jan 2020 18:11:30 -0500 Subject: Add simple manual page --- catgirl.1 | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 catgirl.1 (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 new file mode 100644 index 0000000..ccef5ee --- /dev/null +++ b/catgirl.1 @@ -0,0 +1,59 @@ +.Dd January 31, 2020 +.Dt CATGIRL 1 +.Os +. +.Sh NAME +.Nm catgirl +.Nd IRC client +. +.Sh SYNOPSIS +.Nm +.Op Fl h Ar host +.Op Fl j Ar join +.Op Fl n Ar nick +.Op Fl p Ar port +.Op Fl r Ar real +.Op Fl u Ar user +.Op Fl w Ar pass +. +.Sh DESCRIPTION +The +.Nm +program is a curses +TLS-only IRC client. +. +.Pp +The arguments are as follows: +.Bl -tag -width Ds +.It Fl h Ar host +Connect to +.Ar host . +. +.It Fl j Ar join +Join the comma-separated list of channels +.Ar join . +. +.It Fl n Ar nick +Set nickname to +.Ar nick . +The default nickname is the user's name. +. +.It Fl p Ar port +Connect to +.Ar port . +The default port is 6697. +. +.It Fl r Ar real +Set realname to +.Ar real . +The default realname is the same as the nickname. +. +.It Fl u Ar user +Set username to +.Ar user . +The default username is the same as the nickname. +. +.It Fl w Ar pass +Log in with the server password +.Ar pass . +.El -- cgit 1.4.1-2-gfad0 From f76145645e6e183c53c5601294c985246c00fa92 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 01:17:15 -0500 Subject: Add more login options to the manual page --- catgirl.1 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index ccef5ee..158466b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd January 31, 2020 +.Dd February 1, 2020 .Dt CATGIRL 1 .Os . @@ -8,8 +8,12 @@ . .Sh SYNOPSIS .Nm +.Op Fl e +.Op Fl a Ar auth +.Op Fl c Ar cert .Op Fl h Ar host .Op Fl j Ar join +.Op Fl k Ar priv .Op Fl n Ar nick .Op Fl p Ar port .Op Fl r Ar real @@ -25,6 +29,33 @@ TLS-only IRC client. .Pp The arguments are as follows: .Bl -tag -width Ds +.It Fl a Ar user Ns : Ns Ar pass +Authenticate as +.Ar user +with +.Ar pass +using SASL PLAIN. +Since this requires the account password +in plain text, +it is recommended to use SASL EXTERNAL instead with +.Fl e . +. +.It Fl c Ar path +Load the TLS client certificate from +.Ar path . +If the private key is in a separate file, +it is loaded with +.Fl k . +With +.Fl e , +authenticate using SASL EXTERNAL. +. +.It Fl e +Authenticate using SASL EXTERNAL, +also known as CertFP. +The TLS client certificate is loaded with +.Fl c . +. .It Fl h Ar host Connect to .Ar host . @@ -33,6 +64,10 @@ Connect to Join the comma-separated list of channels .Ar join . . +.It Fl k Ar path +Load the TLS client private key from +.Ar path . +. .It Fl n Ar nick Set nickname to .Ar nick . -- cgit 1.4.1-2-gfad0 From 2b3a8bfb9c022269307feed01419c903ba754508 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 02:26:35 -0500 Subject: Add -v flag --- catgirl.1 | 9 ++++++++- chat.c | 7 ++++++- chat.h | 3 ++- irc.c | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 158466b..e9bb40e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -8,7 +8,7 @@ . .Sh SYNOPSIS .Nm -.Op Fl e +.Op Fl ev .Op Fl a Ar auth .Op Fl c Ar cert .Op Fl h Ar host @@ -88,6 +88,13 @@ Set username to .Ar user . The default username is the same as the nickname. . +.It Fl v +Log raw IRC messages to the +.Sy +window +as well as standard error +if it is not a terminal. +. .It Fl w Ar pass Log in with the server password .Ar pass . diff --git a/chat.c b/chat.c index 89579c0..5796085 100644 --- a/chat.c +++ b/chat.c @@ -39,7 +39,7 @@ int main(int argc, char *argv[]) { const char *real = NULL; int opt; - while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:w:"))) { + while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:vw:"))) { switch (opt) { break; case '!': insecure = true; break; case 'a': sasl = true; self.plain = optarg; @@ -52,6 +52,7 @@ int main(int argc, char *argv[]) { break; case 'p': port = optarg; break; case 'r': real = optarg; break; case 'u': user = optarg; + break; case 'v': self.debug = true; break; case 'w': pass = optarg; } } @@ -70,4 +71,8 @@ int main(int argc, char *argv[]) { ircFormat("CAP LS\r\n"); ircFormat("NICK :%s\r\n", nick); ircFormat("USER %s 0 * :%s\r\n", user, real); + + for (;;) { + ircRecv(); + } } diff --git a/chat.h b/chat.h index bb8929b..4dd4732 100644 --- a/chat.h +++ b/chat.h @@ -33,10 +33,11 @@ enum Cap { }; extern struct Self { + bool debug; + const char *join; enum Cap caps; char *plain; char *nick; - const char *join; } self; #define ENUM_TAG \ diff --git a/irc.c b/irc.c index c1c0e7a..cf8aab7 100644 --- a/irc.c +++ b/irc.c @@ -101,6 +101,18 @@ int ircConnect(const char *host, const char *port) { return sock; } +static void debug(char dir, const char *line) { + if (!self.debug) return; + size_t len = strcspn(line, "\r\n"); + /*uiFormat( + Debug, Cold, NULL, "\3%02d%c%c\3 %.*s", + Gray, dir, dir, (int)len, line + );*/ + if (!isatty(STDERR_FILENO)) { + fprintf(stderr, "%c%c %.*s\n", dir, dir, (int)len, line); + } +} + void ircSend(const char *ptr, size_t len) { assert(client); while (len) { @@ -119,6 +131,7 @@ void ircFormat(const char *format, ...) { int len = vsnprintf(buf, sizeof(buf), format, ap); va_end(ap); assert((size_t)len < sizeof(buf)); + debug('<', buf); ircSend(buf, len); } @@ -196,6 +209,7 @@ void ircRecv(void) { crlf = memmem(line, &buf[len] - line, "\r\n", 2); if (!crlf) break; *crlf = '\0'; + debug('>', line); handle(parse(line)); line = crlf + 2; } -- cgit 1.4.1-2-gfad0 From f4868fc90642928358fae0d8d245a4fc5db7d12e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:22:52 -0500 Subject: Document commands in manual --- catgirl.1 | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index e9bb40e..e6d8efa 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 1, 2020 +.Dd February 5, 2020 .Dt CATGIRL 1 .Os . @@ -99,3 +99,22 @@ if it is not a terminal. Log in with the server password .Ar pass . .El +. +.Sh COMMANDS +.Ss Chat Commands +.Bl -tag -width Ds +.It Ic /me Op Ar action +Send an action message. +.It Ic /notice Ar message +Send a notice. +.It Ic /quit Op Ar message +Quit IRC. +.It Ic /quote Ar command +Send a raw IRC command. +.El +. +.Ss UI Commands +.Bl -tag -width Ds +.It Ic /window Ar num , Ic / Ns Ar num +Switch to window by number. +.El -- cgit 1.4.1-2-gfad0 From 6ca54617ce1fe0ac4dbd8094e13b38a0aa375200 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:25:34 -0500 Subject: Add /window name variant --- catgirl.1 | 2 ++ command.c | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index e6d8efa..fb031c2 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -115,6 +115,8 @@ Send a raw IRC command. . .Ss UI Commands .Bl -tag -width Ds +.It Ic /window Ar name +Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num Switch to window by number. .El diff --git a/command.c b/command.c index f9362dc..e4f035f 100644 --- a/command.c +++ b/command.c @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +#include #include #include @@ -64,8 +65,13 @@ static void commandQuit(size_t id, char *params) { } static void commandWindow(size_t id, char *params) { - (void)id; - if (params) uiShowNum(strtoul(params, NULL, 10)); + if (!params) return; + if (isdigit(params[0])) { + uiShowNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiShowID(id); + } } static const struct Handler { -- cgit 1.4.1-2-gfad0 From 27eaddb6b9524b43448f4e5c88ac74bbe8fdb3a5 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:49:56 -0500 Subject: Use getopt_config to load options I'm really getting a lot of use out of this config.c huh. --- Makefile | 1 + catgirl.1 | 41 ++++++++++----- chat.c | 20 ++++++- chat.h | 7 +++ config.c | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 config.c (limited to 'catgirl.1') 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; + } +} -- cgit 1.4.1-2-gfad0 From 9a585188c546ab65633707c3a3e17dbef1d8e3dc Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 01:05:09 -0500 Subject: Add /join command --- catgirl.1 | 4 +++- command.c | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 91c4ff4..bf6ccc7 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 5, 2020 +.Dd February 6, 2020 .Dt CATGIRL 1 .Os . @@ -120,6 +120,8 @@ Log in with the server password .Sh COMMANDS .Ss Chat Commands .Bl -tag -width Ds +.It Ic /join Ar channel +Join a channel. .It Ic /me Op Ar action Send an action message. .It Ic /notice Ar message diff --git a/command.c b/command.c index e4f035f..3215322 100644 --- a/command.c +++ b/command.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "chat.h" @@ -59,6 +60,10 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandJoin(size_t id, char *params) { + ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); +} + static void commandQuit(size_t id, char *params) { (void)id; set(&self.quit, (params ? params : "Goodbye")); @@ -78,6 +83,7 @@ static const struct Handler { const char *cmd; Command *fn; } Commands[] = { + { "/join", commandJoin }, { "/me", commandMe }, { "/notice", commandNotice }, { "/quit", commandQuit }, -- cgit 1.4.1-2-gfad0 From 306e2b5c5b34ec3349a4bba3fe0e2ec9d427ad8a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 02:56:55 -0500 Subject: Flesh out trailing manual sections --- catgirl.1 | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index bf6ccc7..6b433e9 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -24,8 +24,8 @@ .Sh DESCRIPTION The .Nm -program is a curses -TLS-only IRC client. +program is a TLS-only +curses IRC client. . .Pp Options can be loaded from files @@ -139,3 +139,143 @@ Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num Switch to window by number. .El +. +.Sh FILES +.Bl -tag -width Ds +.It Pa $XDG_CONFIG_DIRS/catgirl +Configuration files are search for first in +.Ev $XDG_CONFIG_HOME , +usually +.Pa ~/.config , +followed by the colon-separated list of paths +.Ev $XDG_CONFIG_DIRS , +usually +.Pa /etc/xdg . +.It Pa ~/.config/catgirl +The most likely location of configuration files. +.El +. +.Sh EXAMPLES +Command line: +.Bd -literal -offset indent +catgirl -h chat.freenode.net -j '#ascii.town' +.Ed +.Pp +Configuration file: +.Bd -literal -offset indent +host = chat.freenode.net +join = #ascii.town +.Ed +. +.Sh STANDARDS +.Bl -item +.It +.Rs +.%A Waldo Bastian +.%A Ryan Lortie +.%A Lennart Poettering +.%T XDG Base Directory Specification +.%D November 24, 2010 +.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +.Re +. +.It +.Rs +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov +.%A James Wheare +.%T IRCv3.2 server-time Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/server-time-3.2 +.Re +. +.It +.Rs +.%A Lee Hardy +.%A Perry Lorier +.%A Kevin L. Mitchell +.%A William Pitcock +.%T IRCv3.1 Client Capability Negotiation +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/core/capability-negotiation-3.1.html +.Re +. +.It +.Rs +.%A S. Josefsson +.%T The Base16, Base32, and Base64 Data Encodings +.%I IETF +.%N RFC 4648 +.%D October 2006 +.%U https://tools.ietf.org/html/rfc4648 +.Re +. +.It +.Rs +.%A C. Kalt +.%T Internet Relay Chat: Client Protocol +.%I IETF +.%N RFC 2812 +.%D April 2000 +.%U https://tools.ietf.org/html/rfc2812 +.Re +. +.It +.Rs +.%A Mantas Mikul\[u0117]nas +.%T IRCv3.2 userhost-in-names Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/userhost-in-names-3.2 +.Re +. +.It +.Rs +.%A Daniel Oaks +.%T IRC Formatting +.%I ircdocs +.%U https://modern.ircdocs.horse/formatting.html +.Re +. +.It +.Rs +.%A William Pitcock +.%A Jilles Tjoelker +.%T IRCv3.1 SASL Authentication +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/sasl-3.1.html +.Re +. +.It +.Rs +.%A Alexey Sokolov +.%A St\('ephan Kochen +.%A Kyle Fuller +.%A Kiyoshi Aman +.%A James Wheare +.%T IRCv3 Message Tags +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/message-tags +.Re +. +.It +.Rs +.%A K. Zeilenga, Ed. +.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism +.%I IETF +.%N RFC 4616 +.%D August 2006 +.%U https://tools.ietf.org/html/rfc4616 +.Re +.El +. +.Sh AUTHORS +.An June Bug Aq Mt june@causal.agency +. +.Sh BUGS +Send mail to +.Aq Mt june@causal.agency +or join +.Li #ascii.town +on +.Li chat.freenode.net . -- cgit 1.4.1-2-gfad0 From 86ee56ec4546b7be4b9f1e683a11135bff9fe787 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 02:05:18 -0500 Subject: Document key bindings in manual --- catgirl.1 | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 6b433e9..991f5b1 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 6, 2020 +.Dd February 7, 2020 .Dt CATGIRL 1 .Os . @@ -140,6 +140,27 @@ Switch to window by name. Switch to window by number. .El . +.Sh KEY BINDINGS +.Ss Line Editing +.Bl -tag -width Ds -compact +.It Ic C-a +Move to beginning of line. +.It Ic C-e +Move to end of line. +.It Ic C-u +Delete line. +.El +. +.Ss Window Keys +.Bl -tag -width Ds -compact +.It Ic C-l +Redraw the UI. +.It Ic M-m +Insert a blank line in the window. +.It Ic M- Ns Ar n +Switch to window by number 0\(en9. +.El +. .Sh FILES .Bl -tag -width Ds .It Pa $XDG_CONFIG_DIRS/catgirl -- cgit 1.4.1-2-gfad0 From 4343f35f9c837444ce6edfede212b89dea422544 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 02:46:40 -0500 Subject: Add key bindings for IRC formatting --- catgirl.1 | 41 +++++++++++++++++++++++++++++++++++++++++ ui.c | 53 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 77 insertions(+), 17 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 991f5b1..e746150 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -161,6 +161,47 @@ Insert a blank line in the window. Switch to window by number 0\(en9. .El . +.Ss IRC Formatting +.Bl -tag -width Ds -compact +.It Ic C-z b +Toggle bold. +.It Ic C-z c +Set or reset color. +.It Ic C-z i +Toggle italics. +.It Ic C-z o +Reset formatting. +.It Ic C-z r +Toggle reverse color. +.It Ic C-z u +Toggle underline. +.El +. +.Pp +To set colors, follow +.Ic C-z c +by one or two digits for the foreground color, +optionally followed by a comma +and one or two digits for the background color. +To reset color, follow +.Ic C-z c +by a non-digit. +. +.Pp +The color numbers are as follows: +.Pp +.Bl -column "99" "orange (dark yellow)" "15" "pink (light magenta)" +.It \ 0 Ta white Ta \ 8 Ta yellow +.It \ 1 Ta black Ta \ 9 Ta light green +.It \ 2 Ta blue Ta 10 Ta cyan +.It \ 3 Ta green Ta 11 Ta light cyan +.It \ 4 Ta red Ta 12 Ta light blue +.It \ 5 Ta brown (dark red) Ta 13 Ta pink (light magenta) +.It \ 6 Ta magenta Ta 14 Ta gray +.It \ 7 Ta orange (dark yellow) Ta 15 Ta light gray +.It 99 Ta default +.El +. .Sh FILES .Bl -tag -width Ds .It Pa $XDG_CONFIG_DIRS/catgirl diff --git a/ui.c b/ui.c index f73020a..d4ef241 100644 --- a/ui.c +++ b/ui.c @@ -261,14 +261,16 @@ static short mapColor(enum Color color) { } } +enum { B = '\2', C = '\3', O = '\17', R = '\26', I = '\35', U = '\37' }; + static void styleParse(struct Style *style, const char **str, size_t *len) { switch (**str) { - break; case '\2': (*str)++; style->attr ^= A_BOLD; - break; case '\17': (*str)++; *style = Reset; - break; case '\26': (*str)++; style->attr ^= A_REVERSE; - break; case '\35': (*str)++; style->attr ^= A_ITALIC; - break; case '\37': (*str)++; style->attr ^= A_UNDERLINE; - break; case '\3': { + break; case B: (*str)++; style->attr ^= A_BOLD; + break; case O: (*str)++; *style = Reset; + break; case R: (*str)++; style->attr ^= A_REVERSE; + break; case I: (*str)++; style->attr ^= A_ITALIC; + break; case U: (*str)++; style->attr ^= A_UNDERLINE; + break; case C: { (*str)++; if (!isdigit(**str)) { style->fg = Default; @@ -283,7 +285,7 @@ static void styleParse(struct Style *style, const char **str, size_t *len) { if (isdigit(**str)) style->bg = style->bg * 10 + *(*str)++ - '0'; } } - *len = strcspn(*str, "\2\3\17\26\35\37"); + *len = strcspn(*str, (const char[]) { B, C, O, R, I, U, '\0' }); } static void statusAdd(const char *str) { @@ -456,12 +458,12 @@ static void inputAdd(struct Style *style, const char *str) { styleParse(style, &str, &len); wattr_set(input, A_BOLD | A_REVERSE, 0, NULL); switch (*code) { - break; case '\2': waddch(input, 'B'); - break; case '\3': waddch(input, 'C'); - break; case '\17': waddch(input, 'O'); - break; case '\26': waddch(input, 'R'); - break; case '\35': waddch(input, 'I'); - break; case '\37': waddch(input, 'U'); + break; case B: waddch(input, 'B'); + break; case C: waddch(input, 'C'); + break; case O: waddch(input, 'O'); + break; case R: waddch(input, 'R'); + break; case I: waddch(input, 'I'); + break; case U: waddch(input, 'U'); } if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); wattr_set( @@ -574,7 +576,7 @@ static void keyMeta(wchar_t ch) { static void keyCtrl(wchar_t ch) { size_t id = windows.active->id; - switch (ch) { + switch (ch ^ L'@') { break; case L'?': edit(id, EditErase, 0); break; case L'A': edit(id, EditHome, 0); break; case L'E': edit(id, EditEnd, 0); @@ -585,10 +587,22 @@ static void keyCtrl(wchar_t ch) { } } +static void keyStyle(wchar_t ch) { + size_t id = windows.active->id; + switch (iswcntrl(ch) ? ch ^ L'@' : towupper(ch)) { + break; case L'B': edit(id, EditInsert, B); + break; case L'C': edit(id, EditInsert, C); + break; case L'I': edit(id, EditInsert, I); + break; case L'O': edit(id, EditInsert, O); + break; case L'R': edit(id, EditInsert, R); + break; case L'U': edit(id, EditInsert, U); + } +} + void uiRead(void) { int ret; wint_t ch; - static bool meta; + static bool meta, style; while (ERR != (ret = wget_wch(input, &ch))) { if (ret == KEY_CODE_YES) { keyCode(ch); @@ -597,12 +611,17 @@ void uiRead(void) { continue; } else if (meta) { keyMeta(ch); + } else if (ch == (L'Z' ^ L'@')) { + style = true; + continue; + } else if (style) { + keyStyle(ch); } else if (iswcntrl(ch)) { - keyCtrl(ch ^ L'@'); + keyCtrl(ch); } else { edit(windows.active->id, EditInsert, ch); } - meta = false; + meta = style = false; } inputUpdate(); } -- cgit 1.4.1-2-gfad0 From b200194206a943bf89dde619288eb7fbe3fee1a2 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 21:53:50 -0500 Subject: Use complete to abbreviate commands --- catgirl.1 | 6 ++++++ command.c | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index e746150..76f527e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -118,6 +118,12 @@ Log in with the server password .El . .Sh COMMANDS +Any unique prefix can be used to abbreviate a command. +For example, +.Ic /join +can be typed +.Ic /j . +. .Ss Chat Commands .Bl -tag -width Ds .It Ic /join Ar channel diff --git a/command.c b/command.c index 41aacc9..8bd8b28 100644 --- a/command.c +++ b/command.c @@ -125,7 +125,12 @@ void command(size_t id, char *input) { } else if (input[0] == '/' && isdigit(input[1])) { commandWindow(id, &input[1]); } else { - char *cmd = strsep(&input, " "); + const char *cmd = strsep(&input, " "); + const char *unique = complete(None, cmd); + if (unique && !complete(None, cmd)) { + cmd = unique; + completeReject(); + } const struct Handler *handler = bsearch( cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); -- cgit 1.4.1-2-gfad0 From 55173ef29777959b0b761097499e3eef397de609 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 00:02:10 -0500 Subject: Add /nick --- catgirl.1 | 2 ++ command.c | 7 +++++++ 2 files changed, 9 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 76f527e..5b9b1a5 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -130,6 +130,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /nick Ar nick +Change nicknames. .It Ic /notice Ar message Send a notice. .It Ic /quit Op Ar message diff --git a/command.c b/command.c index 8bd8b28..7416f81 100644 --- a/command.c +++ b/command.c @@ -69,6 +69,12 @@ static void commandQuit(size_t id, char *params) { set(&self.quit, (params ? params : "Goodbye")); } +static void commandNick(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("NICK :%s\r\n", params); +} + static void commandWindow(size_t id, char *params) { if (!params) return; if (isdigit(params[0])) { @@ -85,6 +91,7 @@ static const struct Handler { } Commands[] = { { "/join", commandJoin }, { "/me", commandMe }, + { "/nick", commandNick }, { "/notice", commandNotice }, { "/quit", commandQuit }, { "/quote", commandQuote }, -- cgit 1.4.1-2-gfad0 From f5783d15c6a640d553e4e356c4ba10895ad602a3 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 01:25:07 -0500 Subject: Add /part --- catgirl.1 | 4 +++- command.c | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 5b9b1a5..4dc002e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 7, 2020 +.Dd February 8, 2020 .Dt CATGIRL 1 .Os . @@ -134,6 +134,8 @@ Send an action message. Change nicknames. .It Ic /notice Ar message Send a notice. +.It Ic /part Op Ar message +Leave the channel. .It Ic /quit Op Ar message Quit IRC. .It Ic /quote Ar command diff --git a/command.c b/command.c index 7416f81..dfe4850 100644 --- a/command.c +++ b/command.c @@ -64,6 +64,14 @@ static void commandJoin(size_t id, char *params) { ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); } +static void commandPart(size_t id, char *params) { + if (params) { + ircFormat("PART %s :%s\r\n", idNames[id], params); + } else { + ircFormat("PART %s\r\n", idNames[id]); + } +} + static void commandQuit(size_t id, char *params) { (void)id; set(&self.quit, (params ? params : "Goodbye")); @@ -93,6 +101,7 @@ static const struct Handler { { "/me", commandMe }, { "/nick", commandNick }, { "/notice", commandNotice }, + { "/part", commandPart }, { "/quit", commandQuit }, { "/quote", commandQuote }, { "/window", commandWindow }, -- cgit 1.4.1-2-gfad0 From 5c10fe0d414b655ae2cbf14c3db9216b438c5193 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 01:34:55 -0500 Subject: Add /query --- catgirl.1 | 2 ++ command.c | 9 +++++++++ 2 files changed, 11 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 4dc002e..0702f58 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -136,6 +136,8 @@ Change nicknames. Send a notice. .It Ic /part Op Ar message Leave the channel. +.It Ic /query Ar nick +Start a private conversation. .It Ic /quit Op Ar message Quit IRC. .It Ic /quote Ar command diff --git a/command.c b/command.c index dfe4850..9047e95 100644 --- a/command.c +++ b/command.c @@ -83,6 +83,13 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandQuery(size_t id, char *params) { + if (!params) return; + size_t query = idFor(params); + idColors[query] = completeColor(id, params); + uiShowID(query); +} + static void commandWindow(size_t id, char *params) { if (!params) return; if (isdigit(params[0])) { @@ -102,6 +109,7 @@ static const struct Handler { { "/nick", commandNick }, { "/notice", commandNotice }, { "/part", commandPart }, + { "/query", commandQuery }, { "/quit", commandQuit }, { "/quote", commandQuote }, { "/window", commandWindow }, @@ -151,6 +159,7 @@ void command(size_t id, char *input) { cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); if (handler) { + if (input && !input[0]) input = NULL; handler->fn(id, input); } else { uiFormat(id, Hot, NULL, "No such command %s", cmd); -- cgit 1.4.1-2-gfad0 From 943502ea82b3965b4f652146ca03262ac6390f83 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 02:26:00 -0500 Subject: Add /close --- catgirl.1 | 2 ++ chat.h | 2 ++ command.c | 12 ++++++++++++ ui.c | 28 ++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 0702f58..9314e7a 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -146,6 +146,8 @@ Send a raw IRC command. . .Ss UI Commands .Bl -tag -width Ds +.It Ic /close Op Ar name | num +Close the named, numbered or current window. .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/chat.h b/chat.h index 081370e..9daa38c 100644 --- a/chat.h +++ b/chat.h @@ -128,6 +128,8 @@ void uiHide(void); void uiDraw(void); void uiShowID(size_t id); void uiShowNum(size_t num); +void uiCloseID(size_t id); +void uiCloseNum(size_t id); void uiRead(void); void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); void uiFormat( diff --git a/command.c b/command.c index 9047e95..e33c57e 100644 --- a/command.c +++ b/command.c @@ -100,10 +100,22 @@ static void commandWindow(size_t id, char *params) { } } +static void commandClose(size_t id, char *params) { + if (!params) { + uiCloseID(id); + } else if (isdigit(params[0])) { + uiCloseNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiCloseID(id); + } +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { + { "/close", commandClose }, { "/join", commandJoin }, { "/me", commandMe }, { "/nick", commandNick }, diff --git a/ui.c b/ui.c index 6d1338b..c738617 100644 --- a/ui.c +++ b/ui.c @@ -573,6 +573,34 @@ void uiShowNum(size_t num) { windowShow(window); } +static void windowClose(struct Window *window) { + if (window->id == Network) return; + if (windows.active == window) { + windowShow(window->prev ? window->prev : window->next); + } + if (windows.other == window) windows.other = NULL; + windowRemove(window); + for (size_t i = 0; i < BufferCap; ++i) { + free(window->buffer.lines[i]); + } + delwin(window->pad); + free(window); + statusUpdate(); +} + +void uiCloseID(size_t id) { + windowClose(windowFor(id)); +} + +void uiCloseNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowClose(window); +} + static void keyCode(int code) { size_t id = windows.active->id; switch (code) { -- cgit 1.4.1-2-gfad0 From 2cacf15314be31b33a61007ba6e063ced96c3d41 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 02:33:41 -0500 Subject: Add /debug --- catgirl.1 | 4 ++++ command.c | 11 +++++++++++ 2 files changed, 15 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 9314e7a..3f8131f 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -148,6 +148,10 @@ Send a raw IRC command. .Bl -tag -width Ds .It Ic /close Op Ar name | num Close the named, numbered or current window. +.It Ic /debug +Toggle logging in the +.Sy +window. .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/command.c b/command.c index e33c57e..1d1c756 100644 --- a/command.c +++ b/command.c @@ -23,6 +23,16 @@ typedef void Command(size_t id, char *params); +static void commandDebug(size_t id, char *params) { + (void)id; + (void)params; + self.debug ^= true; + uiFormat( + Debug, Warm, NULL, + "\3%dDebug is %s", Gray, (self.debug ? "on" : "off") + ); +} + static void commandQuote(size_t id, char *params) { (void)id; if (params) ircFormat("%s\r\n", params); @@ -116,6 +126,7 @@ static const struct Handler { Command *fn; } Commands[] = { { "/close", commandClose }, + { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, { "/nick", commandNick }, -- cgit 1.4.1-2-gfad0 From ff6424a87ce22586c3e2fe1ab57ed3407bef18ca Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 03:19:56 -0500 Subject: Add /names --- catgirl.1 | 2 ++ command.c | 7 +++++++ 2 files changed, 9 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 3f8131f..ccf981b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -130,6 +130,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /names +List users in the channel. .It Ic /nick Ar nick Change nicknames. .It Ic /notice Ar message diff --git a/command.c b/command.c index 9879dbe..a6434bf 100644 --- a/command.c +++ b/command.c @@ -101,6 +101,12 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandNames(size_t id, char *params) { + (void)params; + ircFormat("NAMES :%s\r\n", idNames[id]); + replies.names++; +} + static void commandQuery(size_t id, char *params) { if (!params) return; size_t query = idFor(params); @@ -137,6 +143,7 @@ static const struct Handler { { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, + { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, { "/part", commandPart }, -- cgit 1.4.1-2-gfad0 From b98c7d68630a7af37f61a52a555e1aaed1c2e7af Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 03:25:50 -0500 Subject: Add /topic --- catgirl.1 | 2 ++ command.c | 10 ++++++++++ 2 files changed, 12 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index ccf981b..5394d33 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -144,6 +144,8 @@ Start a private conversation. Quit IRC. .It Ic /quote Ar command Send a raw IRC command. +.It Ic /topic Op Ar topic +Show or set the topic of the channel. .El . .Ss UI Commands diff --git a/command.c b/command.c index a6434bf..eaabc9c 100644 --- a/command.c +++ b/command.c @@ -101,6 +101,15 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandTopic(size_t id, char *params) { + if (params) { + ircFormat("TOPIC %s :%s\r\n", idNames[id], params); + } else { + ircFormat("TOPIC %s\r\n", idNames[id]); + replies.topic++; + } +} + static void commandNames(size_t id, char *params) { (void)params; ircFormat("NAMES :%s\r\n", idNames[id]); @@ -150,6 +159,7 @@ static const struct Handler { { "/query", commandQuery }, { "/quit", commandQuit }, { "/quote", commandQuote }, + { "/topic", commandTopic }, { "/window", commandWindow }, }; -- cgit 1.4.1-2-gfad0 From f502260dd0aa73b09bfbb7289b50a67592866166 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 18:29:01 -0500 Subject: Scan messages for URLs --- Makefile | 1 + catgirl.1 | 9 ++++++ chat.h | 4 +++ command.c | 11 ++++++++ handle.c | 15 ++++++++-- url.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 url.c (limited to 'catgirl.1') diff --git a/Makefile b/Makefile index 48aba7b..bcbb0d8 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ OBJS += edit.o OBJS += handle.o OBJS += irc.o OBJS += ui.o +OBJS += url.o dev: tags all diff --git a/catgirl.1 b/catgirl.1 index 5394d33..f489d07 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -156,6 +156,15 @@ Close the named, numbered or current window. Toggle logging in the .Sy window. +.It Ic /open Op Ar count +Open each of +.Ar count +most recent URLs. +.It Ic /open Ar nick | substring +Open the most recent URL from +.Ar nick +or matching +.Ar substring . .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/chat.h b/chat.h index 909527e..583107a 100644 --- a/chat.h +++ b/chat.h @@ -169,6 +169,10 @@ void completeClear(size_t id); size_t completeID(const char *str); enum Color completeColor(size_t id, const char *str); +void urlScan(size_t id, const char *nick, const char *mesg); +void urlOpenCount(size_t id, size_t count); +void urlOpenMatch(size_t id, const char *str); + FILE *configOpen(const char *path, const char *mode); int getopt_config( int argc, char *const *argv, diff --git a/command.c b/command.c index eaabc9c..4100928 100644 --- a/command.c +++ b/command.c @@ -144,6 +144,16 @@ static void commandClose(size_t id, char *params) { } } +static void commandOpen(size_t id, char *params) { + if (!params) { + urlOpenCount(id, 1); + } else if (isdigit(params[0])) { + urlOpenCount(id, strtoul(params, NULL, 10)); + } else { + urlOpenMatch(id, params); + } +} + static const struct Handler { const char *cmd; Command *fn; @@ -155,6 +165,7 @@ static const struct Handler { { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, + { "/open", commandOpen }, { "/part", commandPart }, { "/query", commandQuery }, { "/quit", commandQuit }, diff --git a/handle.c b/handle.c index 0780767..f919fcb 100644 --- a/handle.c +++ b/handle.c @@ -193,6 +193,7 @@ static void handleReplyISupport(struct Message *msg) { static void handleReplyMOTD(struct Message *msg) { require(msg, false, 2); char *line = msg->params[1]; + urlScan(Network, msg->nick, line); if (!strncmp(line, "- ", 2)) { uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); } else { @@ -227,6 +228,7 @@ static void handlePart(struct Message *msg) { completeClear(id); } completeRemove(id, msg->nick); + urlScan(id, msg->nick, msg->params[1]); uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tleaves \3%02d%s\3%s%s", @@ -241,6 +243,7 @@ static void handleKick(struct Message *msg) { size_t id = idFor(msg->params[0]); bool kicked = self.nick && !strcmp(msg->params[1], self.nick); completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[2]); uiFormat( id, (kicked ? Hot : Cold), tagTime(msg), "%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s", @@ -275,6 +278,7 @@ static void handleQuit(struct Message *msg) { require(msg, true, 0); size_t id; while (None != (id = completeID(msg->nick))) { + urlScan(id, msg->nick, msg->params[0]); uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tleaves%s%s", @@ -333,8 +337,10 @@ static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); if (!replies.topic) return; replies.topic--; + size_t id = idFor(msg->params[1]); + urlScan(id, NULL, msg->params[2]); uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + id, Cold, tagTime(msg), "The sign in \3%02d%s\3 reads: %s", hash(msg->params[1]), msg->params[1], msg->params[2] ); @@ -342,16 +348,18 @@ static void handleReplyTopic(struct Message *msg) { static void handleTopic(struct Message *msg) { require(msg, true, 2); + size_t id = idFor(msg->params[0]); if (msg->params[1][0]) { + urlScan(id, msg->nick, msg->params[1]); uiFormat( - idFor(msg->params[0]), Warm, tagTime(msg), + id, Warm, tagTime(msg), "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s", hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], msg->params[1] ); } else { uiFormat( - idFor(msg->params[0]), Warm, tagTime(msg), + id, Warm, tagTime(msg), "\3%02d%s\3\tremoves the sign in \3%02d%s\3", hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] ); @@ -400,6 +408,7 @@ static void handlePrivmsg(struct Message *msg) { bool action = isAction(msg); bool mention = !mine && isMention(msg); if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[1]); if (notice) { uiFormat( id, Warm, tagTime(msg), diff --git a/url.c b/url.c new file mode 100644 index 0000000..7790461 --- /dev/null +++ b/url.c @@ -0,0 +1,96 @@ +/* Copyright (C) 2020 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 "chat.h" + +static const char *Pattern = { + "(" + "cvs|" + "ftp|" + "git|" + "gopher|" + "http|" + "https|" + "irc|" + "ircs|" + "magnet|" + "sftp|" + "ssh|" + "svn|" + "telnet|" + "vnc" + ")" + ":[^[:space:]>\"]+" +}; +static regex_t Regex; + +static void compile(void) { + static bool compiled; + if (compiled) return; + compiled = true; + int error = regcomp(&Regex, Pattern, REG_EXTENDED); + if (!error) return; + char buf[256]; + regerror(error, &Regex, buf, sizeof(buf)); + errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern); +} + +enum { Cap = 32 }; +static struct { + size_t ids[Cap]; + char *nicks[Cap]; + char *urls[Cap]; + size_t len; +} ring; + +static void push(size_t id, const char *nick, const char *url, size_t len) { + size_t i = ring.len++ % Cap; + free(ring.nicks[i]); + free(ring.urls[i]); + ring.ids[i] = id; + ring.nicks[i] = NULL; + if (nick) { + ring.nicks[i] = strdup(nick); + if (!ring.nicks[i]) err(EX_OSERR, "strdup"); + } + ring.urls[i] = strndup(url, len); + if (!ring.urls[i]) err(EX_OSERR, "strndup"); +} + +void urlScan(size_t id, const char *nick, const char *mesg) { + if (!mesg) return; + compile(); + regmatch_t match = {0}; + for (const char *ptr = mesg; *ptr; ptr += match.rm_eo) { + if (regexec(&Regex, ptr, 1, &match, 0)) break; + push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so); + } +} + +void urlOpenCount(size_t id, size_t count) { + // TODO +} + +void urlOpenMatch(size_t id, const char *str) { + // TODO +} -- cgit 1.4.1-2-gfad0 From 2db17e83a914586fd351437ac5323713f1e66478 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 21:21:21 -0500 Subject: Allow overriding the /open utility --- catgirl.1 | 8 ++++++++ chat.c | 4 +++- chat.h | 1 + url.c | 14 ++++++++++---- 4 files changed, 22 insertions(+), 5 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index f489d07..6129b71 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -9,6 +9,7 @@ .Sh SYNOPSIS .Nm .Op Fl ev +.Op Fl O Ar open .Op Fl a Ar auth .Op Fl c Ar cert .Op Fl h Ar host @@ -46,6 +47,13 @@ following their corresponding flags. .Pp The arguments are as follows: .Bl -tag -width Ds +.It Fl O Ar util , Cm open = Ar util +Set the command used by +.Ic /open . +The default is the first available of +.Xr open 1 , +.Xr xdg-open 1 . +. .It Fl a Ar user Ns : Ns Ar pass , Cm sasl-plain = Ar user Ns : Ns Ar pass Authenticate as .Ar user diff --git a/chat.c b/chat.c index c0c2d28..77aa61d 100644 --- a/chat.c +++ b/chat.c @@ -81,9 +81,10 @@ 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 char *Opts = "!O:a:c:eh:j:k:n:p:r:u:vw:"; const struct option LongOpts[] = { { "insecure", no_argument, NULL, '!' }, + { "open", required_argument, NULL, 'O' }, { "sasl-plain", required_argument, NULL, 'a' }, { "cert", required_argument, NULL, 'c' }, { "sasl-external", no_argument, NULL, 'e' }, @@ -103,6 +104,7 @@ int main(int argc, char *argv[]) { while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) { switch (opt) { break; case '!': insecure = true; + break; case 'O': urlOpenUtil = optarg; break; case 'a': sasl = true; self.plain = optarg; break; case 'c': cert = optarg; break; case 'e': sasl = true; diff --git a/chat.h b/chat.h index 583107a..3084359 100644 --- a/chat.h +++ b/chat.h @@ -169,6 +169,7 @@ void completeClear(size_t id); size_t completeID(const char *str); enum Color completeColor(size_t id, const char *str); +extern const char *urlOpenUtil; void urlScan(size_t id, const char *nick, const char *mesg); void urlOpenCount(size_t id, size_t count); void urlOpenMatch(size_t id, const char *str); diff --git a/url.c b/url.c index 1396765..c9c4d5c 100644 --- a/url.c +++ b/url.c @@ -95,7 +95,8 @@ void urlScan(size_t id, const char *nick, const char *mesg) { } } -static const char *OpenBins[] = { "open", "xdg-open" }; +const char *urlOpenUtil; +static const char *OpenUtils[] = { "open", "xdg-open" }; static void urlOpen(const char *url) { pid_t pid = fork(); @@ -105,10 +106,15 @@ static void urlOpen(const char *url) { close(STDIN_FILENO); dup2(procPipe[1], STDOUT_FILENO); dup2(procPipe[1], STDERR_FILENO); - for (size_t i = 0; i < ARRAY_LEN(OpenBins); ++i) { - execlp(OpenBins[i], OpenBins[i], url, NULL); + if (urlOpenUtil) { + execlp(urlOpenUtil, urlOpenUtil, url, NULL); + warn("%s", urlOpenUtil); + _exit(EX_CONFIG); + } + for (size_t i = 0; i < ARRAY_LEN(OpenUtils); ++i) { + execlp(OpenUtils[i], OpenUtils[i], url, NULL); if (errno != ENOENT) { - warn("%s", OpenBins[i]); + warn("%s", OpenUtils[i]); _exit(EX_CONFIG); } } -- cgit 1.4.1-2-gfad0 From 3e6868414811be8902e6973c78ef2010b26a9e08 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 21:44:50 -0500 Subject: Add /copy --- catgirl.1 | 17 ++++++++++++++++- chat.c | 4 +++- chat.h | 2 ++ command.c | 5 +++++ url.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 6129b71..4dabb4f 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -9,6 +9,7 @@ .Sh SYNOPSIS .Nm .Op Fl ev +.Op Fl C Ar copy .Op Fl O Ar open .Op Fl a Ar auth .Op Fl c Ar cert @@ -47,8 +48,17 @@ following their corresponding flags. .Pp The arguments are as follows: .Bl -tag -width Ds +.It Fl C Ar util , Cm copy = Ar util +Set the utility used by +.Ic /copy . +The default is the first available of +.Xr pbcopy 1 , +.Xr wl-copy 1 , +.Xr xclip 1 , +.Xr xsel 1 . +. .It Fl O Ar util , Cm open = Ar util -Set the command used by +Set the utility used by .Ic /open . The default is the first available of .Xr open 1 , @@ -160,6 +170,11 @@ Show or set the topic of the channel. .Bl -tag -width Ds .It Ic /close Op Ar name | num Close the named, numbered or current window. +.It Ic /copy Op Ar nick | substring +Copy the most recent URL from +.Ar nick +or matching +.Ar substring . .It Ic /debug Toggle logging in the .Sy diff --git a/chat.c b/chat.c index 77aa61d..dbad242 100644 --- a/chat.c +++ b/chat.c @@ -81,9 +81,10 @@ int main(int argc, char *argv[]) { const char *user = NULL; const char *real = NULL; - const char *Opts = "!O:a:c:eh:j:k:n:p:r:u:vw:"; + const char *Opts = "!C:O:a:c:eh:j:k:n:p:r:u:vw:"; const struct option LongOpts[] = { { "insecure", no_argument, NULL, '!' }, + { "copy", required_argument, NULL, 'C' }, { "open", required_argument, NULL, 'O' }, { "sasl-plain", required_argument, NULL, 'a' }, { "cert", required_argument, NULL, 'c' }, @@ -104,6 +105,7 @@ int main(int argc, char *argv[]) { while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) { switch (opt) { break; case '!': insecure = true; + break; case 'C': urlCopyUtil = optarg; break; case 'O': urlOpenUtil = optarg; break; case 'a': sasl = true; self.plain = optarg; break; case 'c': cert = optarg; diff --git a/chat.h b/chat.h index 3084359..8bc8e81 100644 --- a/chat.h +++ b/chat.h @@ -170,9 +170,11 @@ size_t completeID(const char *str); enum Color completeColor(size_t id, const char *str); extern const char *urlOpenUtil; +extern const char *urlCopyUtil; void urlScan(size_t id, const char *nick, const char *mesg); void urlOpenCount(size_t id, size_t count); void urlOpenMatch(size_t id, const char *str); +void urlCopyMatch(size_t id, const char *str); FILE *configOpen(const char *path, const char *mode); int getopt_config( diff --git a/command.c b/command.c index 4100928..feb52b7 100644 --- a/command.c +++ b/command.c @@ -154,11 +154,16 @@ static void commandOpen(size_t id, char *params) { } } +static void commandCopy(size_t id, char *params) { + urlCopyMatch(id, params); +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { { "/close", commandClose }, + { "/copy", commandCopy }, { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, diff --git a/url.c b/url.c index c9c4d5c..7ab1e53 100644 --- a/url.c +++ b/url.c @@ -122,6 +122,47 @@ static void urlOpen(const char *url) { _exit(EX_CONFIG); } +const char *urlCopyUtil; +static const char *CopyUtils[] = { "pbcopy", "wl-copy", "xclip", "xsel" }; + +static void urlCopy(const char *url) { + int rw[2]; + int error = pipe(rw); + if (error) err(EX_OSERR, "pipe"); + + ssize_t len = write(rw[1], url, strlen(url)); + if (len < 0) err(EX_IOERR, "write"); + + error = close(rw[1]); + if (error) err(EX_IOERR, "close"); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) { + close(rw[0]); + return; + } + + dup2(rw[0], STDIN_FILENO); + dup2(procPipe[1], STDOUT_FILENO); + dup2(procPipe[1], STDERR_FILENO); + close(rw[0]); + if (urlCopyUtil) { + execlp(urlCopyUtil, urlCopyUtil, NULL); + warn("%s", urlCopyUtil); + _exit(EX_CONFIG); + } + for (size_t i = 0; i < ARRAY_LEN(CopyUtils); ++i) { + execlp(CopyUtils[i], CopyUtils[i], NULL); + if (errno != ENOENT) { + warn("%s", CopyUtils[i]); + _exit(EX_CONFIG); + } + } + warnx("no copy utility found"); + _exit(EX_CONFIG); +} + void urlOpenCount(size_t id, size_t count) { for (size_t i = 1; i <= Cap; ++i) { const struct URL *url = &ring.urls[(ring.len - i) % Cap]; @@ -143,3 +184,19 @@ void urlOpenMatch(size_t id, const char *str) { } } } + +void urlCopyMatch(size_t id, const char *str) { + for (size_t i = 1; i <= Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len - i) % Cap]; + if (!url->url) break; + if (url->id != id) continue; + if ( + !str + || (url->nick && !strcmp(url->nick, str)) + || strstr(url->url, str) + ) { + urlCopy(url->url); + break; + } + } +} -- cgit 1.4.1-2-gfad0 From a212a7ae2c93092068c8a9c483c4575cc65e7491 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 00:53:55 -0500 Subject: Show realname on JOIN if it is different from nick --- catgirl.1 | 10 +++++++++- chat.h | 1 + handle.c | 11 +++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 4dabb4f..fd00105 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 8, 2020 +.Dd February 9, 2020 .Dt CATGIRL 1 .Os . @@ -287,6 +287,14 @@ join = #ascii.town .Bl -item .It .Rs +.%A Kiyoshi Aman +.%T IRCv3.1 extended-join Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/extended-join-3.1 +.Re +. +.It +.Rs .%A Waldo Bastian .%A Ryan Lortie .%A Lennart Poettering diff --git a/chat.h b/chat.h index 8bc8e81..896549e 100644 --- a/chat.h +++ b/chat.h @@ -59,6 +59,7 @@ static inline size_t idFor(const char *name) { } #define ENUM_CAP \ + X("extended-join", CapExtendedJoin) \ X("sasl", CapSASL) \ X("server-time", CapServerTime) \ X("userhost-in-names", CapUserhostInNames) diff --git a/handle.c b/handle.c index cf0e853..0297595 100644 --- a/handle.c +++ b/handle.c @@ -214,10 +214,17 @@ static void handleJoin(struct Message *msg) { uiShowID(id); } completeTouch(id, msg->nick, hash(msg->user)); + if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { + msg->params[2] = NULL; + } uiFormat( id, Cold, tagTime(msg), - "\3%02d%s\3\tarrives in \3%02d%s\3", - hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + "\3%02d%s\3\t%s%s%sarrives in \3%02d%s\3", + hash(msg->user), msg->nick, + (msg->params[2] ? "(" : ""), + (msg->params[2] ? msg->params[2] : ""), + (msg->params[2] ? ") " : ""), + hash(msg->params[0]), msg->params[0] ); } -- cgit 1.4.1-2-gfad0 From e6e2021d480adab9dd35873810f040524d97092a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 03:41:46 -0500 Subject: Add C-b and C-f --- catgirl.1 | 4 ++++ ui.c | 2 ++ 2 files changed, 6 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index fd00105..f68e6c3 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -199,8 +199,12 @@ Switch to window by number. .Bl -tag -width Ds -compact .It Ic C-a Move to beginning of line. +.It Ic C-b +Move left. .It Ic C-e Move to end of line. +.It Ic C-f +Move right. .It Ic C-u Delete line. .El diff --git a/ui.c b/ui.c index c342339..4478478 100644 --- a/ui.c +++ b/ui.c @@ -644,7 +644,9 @@ static void keyCtrl(wchar_t ch) { switch (ch ^ L'@') { break; case L'?': edit(id, EditErase, 0); break; case L'A': edit(id, EditHome, 0); + break; case L'B': edit(id, EditLeft, 0); break; case L'E': edit(id, EditEnd, 0); + break; case L'F': edit(id, EditRight, 0); break; case L'H': edit(id, EditErase, 0); break; case L'I': edit(id, EditComplete, 0); break; case L'J': edit(id, EditEnter, 0); -- cgit 1.4.1-2-gfad0 From d7c96fc81b71b77b30511d6526fe3acaa84c39ee Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 03:56:18 -0500 Subject: Add C-k Also rename all the edit ops to something consistent. --- catgirl.1 | 6 +++++- chat.h | 15 ++++++++------- edit.c | 17 +++++++++-------- ui.c | 29 +++++++++++++++-------------- 4 files changed, 37 insertions(+), 30 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index f68e6c3..a356fe0 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -201,12 +201,16 @@ Switch to window by number. Move to beginning of line. .It Ic C-b Move left. +.It Ic C-d +Delete next character. .It Ic C-e Move to end of line. .It Ic C-f Move right. +.It Ic C-k +Delete to end of line. .It Ic C-u -Delete line. +Delete to beginning of line. .El . .Ss Window Keys diff --git a/chat.h b/chat.h index d6d9e1c..aa1bcc1 100644 --- a/chat.h +++ b/chat.h @@ -145,13 +145,14 @@ void uiFormat( ) __attribute__((format(printf, 4, 5))); enum Edit { - EditHome, - EditEnd, - EditLeft, - EditRight, - EditKill, - EditErase, - EditDelete, + EditHead, + EditTail, + EditPrev, + EditNext, + EditKillPrev, + EditKillNext, + EditDeletePrev, + EditDeleteNext, EditInsert, EditComplete, EditEnter, diff --git a/edit.c b/edit.c index c30e725..7fcff40 100644 --- a/edit.c +++ b/edit.c @@ -133,14 +133,15 @@ static void tabReject(void) { void edit(size_t id, enum Edit op, wchar_t ch) { size_t init = pos; switch (op) { - break; case EditHome: pos = 0; - break; case EditEnd: pos = len; - break; case EditLeft: if (pos) pos--; - break; case EditRight: if (pos < len) pos++; - - break; case EditKill: len = pos = 0; - break; case EditErase: if (pos) delete(--pos, 1); - break; case EditDelete: delete(pos, 1); + break; case EditHead: pos = 0; + break; case EditTail: pos = len; + break; case EditPrev: if (pos) pos--; + break; case EditNext: if (pos < len) pos++; + + break; case EditDeletePrev: if (pos) delete(--pos, 1); + break; case EditDeleteNext: delete(pos, 1); + break; case EditKillPrev: delete(0, pos); pos = 0; + break; case EditKillNext: delete(pos, len - pos); break; case EditInsert: { reserve(pos, 1); diff --git a/ui.c b/ui.c index 8e502ca..d83a1f3 100644 --- a/ui.c +++ b/ui.c @@ -624,13 +624,13 @@ static void keyCode(int code) { break; case KeyMetaM: waddch(windows.active->pad, '\n'); - break; case KEY_BACKSPACE: edit(id, EditErase, 0); - break; case KEY_DC: edit(id, EditDelete, 0); - break; case KEY_END: edit(id, EditEnd, 0); + break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); + break; case KEY_DC: edit(id, EditDeleteNext, 0); + break; case KEY_END: edit(id, EditTail, 0); break; case KEY_ENTER: edit(id, EditEnter, 0); - break; case KEY_HOME: edit(id, EditHome, 0); - break; case KEY_LEFT: edit(id, EditLeft, 0); - break; case KEY_RIGHT: edit(id, EditRight, 0); + break; case KEY_HOME: edit(id, EditHead, 0); + break; case KEY_LEFT: edit(id, EditPrev, 0); + break; case KEY_RIGHT: edit(id, EditNext, 0); break; default: { if (code >= KeyMeta0 && code <= KeyMeta9) { @@ -643,17 +643,18 @@ static void keyCode(int code) { static void keyCtrl(wchar_t ch) { size_t id = windows.active->id; switch (ch ^ L'@') { - break; case L'?': edit(id, EditErase, 0); - break; case L'A': edit(id, EditHome, 0); - break; case L'B': edit(id, EditLeft, 0); - break; case L'D': edit(id, EditDelete, 0); - break; case L'E': edit(id, EditEnd, 0); - break; case L'F': edit(id, EditRight, 0); - break; case L'H': edit(id, EditErase, 0); + break; case L'?': edit(id, EditDeletePrev, 0); + break; case L'A': edit(id, EditHead, 0); + break; case L'B': edit(id, EditPrev, 0); + break; case L'D': edit(id, EditDeleteNext, 0); + break; case L'E': edit(id, EditTail, 0); + break; case L'F': edit(id, EditNext, 0); + break; case L'H': edit(id, EditDeletePrev, 0); break; case L'I': edit(id, EditComplete, 0); break; case L'J': edit(id, EditEnter, 0); + break; case L'K': edit(id, EditKillNext, 0); break; case L'L': clearok(curscr, true); - break; case L'U': edit(id, EditKill, 0); + break; case L'U': edit(id, EditKillPrev, 0); } } -- cgit 1.4.1-2-gfad0 From b08c2d03efa08bd319a0665d12bef34df08ab283 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 04:20:07 -0500 Subject: Add M-b and M-f --- catgirl.1 | 4 ++++ chat.h | 2 ++ edit.c | 8 ++++++++ ui.c | 4 ++++ 4 files changed, 18 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index a356fe0..9cb208e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -211,6 +211,10 @@ Move right. Delete to end of line. .It Ic C-u Delete to beginning of line. +.It Ic M-b +Move to previous word. +.It Ic M-f +Move to next word. .El . .Ss Window Keys diff --git a/chat.h b/chat.h index aa1bcc1..6b68eae 100644 --- a/chat.h +++ b/chat.h @@ -149,6 +149,8 @@ enum Edit { EditTail, EditPrev, EditNext, + EditPrevWord, + EditNextWord, EditKillPrev, EditKillNext, EditDeletePrev, diff --git a/edit.c b/edit.c index 7fcff40..38b2dea 100644 --- a/edit.c +++ b/edit.c @@ -137,6 +137,14 @@ void edit(size_t id, enum Edit op, wchar_t ch) { break; case EditTail: pos = len; break; case EditPrev: if (pos) pos--; break; case EditNext: if (pos < len) pos++; + break; case EditPrevWord: { + if (pos) pos--; + while (pos && buf[pos - 1] != L' ') pos--; + } + break; case EditNextWord: { + if (pos < len) pos++; + while (pos < len && buf[pos] != L' ') pos++; + } break; case EditDeletePrev: if (pos) delete(--pos, 1); break; case EditDeleteNext: delete(pos, 1); diff --git a/ui.c b/ui.c index d83a1f3..7811e88 100644 --- a/ui.c +++ b/ui.c @@ -192,6 +192,8 @@ static void errExit(void) { X(KeyMeta7, "\0337") \ X(KeyMeta8, "\0338") \ X(KeyMeta9, "\0339") \ + X(KeyMetaB, "\033b") \ + X(KeyMetaF, "\033f") \ X(KeyMetaM, "\33m") \ X(KeyFocusIn, "\33[I") \ X(KeyFocusOut, "\33[O") \ @@ -622,6 +624,8 @@ static void keyCode(int code) { break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO + break; case KeyMetaB: edit(id, EditPrevWord, 0); + break; case KeyMetaF: edit(id, EditNextWord, 0); break; case KeyMetaM: waddch(windows.active->pad, '\n'); break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); -- cgit 1.4.1-2-gfad0 From 5e637324c9f2b16a602c1b66081390624598c703 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 04:32:32 -0500 Subject: Add C-w and M-d --- catgirl.1 | 4 ++++ chat.h | 2 ++ edit.c | 17 +++++++++++++++-- ui.c | 7 +++++-- 4 files changed, 26 insertions(+), 4 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 9cb208e..6f8256b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -211,8 +211,12 @@ Move right. Delete to end of line. .It Ic C-u Delete to beginning of line. +.It Ic C-w +Delete previous word. .It Ic M-b Move to previous word. +.It Ic M-d +Delete next word. .It Ic M-f Move to next word. .El diff --git a/chat.h b/chat.h index ac56f51..fc18b15 100644 --- a/chat.h +++ b/chat.h @@ -155,6 +155,8 @@ enum Edit { EditDeleteTail, EditDeletePrev, EditDeleteNext, + EditDeletePrevWord, + EditDeleteNextWord, EditInsert, EditComplete, EditEnter, diff --git a/edit.c b/edit.c index 7b20079..47478ec 100644 --- a/edit.c +++ b/edit.c @@ -146,10 +146,23 @@ void edit(size_t id, enum Edit op, wchar_t ch) { while (pos < len && buf[pos] != L' ') pos++; } - break; case EditDeletePrev: if (pos) delete(--pos, 1); - break; case EditDeleteNext: delete(pos, 1); break; case EditDeleteHead: delete(0, pos); pos = 0; break; case EditDeleteTail: delete(pos, len - pos); + break; case EditDeletePrev: if (pos) delete(--pos, 1); + break; case EditDeleteNext: delete(pos, 1); + break; case EditDeletePrevWord: { + if (!pos) break; + size_t word = pos - 1; + while (word && buf[word - 1] != L' ') word--; + delete(word, pos - word); + pos = word; + } + break; case EditDeleteNextWord: { + if (pos == len) break; + size_t word = pos + 1; + while (word < len && buf[word] != L' ') word++; + delete(pos, word - pos); + } break; case EditInsert: { reserve(pos, 1); diff --git a/ui.c b/ui.c index e3b9cb5..65b4760 100644 --- a/ui.c +++ b/ui.c @@ -192,8 +192,9 @@ static void errExit(void) { X(KeyMeta7, "\0337") \ X(KeyMeta8, "\0338") \ X(KeyMeta9, "\0339") \ - X(KeyMetaB, "\033b") \ - X(KeyMetaF, "\033f") \ + X(KeyMetaB, "\33b") \ + X(KeyMetaD, "\33d") \ + X(KeyMetaF, "\33f") \ X(KeyMetaM, "\33m") \ X(KeyFocusIn, "\33[I") \ X(KeyFocusOut, "\33[O") \ @@ -625,6 +626,7 @@ static void keyCode(int code) { break; case KeyPasteOff:; // TODO break; case KeyMetaB: edit(id, EditPrevWord, 0); + break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); break; case KeyMetaF: edit(id, EditNextWord, 0); break; case KeyMetaM: waddch(windows.active->pad, '\n'); @@ -659,6 +661,7 @@ static void keyCtrl(wchar_t ch) { break; case L'K': edit(id, EditDeleteTail, 0); break; case L'L': clearok(curscr, true); break; case L'U': edit(id, EditDeleteHead, 0); + break; case L'W': edit(id, EditDeletePrevWord, 0); } } -- cgit 1.4.1-2-gfad0 From cbc6ff2da722df93fdff4df7268128937d6dd9b0 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 04:44:46 -0500 Subject: Add general key bindings paragraph to manual --- catgirl.1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 6f8256b..752a9d2 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -195,6 +195,19 @@ Switch to window by number. .El . .Sh KEY BINDINGS +The +.Nm +interface provides +.Xr emacs 1 Ns -like +line editing +as well as keys for IRC formatting. +The prefixes +.Ic C- +and +.Ic M- +represent the control and meta (alt) +modifiers, respectively. +. .Ss Line Editing .Bl -tag -width Ds -compact .It Ic C-a @@ -219,6 +232,8 @@ Move to previous word. Delete next word. .It Ic M-f Move to next word. +.It Ic Tab +Complete nick, channel or command. .El . .Ss Window Keys -- cgit 1.4.1-2-gfad0 From 2aa2005339750e64a587f6117ae21960e975e211 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 07:09:51 -0500 Subject: Add C-y This is weechat's binding for it. --- catgirl.1 | 2 ++ chat.h | 1 + edit.c | 26 ++++++++++++++++++++++---- ui.c | 7 +++++-- 4 files changed, 30 insertions(+), 6 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 752a9d2..2a3828d 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -226,6 +226,8 @@ Delete to end of line. Delete to beginning of line. .It Ic C-w Delete previous word. +.It Ic C-y +Paste previously deleted text. .It Ic M-b Move to previous word. .It Ic M-d diff --git a/chat.h b/chat.h index fc18b15..24360f0 100644 --- a/chat.h +++ b/chat.h @@ -157,6 +157,7 @@ enum Edit { EditDeleteNext, EditDeletePrevWord, EditDeleteNextWord, + EditPaste, EditInsert, EditComplete, EditEnter, diff --git a/edit.c b/edit.c index 47478ec..16fa910 100644 --- a/edit.c +++ b/edit.c @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -46,14 +47,24 @@ char *editBuffer(size_t *mbsPos) { return mbs; } -static void reserve(size_t index, size_t count) { - if (len + count > Cap) return; +static struct { + wchar_t buf[Cap]; + size_t len; +} cut; + +static bool reserve(size_t index, size_t count) { + if (len + count > Cap) return false; memmove(&buf[index + count], &buf[index], sizeof(*buf) * (len - index)); len += count; + return true; } static void delete(size_t index, size_t count) { if (index + count > len) return; + if (count > 1) { + memcpy(cut.buf, &buf[index], sizeof(*buf) * count); + cut.len = count; + } memmove( &buf[index], &buf[index + count], sizeof(*buf) * (len - index - count) ); @@ -163,10 +174,17 @@ void edit(size_t id, enum Edit op, wchar_t ch) { while (word < len && buf[word] != L' ') word++; delete(pos, word - pos); } + break; case EditPaste: { + if (reserve(pos, cut.len)) { + memcpy(&buf[pos], cut.buf, sizeof(*buf) * cut.len); + pos += cut.len; + } + } break; case EditInsert: { - reserve(pos, 1); - if (pos < Cap) buf[pos++] = ch; + if (reserve(pos, 1)) { + buf[pos++] = ch; + } } break; case EditComplete: { tabComplete(id); diff --git a/ui.c b/ui.c index 65b4760..d946854 100644 --- a/ui.c +++ b/ui.c @@ -166,12 +166,14 @@ void uiHide(void) { endwin(); } -static void disableFlowControl(void) { +// Gain use of C-q, C-s, C-z, C-y, C-o. +static void acquireKeys(void) { struct termios term; int error = tcgetattr(STDOUT_FILENO, &term); if (error) err(EX_OSERR, "tcgetattr"); term.c_iflag &= ~IXON; term.c_cc[VSUSP] = _POSIX_VDISABLE; + term.c_cc[VDSUSP] = _POSIX_VDISABLE; term.c_cc[VDISCARD] = _POSIX_VDISABLE; error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term); if (error) err(EX_OSERR, "tcsetattr"); @@ -212,7 +214,7 @@ void uiInit(void) { initscr(); cbreak(); noecho(); - disableFlowControl(); + acquireKeys(); def_prog_mode(); atexit(errExit); colorInit(); @@ -662,6 +664,7 @@ static void keyCtrl(wchar_t ch) { break; case L'L': clearok(curscr, true); break; case L'U': edit(id, EditDeleteHead, 0); break; case L'W': edit(id, EditDeletePrevWord, 0); + break; case L'Y': edit(id, EditPaste, 0); } } -- cgit 1.4.1-2-gfad0 From 26eefa35c90760536a2045a5d097e7670613c4b0 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 07:46:30 -0500 Subject: Add C-n and C-p --- catgirl.1 | 4 ++++ ui.c | 3 +++ 2 files changed, 7 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 2a3828d..5648c92 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -242,6 +242,10 @@ Complete nick, channel or command. .Bl -tag -width Ds -compact .It Ic C-l Redraw the UI. +.It Ic C-n +Switch to next window. +.It Ic C-p +Switch to previous window. .It Ic M-m Insert a blank line in the window. .It Ic M- Ns Ar n diff --git a/ui.c b/ui.c index d946854..8d0f0f7 100644 --- a/ui.c +++ b/ui.c @@ -565,6 +565,7 @@ static void inputUpdate(void) { } static void windowShow(struct Window *window) { + if (!window) return; touchwin(window->pad); windows.other = windows.active; windows.active = window; @@ -662,6 +663,8 @@ static void keyCtrl(wchar_t ch) { break; case L'J': edit(id, EditEnter, 0); break; case L'K': edit(id, EditDeleteTail, 0); break; case L'L': clearok(curscr, true); + break; case L'N': windowShow(windows.active->next); + break; case L'P': windowShow(windows.active->prev); break; case L'U': edit(id, EditDeleteHead, 0); break; case L'W': edit(id, EditDeletePrevWord, 0); break; case L'Y': edit(id, EditPaste, 0); -- cgit 1.4.1-2-gfad0 From 16316679a136bae76c73e328858eceb7fffae72c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 08:14:22 -0500 Subject: Add M-a --- catgirl.1 | 2 ++ ui.c | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 5648c92..e5d17b4 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -246,6 +246,8 @@ Redraw the UI. Switch to next window. .It Ic C-p Switch to previous window. +.It Ic M-a +Cycle through unread windows. .It Ic M-m Insert a blank line in the window. .It Ic M- Ns Ar n diff --git a/ui.c b/ui.c index 8d0f0f7..ace7c1e 100644 --- a/ui.c +++ b/ui.c @@ -194,6 +194,7 @@ static void errExit(void) { X(KeyMeta7, "\0337") \ X(KeyMeta8, "\0338") \ X(KeyMeta9, "\0339") \ + X(KeyMetaA, "\33a") \ X(KeyMetaB, "\33b") \ X(KeyMetaD, "\33d") \ X(KeyMetaF, "\33f") \ @@ -619,6 +620,29 @@ void uiCloseNum(size_t num) { windowClose(window); } +static void showAuto(void) { + static bool origin; + if (!origin) { + windows.other = windows.active; + origin = true; + } + struct Window *other = windows.other; + for (struct Window *window = windows.head; window; window = window->next) { + if (window->heat < Hot) continue; + windowShow(window); + windows.other = other; + return; + } + for (struct Window *window = windows.head; window; window = window->next) { + if (window->heat < Warm) continue; + windowShow(window); + windows.other = other; + return; + } + windowShow(windows.other); + origin = false; +} + static void keyCode(int code) { size_t id = windows.active->id; switch (code) { @@ -628,6 +652,7 @@ static void keyCode(int code) { break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO + break; case KeyMetaA: showAuto(); break; case KeyMetaB: edit(id, EditPrevWord, 0); break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); break; case KeyMetaF: edit(id, EditNextWord, 0); -- cgit 1.4.1-2-gfad0 From 8ce6d4c37715f37b95dcb5a302438e7db4c00ad3 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 08:17:05 -0500 Subject: Add M-/ --- catgirl.1 | 2 ++ ui.c | 3 +++ 2 files changed, 5 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index e5d17b4..f11cae0 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -246,6 +246,8 @@ Redraw the UI. Switch to next window. .It Ic C-p Switch to previous window. +.It Ic M-/ +Switch to previously selected window. .It Ic M-a Cycle through unread windows. .It Ic M-m diff --git a/ui.c b/ui.c index ace7c1e..33d6a15 100644 --- a/ui.c +++ b/ui.c @@ -199,6 +199,7 @@ static void errExit(void) { X(KeyMetaD, "\33d") \ X(KeyMetaF, "\33f") \ X(KeyMetaM, "\33m") \ + X(KeyMetaSlash, "\33/") \ X(KeyFocusIn, "\33[I") \ X(KeyFocusOut, "\33[O") \ X(KeyPasteOn, "\33[200~") \ @@ -652,6 +653,8 @@ static void keyCode(int code) { break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO + break; case KeyMetaSlash: windowShow(windows.other); + break; case KeyMetaA: showAuto(); break; case KeyMetaB: edit(id, EditPrevWord, 0); break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); -- cgit 1.4.1-2-gfad0 From f0e2c089c943abc2d298646e9fd988aa2a1c0c16 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 12:13:51 -0500 Subject: Add M-u --- catgirl.1 | 2 ++ ui.c | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index f11cae0..eb7310d 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -254,6 +254,8 @@ Cycle through unread windows. Insert a blank line in the window. .It Ic M- Ns Ar n Switch to window by number 0\(en9. +.It Ic M-u +Scroll to first unread line. .El . .Ss IRC Formatting diff --git a/ui.c b/ui.c index 050ec2e..9abfffc 100644 --- a/ui.c +++ b/ui.c @@ -71,10 +71,11 @@ struct Window { size_t id; struct Buffer buffer; WINDOW *pad; + bool mark; enum Heat heat; - int unread; + int unreadCount; + int unreadLines; int scroll; - bool mark; struct Window *prev; struct Window *next; }; @@ -200,6 +201,7 @@ static void errExit(void) { X(KeyMetaD, "\33d") \ X(KeyMetaF, "\33f") \ X(KeyMetaM, "\33m") \ + X(KeyMetaU, "\33u") \ X(KeyMetaSlash, "\33/") \ X(KeyFocusIn, "\33[I") \ X(KeyFocusOut, "\33[O") \ @@ -347,7 +349,7 @@ static void statusUpdate(void) { int num; const struct Window *window; for (num = 0, window = windows.head; window; ++num, window = window->next) { - if (!window->unread && window != windows.active) continue; + if (!window->unreadCount && window != windows.active) continue; int unread; char buf[256]; snprintf( @@ -355,10 +357,10 @@ static void statusUpdate(void) { idColors[window->id], (window == windows.active ? "\26" : ""), num, idNames[window->id], &unread, (window->heat > Warm ? White : idColors[window->id]), - window->unread, + window->unreadCount, idColors[window->id] ); - if (!window->unread) buf[unread] = '\0'; + if (!window->unreadCount) buf[unread] = '\0'; statusAdd(buf); } wclrtoeol(status); @@ -368,9 +370,9 @@ static void statusUpdate(void) { snprintf( buf, sizeof(buf), "%s %s%n (%d)", self.network, idNames[windows.active->id], - &unread, windows.active->unread + &unread, windows.active->unreadCount ); - if (!windows.active->unread) buf[unread] = '\0'; + if (!windows.active->unreadCount) buf[unread] = '\0'; putp(to_status_line); putp(buf); putp(from_status_line); @@ -380,7 +382,7 @@ static void statusUpdate(void) { static void unmark(struct Window *window) { if (!window->scroll) { window->heat = Cold; - window->unread = 0; + window->unreadCount = 0; window->mark = false; } statusUpdate(); @@ -396,6 +398,11 @@ static void windowScroll(struct Window *window, int n) { if (!window->scroll) unmark(window); } +static void windowScrollUnread(struct Window *window) { + window->scroll = 0; + windowScroll(window, window->unreadLines - PAGE_LINES); +} + static int wordWidth(const char *str) { size_t len = strcspn(str, " "); int width = 0; @@ -466,8 +473,9 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { int lines = 1; waddch(window->pad, '\n'); + if (window->mark && !window->unreadCount) window->unreadLines = 0; if (window->mark && heat > Cold) { - if (!window->unread++) { + if (!window->unreadCount++) { lines++; waddch(window->pad, '\n'); } @@ -475,6 +483,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { statusUpdate(); } lines += wordWrap(window->pad, str); + if (window->mark) window->unreadLines += lines; if (window->scroll) windowScroll(window, lines); if (heat > Warm) beep(); } @@ -689,6 +698,7 @@ static void keyCode(int code) { break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); break; case KeyMetaF: edit(id, EditNextWord, 0); break; case KeyMetaM: waddch(window->pad, '\n'); + break; case KeyMetaU: windowScrollUnread(window); break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); break; case KEY_DC: edit(id, EditDeleteNext, 0); -- cgit 1.4.1-2-gfad0 From 5254e1035c5945407ee354276f839426fc17e432 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 14:09:27 -0500 Subject: Add /help Now with automatic search! Also had to fix the SIGCHLD handling... --- catgirl.1 | 6 ++++++ chat.c | 2 ++ command.c | 19 +++++++++++++++++++ ui.c | 7 +++++++ 4 files changed, 34 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index eb7310d..5772db3 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -179,6 +179,12 @@ or matching Toggle logging in the .Sy window. +.It Ic /help Op Ar search +View this manual. +Type +.Ic q +to return to +.Nm . .It Ic /open Op Ar count Open each of .Ar count diff --git a/chat.c b/chat.c index dbad242..ff74485 100644 --- a/chat.c +++ b/chat.c @@ -191,6 +191,7 @@ int main(int argc, char *argv[]) { if (signals[SIGINT] || signals[SIGTERM]) break; if (signals[SIGCHLD]) { + signals[SIGCHLD] = 0; int status; while (0 < waitpid(-1, &status, WNOHANG)) { if (WIFEXITED(status) && WEXITSTATUS(status)) { @@ -206,6 +207,7 @@ int main(int argc, char *argv[]) { ); } } + uiShow(); } if (signals[SIGWINCH]) { diff --git a/command.c b/command.c index f88a6d5..44d0d54 100644 --- a/command.c +++ b/command.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "chat.h" @@ -158,6 +159,23 @@ static void commandCopy(size_t id, char *params) { urlCopyMatch(id, params); } +static void commandHelp(size_t id, char *params) { + (void)id; + uiHide(); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + char buf[256]; + snprintf(buf, sizeof(buf), "ip%s$", (params ? params : "COMMANDS")); + setenv("LESS", buf, 1); + execlp("man", "man", "1", "catgirl", NULL); + dup2(procPipe[1], STDERR_FILENO); + warn("man"); + _exit(EX_UNAVAILABLE); +} + static const struct Handler { const char *cmd; Command *fn; @@ -165,6 +183,7 @@ static const struct Handler { { "/close", commandClose }, { "/copy", commandCopy }, { "/debug", commandDebug }, + { "/help", commandHelp }, { "/join", commandJoin }, { "/me", commandMe }, { "/names", commandNames }, diff --git a/ui.c b/ui.c index 9abfffc..66a9c59 100644 --- a/ui.c +++ b/ui.c @@ -156,13 +156,18 @@ static const char *ExitFocusMode = "\33[?1004l"; static const char *EnterPasteMode = "\33[?2004h"; static const char *ExitPasteMode = "\33[?2004l"; +static bool hidden; + void uiShow(void) { putp(EnterFocusMode); putp(EnterPasteMode); fflush(stdout); + hidden = false; + uiDraw(); } void uiHide(void) { + hidden = true; putp(ExitFocusMode); putp(ExitPasteMode); endwin(); @@ -250,6 +255,7 @@ void uiInit(void) { } void uiDraw(void) { + if (hidden) return; wnoutrefresh(status); struct Window *window = windows.active; pnoutrefresh( @@ -755,6 +761,7 @@ static void keyStyle(wchar_t ch) { } void uiRead(void) { + if (hidden) return; int ret; wint_t ch; static bool style; -- cgit 1.4.1-2-gfad0 From 2bb3590de9eb7f9195c32fb94491515ac395f1db Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 15:35:02 -0500 Subject: Add /msg Services tend to tell you to use /msg so it definitely needs to exist. --- catgirl.1 | 2 ++ command.c | 8 ++++++++ 2 files changed, 10 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 5772db3..8679f22 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -148,6 +148,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /msg Ar nick message +Send a private message. .It Ic /names List users in the channel. .It Ic /nick Ar nick diff --git a/command.c b/command.c index 44d0d54..6d9ef9b 100644 --- a/command.c +++ b/command.c @@ -71,6 +71,13 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandMsg(size_t id, char *params) { + (void)id; + char *nick = strsep(¶ms, " "); + if (!params) return; + ircFormat("PRIVMSG %s :%s\r\n", nick, params); +} + static void commandJoin(size_t id, char *params) { size_t count = 1; if (params) { @@ -186,6 +193,7 @@ static const struct Handler { { "/help", commandHelp }, { "/join", commandJoin }, { "/me", commandMe }, + { "/msg", commandMsg }, { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, -- cgit 1.4.1-2-gfad0 From 3436cd1068ca37cf4043bc8dc83e3b8890edcb2b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 16:45:49 -0500 Subject: Add /whois --- catgirl.1 | 2 ++ chat.h | 1 + command.c | 8 +++++ handle.c | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 8679f22..ac558a9 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -166,6 +166,8 @@ Quit IRC. Send a raw IRC command. .It Ic /topic Op Ar topic Show or set the topic of the channel. +.It Ic /whois Ar nick +Query information about a user. .El . .Ss UI Commands diff --git a/chat.h b/chat.h index 24360f0..f79cc70 100644 --- a/chat.h +++ b/chat.h @@ -120,6 +120,7 @@ void ircFormat(const char *format, ...) extern struct Replies { size_t topic; size_t names; + size_t whois; } replies; void handle(struct Message msg); diff --git a/command.c b/command.c index 6d9ef9b..3e201cc 100644 --- a/command.c +++ b/command.c @@ -124,6 +124,13 @@ static void commandNames(size_t id, char *params) { replies.names++; } +static void commandWhois(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("WHOIS :%s\r\n", params); + replies.whois++; +} + static void commandQuery(size_t id, char *params) { if (!params) return; size_t query = idFor(params); @@ -203,6 +210,7 @@ static const struct Handler { { "/quit", commandQuit }, { "/quote", commandQuote }, { "/topic", commandTopic }, + { "/whois", commandWhois }, { "/window", commandWindow }, }; diff --git a/handle.c b/handle.c index 2cc7a25..2ef2477 100644 --- a/handle.c +++ b/handle.c @@ -374,6 +374,97 @@ static void handleTopic(struct Message *msg) { } } +static void handleReplyWhoisUser(struct Message *msg) { + require(msg, false, 6); + if (!replies.whois) return; + completeTouch(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %s!%s@%s (%s)", + hash(msg->params[2]), msg->params[1], + msg->params[1], msg->params[2], msg->params[3], msg->params[5] + ); +} + +static void handleReplyWhoisServer(struct Message *msg) { + require(msg, false, 4); + if (!replies.whois) return; + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis connected to %s (%s)", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], msg->params[3] + ); +} + +static void handleReplyWhoisIdle(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + unsigned long idle = strtoul(msg->params[2], NULL, 10); + const char *unit = "second"; + if (idle / 60) { idle /= 60; unit = "minute"; } + if (idle / 60) { idle /= 60; unit = "hour"; } + if (idle / 24) { idle /= 24; unit = "day"; } + time_t signon = (msg->params[3] ? strtoul(msg->params[3], NULL, 10) : 0); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis idle for %lu %s%s%s%.*s", + completeColor(Network, msg->params[1]), msg->params[1], + idle, unit, (idle != 1 ? "s" : ""), + (signon ? ", signed on " : ""), + 24, (signon ? ctime(&signon) : "") + ); +} + +static void handleReplyWhoisChannels(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + char buf[1024]; + size_t len = 0; + while (msg->params[2]) { + char *channel = strsep(&msg->params[2], " "); + channel += strspn(channel, self.prefixes); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), hash(channel), channel + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis in %s", + completeColor(Network, msg->params[1]), msg->params[1], buf + ); +} + +static void handleReplyWhoisGeneric(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + if (msg->params[3]) { + msg->params[0] = msg->params[2]; + msg->params[2] = msg->params[3]; + msg->params[3] = msg->params[0]; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%s%s%s", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], + (msg->params[3] ? " " : ""), + (msg->params[3] ? msg->params[3] : "") + ); +} + +static void handleReplyEndOfWhois(struct Message *msg) { + require(msg, false, 2); + if (!replies.whois) return; + if (!self.nick || strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } + replies.whois--; +} + static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -495,6 +586,15 @@ static const struct Handler { } Handlers[] = { { "001", handleReplyWelcome }, { "005", handleReplyISupport }, + { "276", handleReplyWhoisGeneric }, + { "307", handleReplyWhoisGeneric }, + { "311", handleReplyWhoisUser }, + { "312", handleReplyWhoisServer }, + { "313", handleReplyWhoisGeneric }, + { "317", handleReplyWhoisIdle }, + { "318", handleReplyEndOfWhois }, + { "319", handleReplyWhoisChannels }, + { "330", handleReplyWhoisGeneric }, { "331", handleReplyNoTopic }, { "332", handleReplyTopic }, { "353", handleReplyNames }, @@ -502,6 +602,7 @@ static const struct Handler { { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, + { "671", handleReplyWhoisGeneric }, { "900", handleReplyLoggedIn }, { "904", handleErrorSASLFail }, { "905", handleErrorSASLFail }, -- cgit 1.4.1-2-gfad0 From 7470a705b3b0c577f7531bbbfa28ab2156678c94 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 18:16:01 -0500 Subject: Add M-l --- catgirl.1 | 7 +++++++ ui.c | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index ac558a9..5e333a8 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -260,6 +260,13 @@ Switch to previous window. Switch to previously selected window. .It Ic M-a Cycle through unread windows. +.It Ic M-l +List the contents of the window +without word-wrapping. +Press +.Ic Enter +to return to +.Nm . .It Ic M-m Insert a blank line in the window. .It Ic M- Ns Ar n diff --git a/ui.c b/ui.c index b3b98dd..66c695f 100644 --- a/ui.c +++ b/ui.c @@ -157,6 +157,7 @@ static const char *EnterPasteMode = "\33[?2004h"; static const char *ExitPasteMode = "\33[?2004l"; static bool hidden; +static bool waiting; void uiShow(void) { putp(EnterFocusMode); @@ -204,6 +205,7 @@ static void errExit(void) { X(KeyMetaB, "\33b") \ X(KeyMetaD, "\33d") \ X(KeyMetaF, "\33f") \ + X(KeyMetaL, "\33l") \ X(KeyMetaM, "\33m") \ X(KeyMetaU, "\33u") \ X(KeyMetaSlash, "\33/") \ @@ -528,6 +530,37 @@ static void resize(void) { statusUpdate(); } +static void bufferList(struct Buffer *buffer) { + uiHide(); + waiting = true; + for (size_t i = 0; i < BufferCap; ++i) { + time_t time = buffer->times[(buffer->len + i) % BufferCap]; + const char *line = buffer->lines[(buffer->len + i) % BufferCap]; + if (!line) continue; + + struct tm *tm = localtime(&time); + if (!tm) continue; + char buf[sizeof("[00:00:00]")]; + strftime(buf, sizeof(buf), "[%T]", tm); + vid_attr(colorAttr(mapColor(Gray)), colorPair(mapColor(Gray), -1), NULL); + printf("%s\t", buf); + + size_t len; + struct Style style = Reset; + while (*line) { + styleParse(&style, &line, &len); + vid_attr( + style.attr | colorAttr(mapColor(style.fg)), + colorPair(mapColor(style.fg), mapColor(style.bg)), + NULL + ); + if (len) printf("%.*s", (int)len, line); + line += len; + } + printf("\n"); + } +} + static void inputAdd(struct Style *style, const char *str) { size_t len; while (*str) { @@ -702,6 +735,7 @@ static void keyCode(int code) { break; case KeyMetaB: edit(id, EditPrevWord, 0); break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); break; case KeyMetaF: edit(id, EditNextWord, 0); + break; case KeyMetaL: bufferList(&window->buffer); break; case KeyMetaM: waddch(window->pad, '\n'); break; case KeyMetaU: windowScrollUnread(window); @@ -760,7 +794,16 @@ static void keyStyle(wchar_t ch) { } void uiRead(void) { - if (hidden) return; + if (hidden) { + if (waiting) { + uiShow(); + flushinp(); + waiting = false; + } else { + return; + } + } + int ret; wint_t ch; static bool style; -- cgit 1.4.1-2-gfad0 From 0d93e66a68ded28440e20cd7012b4e8b0c705fc6 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 05:50:28 -0500 Subject: Add -H --- catgirl.1 | 7 ++++++- chat.c | 7 ++++++- chat.h | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 5e333a8..15b387b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 9, 2020 +.Dd February 10, 2020 .Dt CATGIRL 1 .Os . @@ -10,6 +10,7 @@ .Nm .Op Fl ev .Op Fl C Ar copy +.Op Fl H Ar hash .Op Fl O Ar open .Op Fl a Ar auth .Op Fl c Ar cert @@ -57,6 +58,10 @@ The default is the first available of .Xr xclip 1 , .Xr xsel 1 . . +.It Fl H Ar hash , Cm hash = Ar hash +Set the initial value of +the nick color hash function. +. .It Fl O Ar util , Cm open = Ar util Set the utility used by .Ic /open . diff --git a/chat.c b/chat.c index ff74485..c58fdc5 100644 --- a/chat.c +++ b/chat.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,8 @@ size_t idNext = Network + 1; struct Self self = { .color = Default }; +uint32_t hashInit; + int procPipe[2] = { -1, -1 }; static void pipeRead(void) { @@ -81,10 +84,11 @@ int main(int argc, char *argv[]) { const char *user = NULL; const char *real = NULL; - const char *Opts = "!C:O:a:c:eh:j:k:n:p:r:u:vw:"; + const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:u:vw:"; const struct option LongOpts[] = { { "insecure", no_argument, NULL, '!' }, { "copy", required_argument, NULL, 'C' }, + { "hash", required_argument, NULL, 'H' }, { "open", required_argument, NULL, 'O' }, { "sasl-plain", required_argument, NULL, 'a' }, { "cert", required_argument, NULL, 'c' }, @@ -106,6 +110,7 @@ int main(int argc, char *argv[]) { switch (opt) { break; case '!': insecure = true; break; case 'C': urlCopyUtil = optarg; + break; case 'H': hashInit = strtoul(optarg, NULL, 0); break; case 'O': urlOpenUtil = optarg; break; case 'a': sasl = true; self.plain = optarg; break; case 'c': cert = optarg; diff --git a/chat.h b/chat.h index e7bb9cc..16cc683 100644 --- a/chat.h +++ b/chat.h @@ -190,9 +190,10 @@ int getopt_config( const char *optstring, const struct option *longopts, int *longindex ); +extern uint32_t hashInit; static inline enum Color hash(const char *str) { if (*str == '~') str++; - uint32_t hash = 0; + uint32_t hash = hashInit; for (; *str; ++str) { hash = (hash << 5) | (hash >> 27); hash ^= *str; -- cgit 1.4.1-2-gfad0 From b59431bb15ec74f05119a7c710a1f6a21e702bad Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 19:40:13 -0500 Subject: Add -s to save and load buffers --- catgirl.1 | 27 +++++++++- chat.c | 17 +++++- chat.h | 4 ++ config.c | 8 ++- ui.c | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 220 insertions(+), 12 deletions(-) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 15b387b..00f875b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -20,6 +20,7 @@ .Op Fl n Ar nick .Op Fl p Ar port .Op Fl r Ar real +.Op Fl s Ar save .Op Fl u Ar user .Op Fl w Ar pass .Op Ar config ... @@ -123,6 +124,18 @@ Set realname to .Ar real . The default realname is the same as the nickname. . +.It Fl s Ar name , Cm save = Ar name +Load and save the contents of windows from +.Ar name +in +.Pa $XDG_DATA_DIRS/catgirl , +or an absolute or relative path if +.Ar name +starts with +.Ql / +or +.Ql \&. . +. .It Fl u Ar user , Cm user = Ar user Set username to .Ar user . @@ -324,7 +337,7 @@ The color numbers are as follows: .Sh FILES .Bl -tag -width Ds .It Pa $XDG_CONFIG_DIRS/catgirl -Configuration files are search for first in +Configuration files are searched for first in .Ev $XDG_CONFIG_HOME , usually .Pa ~/.config , @@ -334,6 +347,18 @@ usually .Pa /etc/xdg . .It Pa ~/.config/catgirl The most likely location of configuration files. +. +.It Pa $XDG_DATA_DIRS/catgirl +Save files are searched for first in +.Ev $XDG_DATA_HOME , +usually +.Pa ~/.local/share , +followed by the colon-separated list of paths +.Ev $XDG_DATA_DIRS , +usually +.Pa /usr/local/share:/usr/share . +.It Pa ~/.local/share/catgirl +The most likely location of save files. .El . .Sh EXAMPLES diff --git a/chat.c b/chat.c index c58fdc5..e8713bb 100644 --- a/chat.c +++ b/chat.c @@ -47,6 +47,15 @@ size_t idNext = Network + 1; struct Self self = { .color = Default }; +static const char *save; +static void exitSave(void) { + int error = uiSave(save); + if (error) { + warn("%s", save); + _exit(EX_IOERR); + } +} + uint32_t hashInit; int procPipe[2] = { -1, -1 }; @@ -84,7 +93,7 @@ int main(int argc, char *argv[]) { const char *user = NULL; const char *real = NULL; - const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:u:vw:"; + const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:s:u:vw:"; const struct option LongOpts[] = { { "insecure", no_argument, NULL, '!' }, { "copy", required_argument, NULL, 'C' }, @@ -99,6 +108,7 @@ int main(int argc, char *argv[]) { { "nick", required_argument, NULL, 'n' }, { "port", required_argument, NULL, 'p' }, { "real", required_argument, NULL, 'r' }, + { "save", required_argument, NULL, 's' }, { "user", required_argument, NULL, 'u' }, { "debug", no_argument, NULL, 'v' }, { "pass", required_argument, NULL, 'w' }, @@ -121,6 +131,7 @@ int main(int argc, char *argv[]) { break; case 'n': nick = optarg; break; case 'p': port = optarg; break; case 'r': real = optarg; + break; case 's': save = optarg; break; case 'u': user = optarg; break; case 'v': self.debug = true; break; case 'w': pass = optarg; @@ -154,6 +165,10 @@ int main(int argc, char *argv[]) { if (privFile) fclose(privFile); uiInit(); + if (save) { + uiLoad(save); + atexit(exitSave); + } uiShowID(Network); uiFormat(Network, Cold, NULL, "Traveling..."); uiDraw(); diff --git a/chat.h b/chat.h index 13319da..47a6163 100644 --- a/chat.h +++ b/chat.h @@ -26,6 +26,8 @@ #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit +#define XDG_SUBDIR "catgirl" + typedef unsigned char byte; int procPipe[2]; @@ -144,6 +146,8 @@ void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); void uiFormat( size_t id, enum Heat heat, const time_t *time, const char *format, ... ) __attribute__((format(printf, 4, 5))); +void uiLoad(const char *name); +int uiSave(const char *name); enum Edit { EditHead, diff --git a/config.c b/config.c index b3e42f9..3bf56c0 100644 --- a/config.c +++ b/config.c @@ -24,8 +24,6 @@ #include "chat.h" -#define CONFIG_DIR "catgirl" - FILE *configOpen(const char *path, const char *mode) { if (path[0] == '/' || path[0] == '.') goto local; @@ -35,10 +33,10 @@ FILE *configOpen(const char *path, const char *mode) { char buf[PATH_MAX]; if (configHome) { - snprintf(buf, sizeof(buf), "%s/" CONFIG_DIR "/%s", configHome, path); + snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path); } else { if (!home) goto local; - snprintf(buf, sizeof(buf), "%s/.config/" CONFIG_DIR "/%s", home, path); + snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path); } FILE *file = fopen(buf, mode); if (file) return file; @@ -48,7 +46,7 @@ FILE *configOpen(const char *path, const char *mode) { while (*configDirs) { size_t len = strcspn(configDirs, ":"); snprintf( - buf, sizeof(buf), "%.*s/" CONFIG_DIR "/%s", + buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s", (int)len, configDirs, path ); file = fopen(buf, mode); diff --git a/ui.c b/ui.c index 9a070a6..57ef322 100644 --- a/ui.c +++ b/ui.c @@ -20,11 +20,14 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -66,6 +69,13 @@ static void bufferPush(struct Buffer *buffer, time_t time, const char *line) { if (!buffer->lines[i]) err(EX_OSERR, "strdup"); } +static time_t bufferTime(const struct Buffer *buffer, size_t i) { + return buffer->times[(buffer->len + i) % BufferCap]; +} +static const char *bufferLine(const struct Buffer *buffer, size_t i) { + return buffer->lines[(buffer->len + i) % BufferCap]; +} + enum { WindowLines = BufferCap }; struct Window { size_t id; @@ -532,9 +542,8 @@ static void reflow(struct Window *window) { werase(window->pad); wmove(window->pad, WindowLines - 1, 0); window->unreadLines = 0; - struct Buffer *buffer = &window->buffer; for (size_t i = 0; i < BufferCap; ++i) { - char *line = buffer->lines[(buffer->len + i) % BufferCap]; + const char *line = bufferLine(&window->buffer, i); if (!line) continue; waddch(window->pad, '\n'); if (i >= (size_t)(BufferCap - window->unreadCount)) { @@ -557,12 +566,12 @@ static void resize(void) { statusUpdate(); } -static void bufferList(struct Buffer *buffer) { +static void bufferList(const struct Buffer *buffer) { uiHide(); waiting = true; for (size_t i = 0; i < BufferCap; ++i) { - time_t time = buffer->times[(buffer->len + i) % BufferCap]; - const char *line = buffer->lines[(buffer->len + i) % BufferCap]; + time_t time = bufferTime(buffer, i); + const char *line = bufferLine(buffer, i); if (!line) continue; struct tm *tm = localtime(&time); @@ -848,3 +857,160 @@ void uiRead(void) { } inputUpdate(); } + +static FILE *dataOpen(const char *path, const char *mode) { + if (path[0] == '/' || path[0] == '.') goto local; + + const char *home = getenv("HOME"); + const char *dataHome = getenv("XDG_DATA_HOME"); + const char *dataDirs = getenv("XDG_DATA_DIRS"); + + char homePath[PATH_MAX]; + if (dataHome) { + snprintf( + homePath, sizeof(homePath), + "%s/" XDG_SUBDIR "/%s", dataHome, path + ); + } else { + if (!home) goto local; + snprintf( + homePath, sizeof(homePath), + "%s/.local/share/" XDG_SUBDIR "/%s", home, path + ); + } + FILE *file = fopen(homePath, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", homePath); + return NULL; + } + + char buf[PATH_MAX]; + if (!dataDirs) dataDirs = "/usr/local/share:/usr/share"; + while (*dataDirs) { + size_t len = strcspn(dataDirs, ":"); + snprintf( + buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s", + (int)len, dataDirs, path + ); + file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + dataDirs += len; + if (*dataDirs) dataDirs++; + } + + if (mode[0] != 'r') { + char *base = strrchr(homePath, '/'); + *base = '\0'; + int error = mkdir(homePath, S_IRWXU); + if (error && errno != EEXIST) { + warn("%s", homePath); + return NULL; + } + *base = '/'; + file = fopen(homePath, mode); + if (!file) warn("%s", homePath); + return file; + } + +local: + file = fopen(path, mode); + if (!file) warn("%s", path); + return file; +} + +static const size_t Signatures[] = { + 0x6C72696774616301, +}; + +static size_t signatureVersion(size_t signature) { + for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) { + if (signature == Signatures[i]) return i; + } + err(EX_DATAERR, "unknown file signature %zX", signature); +} + +static int writeSize(FILE *file, size_t value) { + return (fwrite(&value, sizeof(value), 1, file) ? 0 : -1); +} +static int writeTime(FILE *file, time_t time) { + return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1); +} +static int writeString(FILE *file, const char *str) { + return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1); +} + +int uiSave(const char *name) { + FILE *file = dataOpen(name, "w"); + if (!file) return -1; + + if (writeSize(file, Signatures[0])) return -1; + const struct Window *window; + for (window = windows.head; window; window = window->next) { + if (writeString(file, idNames[window->id])) return -1; + for (size_t i = 0; i < BufferCap; ++i) { + time_t time = bufferTime(&window->buffer, i); + const char *line = bufferLine(&window->buffer, i); + if (!line) continue; + if (writeTime(file, time)) return -1; + if (writeString(file, line)) return -1; + } + if (writeTime(file, 0)) return -1; + } + return fclose(file); +} + +static size_t readSize(FILE *file) { + size_t value; + fread(&value, sizeof(value), 1, file); + if (ferror(file)) err(EX_IOERR, "fread"); + if (feof(file)) errx(EX_DATAERR, "unexpected eof"); + return value; +} +static time_t readTime(FILE *file) { + time_t time; + fread(&time, sizeof(time), 1, file); + if (ferror(file)) err(EX_IOERR, "fread"); + if (feof(file)) errx(EX_DATAERR, "unexpected eof"); + return time; +} +static ssize_t readString(FILE *file, char **buf, size_t *cap) { + ssize_t len = getdelim(buf, cap, '\0', file); + if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim"); + return len; +} + +void uiLoad(const char *name) { + FILE *file = dataOpen(name, "r"); + if (!file) { + if (errno != ENOENT) exit(EX_NOINPUT); + file = dataOpen(name, "w"); + if (!file) exit(EX_CANTCREAT); + fclose(file); + return; + } + + size_t signature = readSize(file); + signatureVersion(signature); + + char *buf = NULL; + size_t cap = 0; + while (0 < readString(file, &buf, &cap)) { + struct Window *window = windowFor(idFor(buf)); + for (;;) { + time_t time = readTime(file); + if (!time) break; + readString(file, &buf, &cap); + bufferPush(&window->buffer, time, buf); + } + reflow(window); + // TODO: Place some marker of end of save. + } + + free(buf); + fclose(file); +} -- cgit 1.4.1-2-gfad0 From 3a156540b8d134b05d7c318ac047a0c690cdc950 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:29:19 -0500 Subject: Add C-o as alias of M-/ M-/ is from weechat. C-o is like in vim. --- catgirl.1 | 2 ++ ui.c | 1 + 2 files changed, 3 insertions(+) (limited to 'catgirl.1') diff --git a/catgirl.1 b/catgirl.1 index 00f875b..7c51b08 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -272,6 +272,8 @@ Complete nick, channel or command. Redraw the UI. .It Ic C-n Switch to next window. +.It Ic C-o +Switch to previously selected window. .It Ic C-p Switch to previous window. .It Ic M-/ diff --git a/ui.c b/ui.c index 9601aaa..5f912b7 100644 --- a/ui.c +++ b/ui.c @@ -805,6 +805,7 @@ static void keyCtrl(wchar_t ch) { break; case L'K': edit(id, EditDeleteTail, 0); break; case L'L': clearok(curscr, true); break; case L'N': windowShow(windows.active->next); + break; case L'O': windowShow(windows.other); break; case L'P': windowShow(windows.active->prev); break; case L'U': edit(id, EditDeleteHead, 0); break; case L'W': edit(id, EditDeletePrevWord, 0); -- cgit 1.4.1-2-gfad0