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 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(-) 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 843160236381d0c76bef1eac89e556920d700a9d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 01:18:01 -0500 Subject: Blindly implement login flow --- .gitignore | 2 + Makefile | 18 ++++++ chat.c | 73 ++++++++++++++++++++++ chat.h | 102 ++++++++++++++++++++++++++++++ handle.c | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ irc.c | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 563 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 chat.c create mode 100644 chat.h create mode 100644 handle.c create mode 100644 irc.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab80afd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +catgirl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30bcf66 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +LIBRESSL_PREFIX = /usr/local +CFLAGS += -I${LIBRESSL_PREFIX}/include +LDFLAGS += -L${LIBRESSL_PREFIX}/lib + +CFLAGS += -std=c11 -Wall -Wextra -Wpedantic +LDLIBS = -lcrypto -ltls + +OBJS += chat.o +OBJS += handle.o +OBJS += irc.o + +catgirl: ${OBJS} + ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ + +${OBJS}: chat.h + +clean: + rm -f catgirl ${OBJS} diff --git a/chat.c b/chat.c new file mode 100644 index 0000000..89579c0 --- /dev/null +++ b/chat.c @@ -0,0 +1,73 @@ +/* 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" + +struct Self self; + +int main(int argc, char *argv[]) { + bool insecure = false; + const char *host = NULL; + const char *port = "6697"; + const char *cert = NULL; + const char *priv = NULL; + + bool sasl = false; + const char *pass = NULL; + const char *nick = NULL; + const char *user = NULL; + const char *real = NULL; + + int opt; + while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:w:"))) { + switch (opt) { + break; case '!': insecure = true; + break; case 'a': sasl = true; self.plain = optarg; + break; case 'c': cert = optarg; + break; case 'e': sasl = true; + break; case 'h': host = optarg; + break; case 'j': self.join = optarg; + break; case 'k': priv = optarg; + break; case 'n': nick = optarg; + break; case 'p': port = optarg; + break; case 'r': real = optarg; + break; case 'u': user = optarg; + break; case 'w': pass = optarg; + } + } + if (!host) errx(EX_USAGE, "host required"); + + if (!nick) nick = getenv("USER"); + if (!nick) errx(EX_CONFIG, "USER unset"); + if (!user) user = nick; + if (!real) real = nick; + + ircConfig(insecure, cert, priv); + + int irc = ircConnect(host, port); + if (pass) ircFormat("PASS :%s\r\n", pass); + if (sasl) ircFormat("CAP REQ :sasl\r\n"); + ircFormat("CAP LS\r\n"); + ircFormat("NICK :%s\r\n", nick); + ircFormat("USER %s 0 * :%s\r\n", user, real); +} diff --git a/chat.h b/chat.h new file mode 100644 index 0000000..bb8929b --- /dev/null +++ b/chat.h @@ -0,0 +1,102 @@ +/* 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 + +#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) +#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit + +typedef unsigned char byte; + +#define ENUM_CAP \ + X("sasl", CapSASL) \ + X("server-time", CapServerTime) \ + X("userhost-in-names", CapUserhostInNames) + +enum Cap { +#define X(name, id) BIT(id), + ENUM_CAP +#undef X +}; + +extern struct Self { + enum Cap caps; + char *plain; + char *nick; + const char *join; +} self; + +#define ENUM_TAG \ + X("time", TagTime) + +enum Tag { +#define X(name, id) id, + ENUM_TAG +#undef X + TagCap, +}; + +enum { ParamCap = 15 }; +struct Message { + char *tags[TagCap]; + char *nick; + char *user; + char *host; + char *cmd; + char *params[ParamCap]; +}; + +void ircConfig(bool insecure, const char *cert, const char *priv); +int ircConnect(const char *host, const char *port); +void ircRecv(void); +void ircSend(const char *ptr, size_t len); +void ircFormat(const char *format, ...) + __attribute__((format(printf, 1, 2))); + +void handle(struct Message msg); + +#define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) + +static const char Base64[64] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +}; + +static inline void base64(char *dst, const byte *src, size_t len) { + size_t i = 0; + while (len > 2) { + dst[i++] = Base64[0x3F & (src[0] >> 2)]; + dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; + dst[i++] = Base64[0x3F & (src[1] << 2 | src[2] >> 6)]; + dst[i++] = Base64[0x3F & src[2]]; + src += 3; + len -= 3; + } + if (len) { + dst[i++] = Base64[0x3F & (src[0] >> 2)]; + if (len > 1) { + dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; + dst[i++] = Base64[0x3F & (src[1] << 2)]; + } else { + dst[i++] = Base64[0x3F & (src[0] << 4)]; + dst[i++] = '='; + } + dst[i++] = '='; + } + dst[i] = '\0'; +} + +// Defined in libcrypto if missing from libc: +void explicit_bzero(void *b, size_t len); diff --git a/handle.c b/handle.c new file mode 100644 index 0000000..4084525 --- /dev/null +++ b/handle.c @@ -0,0 +1,163 @@ +/* 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 *CapNames[] = { +#define X(name, id) [id] = name, + ENUM_CAP +#undef X +}; + +static enum Cap capParse(const char *list) { + enum Cap caps = 0; + while (*list) { + enum Cap cap = 0; + size_t len = strcspn(list, " "); + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (len != strlen(CapNames[i])) continue; + if (strncmp(list, CapNames[i], len)) continue; + cap = 1 << i; + break; + } + caps |= cap; + list += len; + if (*list) list++; + } + return caps; +} + +static const char *capList(enum Cap caps) { + static char buf[1024]; + buf[0] = '\0'; + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (caps & (1 << i)) { + if (buf[0]) strlcat(buf, " ", sizeof(buf)); + strlcat(buf, CapNames[i], sizeof(buf)); + } + } + return buf; +} + +static void set(char **field, const char *value) { + free(*field); + *field = strdup(value); + if (!*field) err(EX_OSERR, "strdup"); +} + +typedef void Handler(struct Message *msg); + +static void require(const struct Message *msg, bool origin, size_t len) { + if (origin && !msg->nick) { + errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + } + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", msg->cmd, 1 + i); + } +} + +static void handleCap(struct Message *msg) { + require(msg, false, 3); + enum Cap caps = capParse(msg->params[2]); + if (!strcmp(msg->params[1], "LS")) { + caps &= ~CapSASL; + ircFormat("CAP REQ :%s\r\n", capList(caps)); + } else if (!strcmp(msg->params[1], "ACK")) { + self.caps |= caps; + if (caps & CapSASL) { + ircFormat("AUTHENTICATE %s\r\n", (self.plain ? "PLAIN" : "EXTERNAL")); + } + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } else if (!strcmp(msg->params[1], "NAK")) { + errx(EX_CONFIG, "server does not support %s", msg->params[2]); + } +} + +static void handleAuthenticate(struct Message *msg) { + (void)msg; + if (!self.plain) { + ircFormat("AUTHENTICATE +\r\n"); + return; + } + + byte buf[299]; + size_t len = 1 + strlen(self.plain); + if (sizeof(buf) < len) errx(EX_CONFIG, "SASL PLAIN is too long"); + buf[0] = 0; + for (size_t i = 0; self.plain[i]; ++i) { + buf[1 + i] = (self.plain[i] == ':' ? 0 : self.plain[i]); + } + + char b64[BASE64_SIZE(sizeof(buf))]; + base64(b64, buf, len); + ircFormat("AUTHENTICATE "); + ircSend(b64, BASE64_SIZE(len)); + ircFormat("\r\n"); + + explicit_bzero(b64, sizeof(b64)); + explicit_bzero(buf, sizeof(buf)); + explicit_bzero(self.plain, strlen(self.plain)); +} + +static void handleReplyLoggedIn(struct Message *msg) { + (void)msg; + ircFormat("CAP END\r\n"); +} + +static void handleErrorSASLFail(struct Message *msg) { + require(msg, false, 2); + errx(EX_CONFIG, "%s", msg->params[1]); +} + +static void handleReplyWelcome(struct Message *msg) { + require(msg, false, 1); + set(&self.nick, msg->params[0]); + if (self.join) ircFormat("JOIN :%s\r\n", self.join); +} + +static const struct Handler { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "900", handleReplyLoggedIn }, + { "904", handleErrorSASLFail }, + { "905", handleErrorSASLFail }, + { "906", handleErrorSASLFail }, + { "AUTHENTICATE", handleAuthenticate }, + { "CAP", handleCap }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + +void handle(struct Message msg) { + if (!msg.cmd) return; + const struct Handler *handler = bsearch( + msg.cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar + ); + if (handler) handler->fn(&msg); +} diff --git a/irc.c b/irc.c new file mode 100644 index 0000000..c1c0e7a --- /dev/null +++ b/irc.c @@ -0,0 +1,205 @@ +/* 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 +#include +#include +#include +#include +#include + +#include "chat.h" + +struct tls *client; + +void ircConfig(bool insecure, const char *cert, const char *priv) { + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + int error = tls_config_set_ciphers(config, "compat"); + if (error) { + errx( + EX_SOFTWARE, "tls_config_set_ciphers: %s", + tls_config_error(config) + ); + } + + if (insecure) { + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + } + + if (cert) { + error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); + if (error) { + errx( + EX_SOFTWARE, "tls_config_set_keypair_file: %s", + tls_config_error(config) + ); + } + } + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); +} + +int ircConnect(const char *host, const char *port) { + assert(client); + + struct addrinfo *head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP, + }; + int error = getaddrinfo(host, port, &hints, &head); + if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error)); + + int sock = -1; + for (struct addrinfo *ai = head; ai; ai = ai->ai_next) { + sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock < 0) err(EX_OSERR, "socket"); + + error = connect(sock, ai->ai_addr, ai->ai_addrlen); + if (!error) break; + + close(sock); + sock = -1; + } + if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port); + freeaddrinfo(head); + + error = tls_connect_socket(client, sock, host); + if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client)); + + error = tls_handshake(client); + if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client)); + + return sock; +} + +void ircSend(const char *ptr, size_t len) { + assert(client); + while (len) { + ssize_t ret = tls_write(client, ptr, len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client)); + ptr += ret; + len -= ret; + } +} + +void ircFormat(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + ircSend(buf, len); +} + +static const char *TagNames[TagCap] = { +#define X(name, id) [id] = name, + ENUM_TAG +#undef X +}; + +static void unescape(char *tag) { + for (;;) { + tag = strchr(tag, '\\'); + if (!tag) break; + switch (tag[1]) { + break; case ':': tag[1] = ';'; + break; case 's': tag[1] = ' '; + break; case 'r': tag[1] = '\r'; + break; case 'n': tag[1] = '\n'; + } + memmove(tag, &tag[1], strlen(&tag[1]) + 1); + if (tag[0]) tag = &tag[1]; + } +} + +static struct Message parse(char *line) { + struct Message msg = { .cmd = NULL }; + + if (line[0] == '@') { + char *tags = 1 + strsep(&line, " "); + while (tags) { + char *tag = strsep(&tags, ";"); + char *key = strsep(&tag, "="); + for (size_t i = 0; i < TagCap; ++i) { + if (strcmp(key, TagNames[i])) continue; + unescape(tag); + msg.tags[i] = tag; + break; + } + } + } + + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + msg.user = strsep(&origin, "@"); + msg.host = origin; + } + + msg.cmd = strsep(&line, " "); + for (size_t i = 0; line && i < ParamCap; ++i) { + if (line[0] == ':') { + msg.params[i] = &line[1]; + break; + } + msg.params[i] = strsep(&line, " "); + } + + return msg; +} + +void ircRecv(void) { + static char buf[8191 + 512]; + static size_t len = 0; + + assert(client); + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) return; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + if (!ret) errx(EX_PROTOCOL, "server closed connection"); + len += ret; + + char *crlf; + char *line = buf; + for (;;) { + crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + *crlf = '\0'; + handle(parse(line)); + line = crlf + 2; + } + + len -= line - buf; + memmove(buf, line, len); +} -- cgit 1.4.1-2-gfad0 From 856d40d1212ec835b092a8f275124d09a65ba59d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 02:19:55 -0500 Subject: Fix CapNames array indices --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 4084525..96bd2a2 100644 --- a/handle.c +++ b/handle.c @@ -24,7 +24,7 @@ #include "chat.h" static const char *CapNames[] = { -#define X(name, id) [id] = name, +#define X(name, id) [id##Bit] = name, ENUM_CAP #undef X }; -- 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(-) 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 03cb0d7c04d287ba95b26924620ece0855002ad5 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 02:33:17 -0500 Subject: Add IDs and names --- chat.c | 7 +++++++ chat.h | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/chat.c b/chat.c index 5796085..462faa0 100644 --- a/chat.c +++ b/chat.c @@ -23,6 +23,13 @@ #include "chat.h" +char *idNames[IDCap] = { + [None] = "", + [Debug] = "", + [Network] = "", +}; +size_t idNext = Network + 1; + struct Self self; int main(int argc, char *argv[]) { diff --git a/chat.h b/chat.h index 4dd4732..8c13d49 100644 --- a/chat.h +++ b/chat.h @@ -14,13 +14,35 @@ * along with this program. If not, see . */ +#include #include +#include +#include +#include #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit typedef unsigned char byte; +enum { None, Debug, Network, IDCap = 256 }; +extern char *idNames[IDCap]; +extern size_t idNext; + +static inline size_t idFind(const char *name) { + for (size_t id = 0; id < idNext; ++id) { + if (!strcmp(idNames[id], name)) return id; + } + return None; +} +static inline size_t idFor(const char *name) { + size_t id = idFind(name); + if (id) return id; + idNames[idNext] = strdup(name); + if (!idNames[idNext]) err(EX_OSERR, "strdup"); + return idNext++; +} + #define ENUM_CAP \ X("sasl", CapSASL) \ X("server-time", CapServerTime) \ -- cgit 1.4.1-2-gfad0 From e289ff6b18e643eea4c72e04f7c0dc6ff768a335 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 02:55:07 -0500 Subject: Add term stuff Copied almost verbatim from existing catgirl... I think I did a better job on that state machine this time tbh. --- Makefile | 1 + chat.h | 17 ++++++++++++++ term.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 term.c diff --git a/Makefile b/Makefile index 30bcf66..999b491 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ LDLIBS = -lcrypto -ltls OBJS += chat.o OBJS += handle.o OBJS += irc.o +OBJS += term.o catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ diff --git a/chat.h b/chat.h index 8c13d49..93014ef 100644 --- a/chat.h +++ b/chat.h @@ -91,6 +91,23 @@ void ircFormat(const char *format, ...) void handle(struct Message msg); +enum TermMode { + TermFocus, + TermPaste, +}; +enum TermEvent { + TermNone, + TermFocusIn, + TermFocusOut, + TermPasteStart, + TermPasteEnd, +}; +void termInit(void); +void termNoFlow(void); +void termTitle(const char *title); +void termMode(enum TermMode mode, bool set); +enum TermEvent termEvent(char ch); + #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) static const char Base64[64] = { diff --git a/term.c b/term.c new file mode 100644 index 0000000..ade5392 --- /dev/null +++ b/term.c @@ -0,0 +1,79 @@ +/* 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 "chat.h" + +static bool xterm; + +void termInit(void) { + const char *term = getenv("TERM"); + xterm = (term && !strncmp(term, "xterm", 5)); +} + +void termNoFlow(void) { + struct termios attr; + int error = tcgetattr(STDIN_FILENO, &attr); + if (error) return; + attr.c_iflag &= ~IXON; + attr.c_cc[VDISCARD] = _POSIX_VDISABLE; + tcsetattr(STDIN_FILENO, TCSANOW, &attr); +} + +void termTitle(const char *title) { + if (!xterm) return; + printf("\33]0;%s\33\\", title); + fflush(stdout); +} + +static void privateMode(const char *mode, bool set) { + printf("\33[?%s%c", mode, (set ? 'h' : 'l')); + fflush(stdout); +} + +void termMode(enum TermMode mode, bool set) { + switch (mode) { + break; case TermFocus: privateMode("1004", set); + break; case TermPaste: privateMode("2004", set); + } +} + +enum { Esc = '\33' }; + +enum TermEvent termEvent(char ch) { + static int st; +#define T(st, ch) ((st) << 8 | (ch)) + switch (T(st, ch)) { + break; case T(0, Esc): st = 1; + break; case T(1, '['): st = 2; + break; case T(2, 'I'): st = 0; return TermFocusIn; + break; case T(2, 'O'): st = 0; return TermFocusOut; + break; case T(2, '2'): st = 3; + break; case T(3, '0'): st = 4; + break; case T(4, '0'): st = 5; + break; case T(5, '~'): st = 0; return TermPasteStart; + break; case T(4, '1'): st = 6; + break; case T(6, '~'): st = 0; return TermPasteEnd; + break; default: st = 0; + } + return 0; +#undef T +} -- cgit 1.4.1-2-gfad0 From e5363bcae0f726455fb4198cd21d46721ad5e39a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 19:37:48 -0500 Subject: Implement the beginnings of UI It takes so much code to do anything in curses... --- Makefile | 3 +- chat.c | 8 +++ chat.h | 22 +++++++- handle.c | 21 ++++++++ irc.c | 6 +-- ui.c | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 ui.c diff --git a/Makefile b/Makefile index 999b491..4af59ee 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,13 @@ CFLAGS += -I${LIBRESSL_PREFIX}/include LDFLAGS += -L${LIBRESSL_PREFIX}/lib CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -LDLIBS = -lcrypto -ltls +LDLIBS = -lcurses -lcrypto -ltls OBJS += chat.o OBJS += handle.o OBJS += irc.o OBJS += term.o +OBJS += ui.o catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ diff --git a/chat.c b/chat.c index 462faa0..47227d5 100644 --- a/chat.c +++ b/chat.c @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -33,6 +34,8 @@ size_t idNext = Network + 1; struct Self self; int main(int argc, char *argv[]) { + setlocale(LC_CTYPE, ""); + bool insecure = false; const char *host = NULL; const char *port = "6697"; @@ -71,6 +74,10 @@ int main(int argc, char *argv[]) { if (!real) real = nick; ircConfig(insecure, cert, priv); + + uiInit(); + uiFormat(Network, Cold, NULL, "Traveling..."); + uiDraw(); int irc = ircConnect(host, port); if (pass) ircFormat("PASS :%s\r\n", pass); @@ -80,6 +87,7 @@ int main(int argc, char *argv[]) { ircFormat("USER %s 0 * :%s\r\n", user, real); for (;;) { + uiDraw(); ircRecv(); } } diff --git a/chat.h b/chat.h index 93014ef..be3952c 100644 --- a/chat.h +++ b/chat.h @@ -82,6 +82,18 @@ struct Message { char *params[ParamCap]; }; +#define B "\2" +#define C "\3" +#define R "\17" +#define V "\26" +#define I "\35" +#define U "\37" +enum Color { + White, Black, Blue, Green, Red, Brown, Magenta, Orange, + Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, + Default = 99, +}; + void ircConfig(bool insecure, const char *cert, const char *priv); int ircConnect(const char *host, const char *port); void ircRecv(void); @@ -91,6 +103,14 @@ void ircFormat(const char *format, ...) void handle(struct Message msg); +enum Heat { Cold, Warm, Hot }; +void uiInit(void); +void uiDraw(void); +void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str); +void uiFormat( + size_t id, enum Heat heat, const struct tm *time, const char *format, ... +) __attribute__((format(printf, 4, 5))); + enum TermMode { TermFocus, TermPaste, @@ -109,11 +129,9 @@ void termMode(enum TermMode mode, bool set); enum TermEvent termEvent(char ch); #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) - static const char Base64[64] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" }; - static inline void base64(char *dst, const byte *src, size_t len) { size_t i = 0; while (len > 2) { diff --git a/handle.c b/handle.c index 96bd2a2..b2b2b8d 100644 --- a/handle.c +++ b/handle.c @@ -136,11 +136,32 @@ static void handleReplyWelcome(struct Message *msg) { if (self.join) ircFormat("JOIN :%s\r\n", self.join); } +static void handleReplyISupport(struct Message *msg) { + // TODO: Extract CHANTYPES and PREFIX for future use. + for (size_t i = 1; i < ParamCap; ++i) { + if (!msg->params[i]) break; + char *key = strsep(&msg->params[i], "="); + if (!msg->params[i]) continue; + if (!strcmp(key, "NETWORK")) { + uiFormat(Network, Cold, NULL, "You arrive in %s", msg->params[i]); + } + } +} + +static void handleReplyMOTD(struct Message *msg) { + require(msg, false, 2); + char *line = msg->params[1]; + if (!strncmp(line, "- ", 2)) line += 2; + uiFormat(Network, Cold, NULL, "%s", line); +} + static const struct Handler { const char *cmd; Handler *fn; } Handlers[] = { { "001", handleReplyWelcome }, + { "005", handleReplyISupport }, + { "372", handleReplyMOTD }, { "900", handleReplyLoggedIn }, { "904", handleErrorSASLFail }, { "905", handleErrorSASLFail }, diff --git a/irc.c b/irc.c index cf8aab7..f07fcc8 100644 --- a/irc.c +++ b/irc.c @@ -104,10 +104,10 @@ int ircConnect(const char *host, const char *port) { 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", + uiFormat( + Debug, Cold, NULL, C"%d%c%c"C" %.*s", Gray, dir, dir, (int)len, line - );*/ + ); if (!isatty(STDERR_FILENO)) { fprintf(stderr, "%c%c %.*s\n", dir, dir, (int)len, line); } diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..0295c8d --- /dev/null +++ b/ui.c @@ -0,0 +1,174 @@ +/* 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 +#include +#include + +#include "chat.h" + +#ifndef A_ITALIC +#define A_ITALIC A_UNDERLINE +#endif + +#define BOTTOM (LINES - 1) +#define RIGHT (COLS - 1) +#define WINDOW_LINES (LINES - 2) + +static short colorPairs; + +static void colorInit(void) { + start_color(); + use_default_colors(); + for (short pair = 0; pair < 16; ++pair) { + init_pair(1 + pair, pair % COLORS, -1); + } + colorPairs = 17; +} + +static attr_t colorAttr(short fg) { + return (fg >= COLORS ? A_BOLD : A_NORMAL); +} + +static short colorPair(short fg, short bg) { + if (bg == -1) return 1 + fg; + for (short pair = 17; pair < colorPairs; ++pair) { + short f, b; + pair_content(pair, &f, &b); + if (f == fg && b == bg) return pair; + } + init_pair(colorPairs, fg, bg); + return colorPairs++; +} + +enum { + InputCols = 512, + PadLines = 512, +}; + +static WINDOW *status; +static WINDOW *input; + +struct Window { + size_t id; + WINDOW *pad; + enum Heat heat; + int unread; + int scroll; + bool mark; + struct Window *prev; + struct Window *next; +}; + +static struct { + struct Window *active; + struct Window *other; + struct Window *head; + struct Window *tail; +} windows; + +static void windowAdd(struct Window *window) { + if (windows.tail) windows.tail->next = window; + window->prev = windows.tail; + window->next = NULL; + windows.tail = window; + if (!windows.head) windows.head = window; +} + +static void windowRemove(struct Window *window) { + if (window->prev) window->prev->next = window->next; + if (window->next) window->next->prev = window->prev; + if (windows.head == window) windows.head = window->next; + if (windows.tail == window) windows.tail = window->prev; +} + +static struct Window *windowFor(size_t id) { + struct Window *window; + for (window = windows.head; window; window = window->next) { + if (window->id == id) return window; + } + window = malloc(sizeof(*window)); + if (!window) err(EX_OSERR, "malloc"); + window->id = id; + window->pad = newpad(PadLines, COLS); + wsetscrreg(window->pad, 0, PadLines - 1); + scrollok(window->pad, true); + wmove(window->pad, PadLines - 1, 0); + window->heat = Cold; + window->unread = 0; + window->scroll = PadLines; + window->mark = true; + windowAdd(window); + return window; +} + +void uiInit(void) { + initscr(); + cbreak(); + noecho(); + termInit(); + termNoFlow(); + def_prog_mode(); + colorInit(); + status = newwin(1, COLS, 0, 0); + input = newpad(1, InputCols); + keypad(input, true); + nodelay(input, true); + windows.active = windowFor(Network); +} + +void uiDraw(void) { + wnoutrefresh(status); + pnoutrefresh( + windows.active->pad, + windows.active->scroll - WINDOW_LINES, 0, + 1, 0, + BOTTOM - 1, RIGHT + ); + // TODO: Input scrolling. + pnoutrefresh( + input, + 0, 0, + BOTTOM, 0, + BOTTOM, RIGHT + ); + doupdate(); +} + +void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) { + (void)time; + struct Window *window = windowFor(id); + waddch(window->pad, '\n'); + waddstr(window->pad, str); +} + +void uiFormat( + size_t id, enum Heat heat, const struct tm *time, const char *format, ... +) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + uiWrite(id, heat, time, buf); +} -- cgit 1.4.1-2-gfad0 From d59666cb25de1e0a9322e22d1fa94d26583383b2 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 21:55:05 -0500 Subject: Generate tags file --- .gitignore | 1 + Makefile | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ab80afd..64b2b13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.o catgirl +tags diff --git a/Makefile b/Makefile index 4af59ee..ce27d4e 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,17 @@ OBJS += irc.o OBJS += term.o OBJS += ui.o +dev: tags all + +all: catgirl + catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ ${OBJS}: chat.h +tags: *.h *.c + ctags -w *.h *.c + clean: - rm -f catgirl ${OBJS} + rm -f tags catgirl ${OBJS} -- cgit 1.4.1-2-gfad0 From cd3dc4ef4caaad3a696ad731c197f50105119b31 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 21:57:11 -0500 Subject: Parse IRC styling in UI Wow the colorPair thing actually works. Have I finally cracked curses colors? --- chat.c | 2 +- ui.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/chat.c b/chat.c index 47227d5..75cca62 100644 --- a/chat.c +++ b/chat.c @@ -76,7 +76,7 @@ int main(int argc, char *argv[]) { ircConfig(insecure, cert, priv); uiInit(); - uiFormat(Network, Cold, NULL, "Traveling..."); + uiFormat(Network, Cold, NULL, C "3Trave" U "ling" U C "0,3.." C "0,4."); uiDraw(); int irc = ircConnect(host, port); diff --git a/ui.c b/ui.c index 0295c8d..83c4bc7 100644 --- a/ui.c +++ b/ui.c @@ -15,12 +15,14 @@ */ #include +#include #include #include #include #include #include #include +#include #include #include @@ -56,7 +58,7 @@ static short colorPair(short fg, short bg) { pair_content(pair, &f, &b); if (f == fg && b == bg) return pair; } - init_pair(colorPairs, fg, bg); + init_pair(colorPairs, fg % COLORS, bg % COLORS); return colorPairs++; } @@ -154,11 +156,80 @@ void uiDraw(void) { doupdate(); } +struct Style { + attr_t attr; + enum Color fg, bg; +}; +static const struct Style Reset = { A_NORMAL, Default, Default }; + +static short mapColor(enum Color color) { + switch (color) { + break; case White: return 8 + COLOR_WHITE; + break; case Black: return 0 + COLOR_BLACK; + break; case Blue: return 0 + COLOR_BLUE; + break; case Green: return 0 + COLOR_GREEN; + break; case Red: return 8 + COLOR_RED; + break; case Brown: return 0 + COLOR_RED; + break; case Magenta: return 0 + COLOR_MAGENTA; + break; case Orange: return 0 + COLOR_YELLOW; + break; case Yellow: return 8 + COLOR_YELLOW; + break; case LightGreen: return 8 + COLOR_GREEN; + break; case Cyan: return 0 + COLOR_CYAN; + break; case LightCyan: return 8 + COLOR_CYAN; + break; case LightBlue: return 8 + COLOR_BLUE; + break; case Pink: return 8 + COLOR_MAGENTA; + break; case Gray: return 8 + COLOR_BLACK; + break; case LightGray: return 0 + COLOR_WHITE; + break; default: return -1; + } +} + +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': { + (*str)++; + if (!isdigit(**str)) { + style->fg = Default; + style->bg = Default; + break; + } + style->fg = *(*str)++ - '0'; + if (isdigit(**str)) style->fg = style->fg * 10 + *(*str)++ - '0'; + if ((*str)[0] != ',' || !isdigit((*str)[1])) break; + (*str)++; + style->bg = *(*str)++ - '0'; + if (isdigit(**str)) style->bg = style->bg * 10 + *(*str)++ - '0'; + } + } + *len = strcspn(*str, "\2\3\17\26\35\37"); +} + +static void styleAdd(WINDOW *win, const char *str) { + size_t len; + struct Style style = Reset; + while (*str) { + styleParse(&style, &str, &len); + wattr_set( + win, + style.attr | colorAttr(mapColor(style.fg)), + colorPair(mapColor(style.fg), mapColor(style.bg)), + NULL + ); + waddnstr(win, str, len); + str += len; + } +} + void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) { (void)time; struct Window *window = windowFor(id); waddch(window->pad, '\n'); - waddstr(window->pad, str); + styleAdd(window->pad, str); } void uiFormat( -- cgit 1.4.1-2-gfad0 From 05256b68fef9d9b64b01afb60de31f9c47b60ca1 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 1 Feb 2020 22:40:55 -0500 Subject: Implement word wrap This actually wasn't that bad? --- chat.c | 14 +++++++++++++- ui.c | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/chat.c b/chat.c index 75cca62..ceaf1b5 100644 --- a/chat.c +++ b/chat.c @@ -76,7 +76,19 @@ int main(int argc, char *argv[]) { ircConfig(insecure, cert, priv); uiInit(); - uiFormat(Network, Cold, NULL, C "3Trave" U "ling" U C "0,3.." C "0,4."); + uiFormat(Network, Cold, NULL, C "3Trave" U "ling" U C "0,3.." C "0,4." R); + uiFormat( + Network, Cold, NULL, + "Jackdaws love my big sphinx of quartz. " + "The quick brown fox jumps over the lazy dog. " + "Jackdaws love my big sphinx of quartz. " + "Jackdaws love my big sphinx of quartz. " + "Jackdaws love my big sphinx of quartz. " + "The quick brown fox jumps over the lazy dog. " + "The quick brown fox jumps over the lazy dog. " + "Jackdaws love my big sphinx of quartz. " + "Jackdaws love my big sphinx of quartz. " + ); uiDraw(); int irc = ircConnect(host, port); diff --git a/ui.c b/ui.c index 83c4bc7..7ce0257 100644 --- a/ui.c +++ b/ui.c @@ -209,11 +209,33 @@ static void styleParse(struct Style *style, const char **str, size_t *len) { *len = strcspn(*str, "\2\3\17\26\35\37"); } +static int wordWidth(const char *str) { + size_t len = strcspn(str, " "); + // TODO: wcswidth. + return len; +} + static void styleAdd(WINDOW *win, const char *str) { + int _, x, width; + getmaxyx(win, _, width); + size_t len; struct Style style = Reset; while (*str) { + if (*str == ' ') { + const char *word = &str[strspn(str, " ")]; + getyx(win, _, x); + if (width - x - 1 < wordWidth(word)) { + waddch(win, '\n'); + str = word; + } + } + styleParse(&style, &str, &len); + size_t sp = strspn(str, " "); + sp += strcspn(&str[sp], " "); + if (sp < len) len = sp; + wattr_set( win, style.attr | colorAttr(mapColor(style.fg)), -- cgit 1.4.1-2-gfad0 From c799310d67b825f2aacf7b573f23991654d1e6c4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 01:54:51 -0500 Subject: Implement wordWidth --- ui.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ui.c b/ui.c index 7ce0257..7b1e339 100644 --- a/ui.c +++ b/ui.c @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include "chat.h" @@ -112,7 +114,6 @@ static struct Window *windowFor(size_t id) { if (!window) err(EX_OSERR, "malloc"); window->id = id; window->pad = newpad(PadLines, COLS); - wsetscrreg(window->pad, 0, PadLines - 1); scrollok(window->pad, true); wmove(window->pad, PadLines - 1, 0); window->heat = Cold; @@ -211,20 +212,28 @@ static void styleParse(struct Style *style, const char **str, size_t *len) { static int wordWidth(const char *str) { size_t len = strcspn(str, " "); - // TODO: wcswidth. - return len; + int width = 0; + while (len) { + wchar_t wc; + int n = mbtowc(&wc, str, len); + if (n < 1) return width + len; + width += (iswprint(wc) ? wcwidth(wc) : 0); + str += n; + len -= n; + } + return width; } static void styleAdd(WINDOW *win, const char *str) { - int _, x, width; - getmaxyx(win, _, width); + int y, x, width; + getmaxyx(win, y, width); size_t len; struct Style style = Reset; while (*str) { if (*str == ' ') { + getyx(win, y, x); const char *word = &str[strspn(str, " ")]; - getyx(win, _, x); if (width - x - 1 < wordWidth(word)) { waddch(win, '\n'); str = word; -- cgit 1.4.1-2-gfad0 From 8ef0af34ef1c62c52cfdbe3f440d6017b0feda6f Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 01:58:03 -0500 Subject: Parse time tag --- handle.c | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/handle.c b/handle.c index b2b2b8d..9157419 100644 --- a/handle.c +++ b/handle.c @@ -65,8 +65,6 @@ static void set(char **field, const char *value) { if (!*field) err(EX_OSERR, "strdup"); } -typedef void Handler(struct Message *msg); - static void require(const struct Message *msg, bool origin, size_t len) { if (origin && !msg->nick) { errx(EX_PROTOCOL, "%s missing origin", msg->cmd); @@ -77,6 +75,16 @@ static void require(const struct Message *msg, bool origin, size_t len) { } } +static const struct tm *tagTime(const struct Message *msg) { + if (!msg->tags[TagTime]) return NULL; + static struct tm time; + char *rest = strptime(msg->tags[TagTime], "%FT%T", &time); + time.tm_gmtoff = 0; + return (rest ? &time : NULL); +} + +typedef void Handler(struct Message *msg); + static void handleCap(struct Message *msg) { require(msg, false, 3); enum Cap caps = capParse(msg->params[2]); @@ -143,7 +151,10 @@ static void handleReplyISupport(struct Message *msg) { char *key = strsep(&msg->params[i], "="); if (!msg->params[i]) continue; if (!strcmp(key, "NETWORK")) { - uiFormat(Network, Cold, NULL, "You arrive in %s", msg->params[i]); + uiFormat( + Network, Cold, tagTime(msg), + "You arrive in %s", msg->params[i] + ); } } } @@ -152,7 +163,12 @@ static void handleReplyMOTD(struct Message *msg) { require(msg, false, 2); char *line = msg->params[1]; if (!strncmp(line, "- ", 2)) line += 2; - uiFormat(Network, Cold, NULL, "%s", line); + uiFormat(Network, Cold, tagTime(msg), "%s", line); +} + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + ircFormat("PONG :%s\r\n", msg->params[0]); } static const struct Handler { @@ -168,6 +184,7 @@ static const struct Handler { { "906", handleErrorSASLFail }, { "AUTHENTICATE", handleAuthenticate }, { "CAP", handleCap }, + { "PING", handlePing }, }; static int compar(const void *cmd, const void *_handler) { -- cgit 1.4.1-2-gfad0 From c18dc35377d88f331de15ea4c6e1ab41505d30df Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 02:04:08 -0500 Subject: Wrap before the very edge of the screen --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 7b1e339..11ee426 100644 --- a/ui.c +++ b/ui.c @@ -234,7 +234,7 @@ static void styleAdd(WINDOW *win, const char *str) { if (*str == ' ') { getyx(win, y, x); const char *word = &str[strspn(str, " ")]; - if (width - x - 1 < wordWidth(word)) { + if (width - x - 1 <= wordWidth(word)) { waddch(win, '\n'); str = word; } -- cgit 1.4.1-2-gfad0 From 09754ed91243f497065d888d19fc7c3c63ce19a9 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 02:28:14 -0500 Subject: Call reset_shell_mode on err This restores the terminal but doesn't clear the screen, so the error stays visible. --- ui.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui.c b/ui.c index 11ee426..90ba726 100644 --- a/ui.c +++ b/ui.c @@ -124,6 +124,11 @@ static struct Window *windowFor(size_t id) { return window; } +static void errExit(int eval) { + (void)eval; + reset_shell_mode(); +} + void uiInit(void) { initscr(); cbreak(); @@ -131,6 +136,7 @@ void uiInit(void) { termInit(); termNoFlow(); def_prog_mode(); + err_set_exit(errExit); colorInit(); status = newwin(1, COLS, 0, 0); input = newpad(1, InputCols); -- cgit 1.4.1-2-gfad0 From 14066b79d424561b0ab4be74574edf6fae422378 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 02:30:35 -0500 Subject: Handle nickname errors --- handle.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/handle.c b/handle.c index 9157419..350a636 100644 --- a/handle.c +++ b/handle.c @@ -85,6 +85,17 @@ static const struct tm *tagTime(const struct Message *msg) { typedef void Handler(struct Message *msg); +static void handleErrorNicknameInUse(struct Message *msg) { + if (self.nick) return; + require(msg, false, 2); + ircFormat("NICK :%s_\r\n", msg->params[1]); +} + +static void handleErrorErroneousNickname(struct Message *msg) { + require(msg, false, 3); + errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]); +} + static void handleCap(struct Message *msg) { require(msg, false, 3); enum Cap caps = capParse(msg->params[2]); @@ -178,6 +189,8 @@ static const struct Handler { { "001", handleReplyWelcome }, { "005", handleReplyISupport }, { "372", handleReplyMOTD }, + { "432", handleErrorErroneousNickname }, + { "433", handleErrorNicknameInUse }, { "900", handleReplyLoggedIn }, { "904", handleErrorSASLFail }, { "905", handleErrorSASLFail }, -- cgit 1.4.1-2-gfad0 From 3c824684e5f262c2b2fab3f8009b29382277d086 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 02:31:20 -0500 Subject: Add color hashing function --- chat.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chat.h b/chat.h index be3952c..fc5043d 100644 --- a/chat.h +++ b/chat.h @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -93,6 +94,16 @@ enum Color { Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, Default = 99, }; +static inline enum Color hash(const char *str) { + if (*str == '~') str++; + uint32_t hash = 0; + for (; *str; ++str) { + hash = (hash << 5) | (hash >> 27); + hash ^= *str; + hash *= 0x27220A95; + } + return 2 + hash % 14; +} void ircConfig(bool insecure, const char *cert, const char *priv); int ircConnect(const char *host, const char *port); -- cgit 1.4.1-2-gfad0 From 0728eb39a41f35b76cbccef6ef02a654e44c9164 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 02:35:47 -0500 Subject: Preserve copyright year in term.c --- term.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/term.c b/term.c index ade5392..bf4a933 100644 --- a/term.c +++ b/term.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe +/* Copyright (C) 2018, 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 @@ -17,6 +17,7 @@ #include #include #include +#include #include #include -- cgit 1.4.1-2-gfad0 From ec83332e15d31c1ffbb7112ff6743f2a5c815c71 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 03:13:50 -0500 Subject: Implement window switching and status line --- chat.c | 15 ++------------- chat.h | 1 + handle.c | 22 +++++++++++++++++++--- ui.c | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/chat.c b/chat.c index ceaf1b5..d4ed31c 100644 --- a/chat.c +++ b/chat.c @@ -76,19 +76,8 @@ int main(int argc, char *argv[]) { ircConfig(insecure, cert, priv); uiInit(); - uiFormat(Network, Cold, NULL, C "3Trave" U "ling" U C "0,3.." C "0,4." R); - uiFormat( - Network, Cold, NULL, - "Jackdaws love my big sphinx of quartz. " - "The quick brown fox jumps over the lazy dog. " - "Jackdaws love my big sphinx of quartz. " - "Jackdaws love my big sphinx of quartz. " - "Jackdaws love my big sphinx of quartz. " - "The quick brown fox jumps over the lazy dog. " - "The quick brown fox jumps over the lazy dog. " - "Jackdaws love my big sphinx of quartz. " - "Jackdaws love my big sphinx of quartz. " - ); + uiShowID(Network); + uiFormat(Network, Cold, NULL, "Traveling..."); uiDraw(); int irc = ircConnect(host, port); diff --git a/chat.h b/chat.h index fc5043d..9060f29 100644 --- a/chat.h +++ b/chat.h @@ -117,6 +117,7 @@ void handle(struct Message msg); enum Heat { Cold, Warm, Hot }; void uiInit(void); void uiDraw(void); +void uiShowID(size_t id); void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str); void uiFormat( size_t id, enum Heat heat, const struct tm *time, const char *format, ... diff --git a/handle.c b/handle.c index 350a636..609867e 100644 --- a/handle.c +++ b/handle.c @@ -65,9 +65,11 @@ static void set(char **field, const char *value) { if (!*field) err(EX_OSERR, "strdup"); } -static void require(const struct Message *msg, bool origin, size_t len) { - if (origin && !msg->nick) { - errx(EX_PROTOCOL, "%s missing origin", msg->cmd); +static void require(struct Message *msg, bool origin, size_t len) { + if (origin) { + if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + if (!msg->user) msg->user = msg->nick; + if (!msg->host) msg->host = msg->user; } for (size_t i = 0; i < len; ++i) { if (msg->params[i]) continue; @@ -177,6 +179,19 @@ static void handleReplyMOTD(struct Message *msg) { uiFormat(Network, Cold, tagTime(msg), "%s", line); } +static void handleJoin(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + uiShowID(id); + } + uiFormat( + id, Cold, tagTime(msg), + C"%02d%s"C" arrives in "C"%02d%s"C, + hash(msg->user), msg->nick, hash(idNames[id]), idNames[id] + ); +} + static void handlePing(struct Message *msg) { require(msg, false, 1); ircFormat("PONG :%s\r\n", msg->params[0]); @@ -197,6 +212,7 @@ static const struct Handler { { "906", handleErrorSASLFail }, { "AUTHENTICATE", handleAuthenticate }, { "CAP", handleCap }, + { "JOIN", handleJoin }, { "PING", handlePing }, }; diff --git a/ui.c b/ui.c index 90ba726..3ae6592 100644 --- a/ui.c +++ b/ui.c @@ -262,6 +262,40 @@ static void styleAdd(WINDOW *win, const char *str) { } } +static void statusUpdate(void) { + wmove(status, 0, 0); + int num; + const struct Window *window; + for (num = 0, window = windows.head; window; ++num, window = window->next) { + if (!window->unread && window != windows.active) continue; + enum Color color = hash(idNames[window->id]); // FIXME: queries. + int unread; + char buf[256]; + snprintf( + buf, sizeof(buf), C"%d%s %d %s %n("C"%02d%d"C"%d) ", + color, (window == windows.active ? V : ""), + num, idNames[window->id], + &unread, (window->heat > Warm ? White : color), window->unread, + color + ); + if (!window->unread) buf[unread] = '\0'; + styleAdd(status, buf); + } + wclrtoeol(status); +} + +void uiShowID(size_t id) { + struct Window *window = windowFor(id); + window->heat = Cold; + window->unread = 0; + window->mark = false; + if (windows.active) windows.active->mark = true; + windows.other = windows.active; + windows.active = window; + touchwin(window->pad); + statusUpdate(); +} + void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) { (void)time; struct Window *window = windowFor(id); -- cgit 1.4.1-2-gfad0 From 8bb9ea7b7ff2e98bbe629f9f2e63f1dcb70250e3 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 03:27:50 -0500 Subject: Add idColors --- chat.c | 7 +++++++ chat.h | 47 +++++++++++++++++++++++++---------------------- handle.c | 3 ++- ui.c | 8 ++++---- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/chat.c b/chat.c index d4ed31c..b61dd34 100644 --- a/chat.c +++ b/chat.c @@ -29,6 +29,13 @@ char *idNames[IDCap] = { [Debug] = "", [Network] = "", }; + +enum Color idColors[IDCap] = { + [None] = Black, + [Debug] = Red, + [Network] = Gray, +}; + size_t idNext = Network + 1; struct Self self; diff --git a/chat.h b/chat.h index 9060f29..4ced983 100644 --- a/chat.h +++ b/chat.h @@ -26,8 +26,21 @@ typedef unsigned char byte; +#define B "\2" +#define C "\3" +#define R "\17" +#define V "\26" +#define I "\35" +#define U "\37" +enum Color { + White, Black, Blue, Green, Red, Brown, Magenta, Orange, + Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, + Default = 99, +}; + enum { None, Debug, Network, IDCap = 256 }; extern char *idNames[IDCap]; +extern enum Color idColors[IDCap]; extern size_t idNext; static inline size_t idFind(const char *name) { @@ -36,6 +49,7 @@ static inline size_t idFind(const char *name) { } return None; } + static inline size_t idFor(const char *name) { size_t id = idFind(name); if (id) return id; @@ -83,28 +97,6 @@ struct Message { char *params[ParamCap]; }; -#define B "\2" -#define C "\3" -#define R "\17" -#define V "\26" -#define I "\35" -#define U "\37" -enum Color { - White, Black, Blue, Green, Red, Brown, Magenta, Orange, - Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, - Default = 99, -}; -static inline enum Color hash(const char *str) { - if (*str == '~') str++; - uint32_t hash = 0; - for (; *str; ++str) { - hash = (hash << 5) | (hash >> 27); - hash ^= *str; - hash *= 0x27220A95; - } - return 2 + hash % 14; -} - void ircConfig(bool insecure, const char *cert, const char *priv); int ircConnect(const char *host, const char *port); void ircRecv(void); @@ -140,6 +132,17 @@ void termTitle(const char *title); void termMode(enum TermMode mode, bool set); enum TermEvent termEvent(char ch); +static inline enum Color hash(const char *str) { + if (*str == '~') str++; + uint32_t hash = 0; + for (; *str; ++str) { + hash = (hash << 5) | (hash >> 27); + hash ^= *str; + hash *= 0x27220A95; + } + return 2 + hash % 14; +} + #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) static const char Base64[64] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" diff --git a/handle.c b/handle.c index 609867e..f52f6f9 100644 --- a/handle.c +++ b/handle.c @@ -183,12 +183,13 @@ static void handleJoin(struct Message *msg) { require(msg, true, 1); size_t id = idFor(msg->params[0]); if (self.nick && !strcmp(msg->nick, self.nick)) { + idColors[id] = hash(msg->params[0]); uiShowID(id); } uiFormat( id, Cold, tagTime(msg), C"%02d%s"C" arrives in "C"%02d%s"C, - hash(msg->user), msg->nick, hash(idNames[id]), idNames[id] + hash(msg->user), msg->nick, idColors[id], idNames[id] ); } diff --git a/ui.c b/ui.c index 3ae6592..961e448 100644 --- a/ui.c +++ b/ui.c @@ -268,15 +268,15 @@ static void statusUpdate(void) { const struct Window *window; for (num = 0, window = windows.head; window; ++num, window = window->next) { if (!window->unread && window != windows.active) continue; - enum Color color = hash(idNames[window->id]); // FIXME: queries. int unread; char buf[256]; snprintf( buf, sizeof(buf), C"%d%s %d %s %n("C"%02d%d"C"%d) ", - color, (window == windows.active ? V : ""), + idColors[window->id], (window == windows.active ? V : ""), num, idNames[window->id], - &unread, (window->heat > Warm ? White : color), window->unread, - color + &unread, (window->heat > Warm ? White : idColors[window->id]), + window->unread, + idColors[window->id] ); if (!window->unread) buf[unread] = '\0'; styleAdd(status, buf); -- cgit 1.4.1-2-gfad0 From 052cd2ed2688867f8b980d283ea3aa410d9dd6aa Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 03:34:05 -0500 Subject: Remove style string macros --- chat.h | 6 ------ handle.c | 2 +- irc.c | 2 +- ui.c | 4 ++-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/chat.h b/chat.h index 4ced983..275fef9 100644 --- a/chat.h +++ b/chat.h @@ -26,12 +26,6 @@ typedef unsigned char byte; -#define B "\2" -#define C "\3" -#define R "\17" -#define V "\26" -#define I "\35" -#define U "\37" enum Color { White, Black, Blue, Green, Red, Brown, Magenta, Orange, Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, diff --git a/handle.c b/handle.c index f52f6f9..ef61129 100644 --- a/handle.c +++ b/handle.c @@ -188,7 +188,7 @@ static void handleJoin(struct Message *msg) { } uiFormat( id, Cold, tagTime(msg), - C"%02d%s"C" arrives in "C"%02d%s"C, + "\3%02d%s\3 arrives in \3%02d%s\3", hash(msg->user), msg->nick, idColors[id], idNames[id] ); } diff --git a/irc.c b/irc.c index f07fcc8..d8c6a21 100644 --- a/irc.c +++ b/irc.c @@ -105,7 +105,7 @@ static void debug(char dir, const char *line) { if (!self.debug) return; size_t len = strcspn(line, "\r\n"); uiFormat( - Debug, Cold, NULL, C"%d%c%c"C" %.*s", + Debug, Cold, NULL, "\3%d%c%c\3 %.*s", Gray, dir, dir, (int)len, line ); if (!isatty(STDERR_FILENO)) { diff --git a/ui.c b/ui.c index 961e448..3f74e14 100644 --- a/ui.c +++ b/ui.c @@ -271,8 +271,8 @@ static void statusUpdate(void) { int unread; char buf[256]; snprintf( - buf, sizeof(buf), C"%d%s %d %s %n("C"%02d%d"C"%d) ", - idColors[window->id], (window == windows.active ? V : ""), + buf, sizeof(buf), "\3%d%s %d %s %n(\3%02d%d\3%d) ", + idColors[window->id], (window == windows.active ? "\26" : ""), num, idNames[window->id], &unread, (window->heat > Warm ? White : idColors[window->id]), window->unread, -- cgit 1.4.1-2-gfad0 From dce7891331fcf3b86095b64bea8853942dfd667c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 03:43:18 -0500 Subject: Add extremely basid handlePrivmsg --- handle.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/handle.c b/handle.c index ef61129..da635b4 100644 --- a/handle.c +++ b/handle.c @@ -193,6 +193,18 @@ static void handleJoin(struct Message *msg) { ); } +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + bool query = self.nick && !strcmp(msg->params[0], self.nick); + size_t id = idFor(query ? msg->nick : msg->params[0]); + if (query) idColors[id] = hash(msg->user); + uiFormat( + id, Warm, tagTime(msg), + "\3%d<%s>\3 %s", + hash(msg->user), msg->nick, msg->params[1] + ); +} + static void handlePing(struct Message *msg) { require(msg, false, 1); ircFormat("PONG :%s\r\n", msg->params[0]); @@ -215,6 +227,7 @@ static const struct Handler { { "CAP", handleCap }, { "JOIN", handleJoin }, { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, }; static int compar(const void *cmd, const void *_handler) { -- cgit 1.4.1-2-gfad0 From e8d0d71775e2a9602a233682e30246d7b7651e55 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 16:55:45 -0500 Subject: Add option to show style codes This will be used for the input window. --- ui.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ui.c b/ui.c index 3f74e14..2445eb1 100644 --- a/ui.c +++ b/ui.c @@ -193,7 +193,7 @@ static short mapColor(enum Color color) { static void styleParse(struct Style *style, const char **str, size_t *len) { switch (**str) { - break; case '\2': (*str)++; style->attr ^= A_BOLD; + 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; @@ -230,7 +230,7 @@ static int wordWidth(const char *str) { return width; } -static void styleAdd(WINDOW *win, const char *str) { +static void styleAdd(WINDOW *win, const char *str, bool show) { int y, x, width; getmaxyx(win, y, width); @@ -246,7 +246,21 @@ static void styleAdd(WINDOW *win, const char *str) { } } + const char *code = str; styleParse(&style, &str, &len); + if (show) { + wattr_set(win, A_BOLD | A_REVERSE, 0, NULL); + switch (*code) { + break; case '\2': waddch(win, 'B'); + break; case '\3': waddch(win, 'C'); + break; case '\17': waddch(win, 'O'); + break; case '\26': waddch(win, 'R'); + break; case '\35': waddch(win, 'I'); + break; case '\37': waddch(win, 'U'); + } + if (str - code > 1) waddnstr(win, &code[1], str - &code[1]); + } + size_t sp = strspn(str, " "); sp += strcspn(&str[sp], " "); if (sp < len) len = sp; @@ -279,7 +293,7 @@ static void statusUpdate(void) { idColors[window->id] ); if (!window->unread) buf[unread] = '\0'; - styleAdd(status, buf); + styleAdd(status, buf, true); } wclrtoeol(status); } @@ -300,7 +314,7 @@ void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) (void)time; struct Window *window = windowFor(id); waddch(window->pad, '\n'); - styleAdd(window->pad, str); + styleAdd(window->pad, str, true); } void uiFormat( -- cgit 1.4.1-2-gfad0 From b535f0abdde6fb79f9f972d0b39c8b0a7a837339 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 17:26:20 -0500 Subject: Handle notices and actions --- handle.c | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/handle.c b/handle.c index da635b4..2af5837 100644 --- a/handle.c +++ b/handle.c @@ -193,15 +193,31 @@ static void handleJoin(struct Message *msg) { ); } +static bool isAction(struct Message *msg) { + if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; + msg->params[1] += 8; + size_t len = strlen(msg->params[1]); + if (msg->params[1][len - 1] == '\1') msg->params[1][len - 1] = '\0'; + return true; +} + static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); - bool query = self.nick && !strcmp(msg->params[0], self.nick); - size_t id = idFor(query ? msg->nick : msg->params[0]); - if (query) idColors[id] = hash(msg->user); + bool query = msg->params[0][0] != '#'; // FIXME: CHANTYPES. + bool network = query && strchr(msg->nick, '.'); + bool notice = (msg->cmd[0] == 'N'); + bool action = isAction(msg); + // TODO: Send services to Network? + size_t id = (network ? Network : idFor(query ? msg->nick : msg->params[0])); + if (query && !network) idColors[id] = hash(msg->user); uiFormat( id, Warm, tagTime(msg), - "\3%d<%s>\3 %s", - hash(msg->user), msg->nick, msg->params[1] + "\3%d%s%s%s\3 %s", + hash(msg->user), + (action ? "* " : notice ? "-" : "<"), + msg->nick, + (action ? "" : notice ? "-" : ">"), + msg->params[1] ); } @@ -226,6 +242,7 @@ static const struct Handler { { "AUTHENTICATE", handleAuthenticate }, { "CAP", handleCap }, { "JOIN", handleJoin }, + { "NOTICE", handlePrivmsg }, { "PING", handlePing }, { "PRIVMSG", handlePrivmsg }, }; -- cgit 1.4.1-2-gfad0 From 0d6a60cc6634cafe03671e9d5a1a64295c98bb9d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 17:37:36 -0500 Subject: Save NETWORK, CHANTYPES, PREFIX from ISUPPORT --- chat.c | 4 ++++ chat.h | 11 ++++++++++- handle.c | 14 +++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/chat.c b/chat.c index b61dd34..162f68f 100644 --- a/chat.c +++ b/chat.c @@ -80,6 +80,10 @@ int main(int argc, char *argv[]) { if (!user) user = nick; if (!real) real = nick; + set(&self.network, host); + set(&self.chanTypes, "#&"); + set(&self.prefixes, "@+"); + ircConfig(insecure, cert, priv); uiInit(); diff --git a/chat.h b/chat.h index 275fef9..f9de779 100644 --- a/chat.h +++ b/chat.h @@ -65,12 +65,21 @@ enum Cap { extern struct Self { bool debug; + char *plain; const char *join; enum Cap caps; - char *plain; + char *network; + char *chanTypes; + char *prefixes; char *nick; } self; +static inline void set(char **field, const char *value) { + free(*field); + *field = strdup(value); + if (!*field) err(EX_OSERR, "strdup"); +} + #define ENUM_TAG \ X("time", TagTime) diff --git a/handle.c b/handle.c index 2af5837..2766cc8 100644 --- a/handle.c +++ b/handle.c @@ -59,12 +59,6 @@ static const char *capList(enum Cap caps) { return buf; } -static void set(char **field, const char *value) { - free(*field); - *field = strdup(value); - if (!*field) err(EX_OSERR, "strdup"); -} - static void require(struct Message *msg, bool origin, size_t len) { if (origin) { if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); @@ -158,16 +152,22 @@ static void handleReplyWelcome(struct Message *msg) { } static void handleReplyISupport(struct Message *msg) { - // TODO: Extract CHANTYPES and PREFIX for future use. for (size_t i = 1; i < ParamCap; ++i) { if (!msg->params[i]) break; char *key = strsep(&msg->params[i], "="); if (!msg->params[i]) continue; if (!strcmp(key, "NETWORK")) { + set(&self.network, msg->params[i]); uiFormat( Network, Cold, tagTime(msg), "You arrive in %s", msg->params[i] ); + } else if (!strcmp(key, "CHANTYPES")) { + set(&self.chanTypes, msg->params[i]); + } else if (!strcmp(key, "PREFIX")) { + strsep(&msg->params[i], ")"); + if (!msg->params[i]) continue; + set(&self.prefixes, msg->params[i]); } } } -- cgit 1.4.1-2-gfad0 From aec28a9327c963ffda0a438107b9442bc90b84e4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 17:45:19 -0500 Subject: Check queries against chanTypes --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 2766cc8..688dcdb 100644 --- a/handle.c +++ b/handle.c @@ -203,7 +203,7 @@ static bool isAction(struct Message *msg) { static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); - bool query = msg->params[0][0] != '#'; // FIXME: CHANTYPES. + bool query = !strchr(self.chanTypes, msg->params[0][0]); bool network = query && strchr(msg->nick, '.'); bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); -- cgit 1.4.1-2-gfad0 From 2f9a9c663a577e7087c1160a0cd651f706e4921b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 17:50:28 -0500 Subject: Remove services TODO Two goals: 1. Messages should always be routed to the same place. 2. You should be able to see your messages to *Serv and its responses together. --- handle.c | 1 - 1 file changed, 1 deletion(-) diff --git a/handle.c b/handle.c index 688dcdb..4bc2e3d 100644 --- a/handle.c +++ b/handle.c @@ -207,7 +207,6 @@ static void handlePrivmsg(struct Message *msg) { bool network = query && strchr(msg->nick, '.'); bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); - // TODO: Send services to Network? size_t id = (network ? Network : idFor(query ? msg->nick : msg->params[0])); if (query && !network) idColors[id] = hash(msg->user); uiFormat( -- cgit 1.4.1-2-gfad0 From a507ff40732b1f6fe9c5dbc1f1f17ef00bcccf5d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 17:57:07 -0500 Subject: Set title in statusUpdate --- ui.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ui.c b/ui.c index 2445eb1..2d5e454 100644 --- a/ui.c +++ b/ui.c @@ -296,6 +296,16 @@ static void statusUpdate(void) { styleAdd(status, buf, true); } wclrtoeol(status); + + int unread; + char buf[256]; + snprintf( + buf, sizeof(buf), "%s %s%n (%d)", + self.network, idNames[windows.active->id], + &unread, windows.active->unread + ); + if (!windows.active->unread) buf[unread] = '\0'; + termTitle(buf); } void uiShowID(size_t id) { -- cgit 1.4.1-2-gfad0 From 8ec17d4f8ce2edab30d998d6b279f0e5cd840022 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 18:39:08 -0500 Subject: Use tsl/fsl capabilities for title if available Also manually fill them if TERM=xterm* because they really should be there. --- chat.h | 2 -- term.c | 14 -------------- ui.c | 11 +++++++++-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/chat.h b/chat.h index f9de779..8a806f1 100644 --- a/chat.h +++ b/chat.h @@ -129,9 +129,7 @@ enum TermEvent { TermPasteStart, TermPasteEnd, }; -void termInit(void); void termNoFlow(void); -void termTitle(const char *title); void termMode(enum TermMode mode, bool set); enum TermEvent termEvent(char ch); diff --git a/term.c b/term.c index bf4a933..427cac6 100644 --- a/term.c +++ b/term.c @@ -17,19 +17,11 @@ #include #include #include -#include #include #include #include "chat.h" -static bool xterm; - -void termInit(void) { - const char *term = getenv("TERM"); - xterm = (term && !strncmp(term, "xterm", 5)); -} - void termNoFlow(void) { struct termios attr; int error = tcgetattr(STDIN_FILENO, &attr); @@ -39,12 +31,6 @@ void termNoFlow(void) { tcsetattr(STDIN_FILENO, TCSANOW, &attr); } -void termTitle(const char *title) { - if (!xterm) return; - printf("\33]0;%s\33\\", title); - fflush(stdout); -} - static void privateMode(const char *mode, bool set) { printf("\33[?%s%c", mode, (set ? 'h' : 'l')); fflush(stdout); diff --git a/ui.c b/ui.c index 2d5e454..f434289 100644 --- a/ui.c +++ b/ui.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -133,11 +134,15 @@ void uiInit(void) { initscr(); cbreak(); noecho(); - termInit(); termNoFlow(); def_prog_mode(); err_set_exit(errExit); colorInit(); + if (!to_status_line && !strncmp(termname(), "xterm", 5)) { + to_status_line = "\33]2;"; + from_status_line = "\7"; + } + status = newwin(1, COLS, 0, 0); input = newpad(1, InputCols); keypad(input, true); @@ -305,7 +310,9 @@ static void statusUpdate(void) { &unread, windows.active->unread ); if (!windows.active->unread) buf[unread] = '\0'; - termTitle(buf); + putp(to_status_line); + putp(buf); + putp(from_status_line); } void uiShowID(size_t id) { -- cgit 1.4.1-2-gfad0 From 5c328c7a8801d6a4aded769092ead9715d4ecf98 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 19:34:35 -0500 Subject: Remove term.c in favor of more curses APIs --- Makefile | 1 - chat.h | 15 --------------- term.c | 66 ---------------------------------------------------------------- ui.c | 28 +++++++++++++++++++++++++-- 4 files changed, 26 insertions(+), 84 deletions(-) delete mode 100644 term.c diff --git a/Makefile b/Makefile index ce27d4e..6ba0ba5 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,6 @@ LDLIBS = -lcurses -lcrypto -ltls OBJS += chat.o OBJS += handle.o OBJS += irc.o -OBJS += term.o OBJS += ui.o dev: tags all diff --git a/chat.h b/chat.h index 8a806f1..43f62fd 100644 --- a/chat.h +++ b/chat.h @@ -118,21 +118,6 @@ void uiFormat( size_t id, enum Heat heat, const struct tm *time, const char *format, ... ) __attribute__((format(printf, 4, 5))); -enum TermMode { - TermFocus, - TermPaste, -}; -enum TermEvent { - TermNone, - TermFocusIn, - TermFocusOut, - TermPasteStart, - TermPasteEnd, -}; -void termNoFlow(void); -void termMode(enum TermMode mode, bool set); -enum TermEvent termEvent(char ch); - static inline enum Color hash(const char *str) { if (*str == '~') str++; uint32_t hash = 0; diff --git a/term.c b/term.c deleted file mode 100644 index 427cac6..0000000 --- a/term.c +++ /dev/null @@ -1,66 +0,0 @@ -/* Copyright (C) 2018, 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 "chat.h" - -void termNoFlow(void) { - struct termios attr; - int error = tcgetattr(STDIN_FILENO, &attr); - if (error) return; - attr.c_iflag &= ~IXON; - attr.c_cc[VDISCARD] = _POSIX_VDISABLE; - tcsetattr(STDIN_FILENO, TCSANOW, &attr); -} - -static void privateMode(const char *mode, bool set) { - printf("\33[?%s%c", mode, (set ? 'h' : 'l')); - fflush(stdout); -} - -void termMode(enum TermMode mode, bool set) { - switch (mode) { - break; case TermFocus: privateMode("1004", set); - break; case TermPaste: privateMode("2004", set); - } -} - -enum { Esc = '\33' }; - -enum TermEvent termEvent(char ch) { - static int st; -#define T(st, ch) ((st) << 8 | (ch)) - switch (T(st, ch)) { - break; case T(0, Esc): st = 1; - break; case T(1, '['): st = 2; - break; case T(2, 'I'): st = 0; return TermFocusIn; - break; case T(2, 'O'): st = 0; return TermFocusOut; - break; case T(2, '2'): st = 3; - break; case T(3, '0'): st = 4; - break; case T(4, '0'): st = 5; - break; case T(5, '~'): st = 0; return TermPasteStart; - break; case T(4, '1'): st = 6; - break; case T(6, '~'): st = 0; return TermPasteEnd; - break; default: st = 0; - } - return 0; -#undef T -} diff --git a/ui.c b/ui.c index f434289..5d626ce 100644 --- a/ui.c +++ b/ui.c @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #include #include @@ -125,6 +127,23 @@ static struct Window *windowFor(size_t id) { return window; } +enum { + KeyFocusIn = KEY_MAX + 1, + KeyFocusOut, + KeyPasteOn, + KeyPasteOff, +}; + +static void disableFlowControl(void) { + struct termios term; + int error = tcgetattr(STDOUT_FILENO, &term); + if (error) err(EX_OSERR, "tcgetattr"); + term.c_iflag &= ~IXON; + term.c_cc[VDISCARD] = _POSIX_VDISABLE; + error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term); + if (error) err(EX_OSERR, "tcsetattr"); +} + static void errExit(int eval) { (void)eval; reset_shell_mode(); @@ -134,15 +153,20 @@ void uiInit(void) { initscr(); cbreak(); noecho(); - termNoFlow(); + disableFlowControl(); def_prog_mode(); err_set_exit(errExit); - colorInit(); + if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; from_status_line = "\7"; } + define_key("\33[I", KeyFocusIn); + define_key("\33[O", KeyFocusOut); + define_key("\33[200~", KeyPasteOn); + define_key("\33[201~", KeyPasteOff); + colorInit(); status = newwin(1, COLS, 0, 0); input = newpad(1, InputCols); keypad(input, true); -- cgit 1.4.1-2-gfad0 From 5398a6ac9d31916ec1a399813032797988e308d2 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 19:38:37 -0500 Subject: Rearrange some UI code --- ui.c | 58 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/ui.c b/ui.c index 5d626ce..8bc3eae 100644 --- a/ui.c +++ b/ui.c @@ -41,32 +41,6 @@ #define RIGHT (COLS - 1) #define WINDOW_LINES (LINES - 2) -static short colorPairs; - -static void colorInit(void) { - start_color(); - use_default_colors(); - for (short pair = 0; pair < 16; ++pair) { - init_pair(1 + pair, pair % COLORS, -1); - } - colorPairs = 17; -} - -static attr_t colorAttr(short fg) { - return (fg >= COLORS ? A_BOLD : A_NORMAL); -} - -static short colorPair(short fg, short bg) { - if (bg == -1) return 1 + fg; - for (short pair = 17; pair < colorPairs; ++pair) { - short f, b; - pair_content(pair, &f, &b); - if (f == fg && b == bg) return pair; - } - init_pair(colorPairs, fg % COLORS, bg % COLORS); - return colorPairs++; -} - enum { InputCols = 512, PadLines = 512, @@ -115,18 +89,46 @@ static struct Window *windowFor(size_t id) { } window = malloc(sizeof(*window)); if (!window) err(EX_OSERR, "malloc"); + window->id = id; window->pad = newpad(PadLines, COLS); - scrollok(window->pad, true); - wmove(window->pad, PadLines - 1, 0); window->heat = Cold; window->unread = 0; window->scroll = PadLines; window->mark = true; + scrollok(window->pad, true); + wmove(window->pad, PadLines - 1, 0); + windowAdd(window); return window; } +static short colorPairs; + +static void colorInit(void) { + start_color(); + use_default_colors(); + for (short pair = 0; pair < 16; ++pair) { + init_pair(1 + pair, pair % COLORS, -1); + } + colorPairs = 17; +} + +static attr_t colorAttr(short fg) { + return (fg >= COLORS ? A_BOLD : A_NORMAL); +} + +static short colorPair(short fg, short bg) { + if (bg == -1) return 1 + fg; + for (short pair = 17; pair < colorPairs; ++pair) { + short f, b; + pair_content(pair, &f, &b); + if (f == fg && b == bg) return pair; + } + init_pair(colorPairs, fg % COLORS, bg % COLORS); + return colorPairs++; +} + enum { KeyFocusIn = KEY_MAX + 1, KeyFocusOut, -- cgit 1.4.1-2-gfad0 From 81ac0c59f3ce68053d83462577bae7e57c21cc36 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 20:23:36 -0500 Subject: Track unread and window heat --- ui.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 8bc3eae..b9aadec 100644 --- a/ui.c +++ b/ui.c @@ -324,7 +324,7 @@ static void statusUpdate(void) { idColors[window->id] ); if (!window->unread) buf[unread] = '\0'; - styleAdd(status, buf, true); + styleAdd(status, buf, false); } wclrtoeol(status); @@ -342,11 +342,11 @@ static void statusUpdate(void) { } void uiShowID(size_t id) { + windows.active->mark = true; struct Window *window = windowFor(id); window->heat = Cold; window->unread = 0; window->mark = false; - if (windows.active) windows.active->mark = true; windows.other = windows.active; windows.active = window; touchwin(window->pad); @@ -357,7 +357,14 @@ void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) (void)time; struct Window *window = windowFor(id); waddch(window->pad, '\n'); - styleAdd(window->pad, str, true); + if (window->mark && heat > Cold) { + if (!window->unread++) { + waddch(window->pad, '\n'); + } + window->heat = heat; + statusUpdate(); + } + styleAdd(window->pad, str, false); } void uiFormat( -- cgit 1.4.1-2-gfad0 From c9470b59a151f639e7985ca545bd67182e7a88d8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 2 Feb 2020 23:20:19 -0500 Subject: Add sequences for toggling focus/paste modes --- chat.h | 2 ++ ui.c | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/chat.h b/chat.h index 43f62fd..76d69c9 100644 --- a/chat.h +++ b/chat.h @@ -111,6 +111,8 @@ void handle(struct Message msg); enum Heat { Cold, Warm, Hot }; void uiInit(void); +void uiShow(void); +void uiHide(void); void uiDraw(void); void uiShowID(size_t id); void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str); diff --git a/ui.c b/ui.c index b9aadec..072ee84 100644 --- a/ui.c +++ b/ui.c @@ -136,6 +136,24 @@ enum { KeyPasteOff, }; +// XXX: Assuming terminals will be fine with these even if they're unsupported, +// since they're "private" modes. +static const char *EnterFocusMode = "\33[?1004h"; +static const char *ExitFocusMode = "\33[?1004l"; +static const char *EnterPasteMode = "\33[?2004h"; +static const char *ExitPasteMode = "\33[?2004l"; + +void uiShow(void) { + putp(EnterFocusMode); + putp(EnterPasteMode); +} + +void uiHide(void) { + putp(ExitFocusMode); + putp(ExitPasteMode); + endwin(); +} + static void disableFlowControl(void) { struct termios term; int error = tcgetattr(STDOUT_FILENO, &term); @@ -174,6 +192,7 @@ void uiInit(void) { keypad(input, true); nodelay(input, true); windows.active = windowFor(Network); + //uiShow(); } void uiDraw(void) { -- cgit 1.4.1-2-gfad0 From 26e9dd9adfd4df90cd4cc6ef14d91cdad2efb239 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 3 Feb 2020 18:41:52 -0500 Subject: Use time_t rather than struct tm --- chat.h | 4 ++-- handle.c | 11 ++++++----- ui.c | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/chat.h b/chat.h index 76d69c9..9165c13 100644 --- a/chat.h +++ b/chat.h @@ -115,9 +115,9 @@ void uiShow(void); void uiHide(void); void uiDraw(void); void uiShowID(size_t id); -void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str); +void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); void uiFormat( - size_t id, enum Heat heat, const struct tm *time, const char *format, ... + size_t id, enum Heat heat, const time_t *time, const char *format, ... ) __attribute__((format(printf, 4, 5))); static inline enum Color hash(const char *str) { diff --git a/handle.c b/handle.c index 4bc2e3d..ef49f7c 100644 --- a/handle.c +++ b/handle.c @@ -71,12 +71,13 @@ static void require(struct Message *msg, bool origin, size_t len) { } } -static const struct tm *tagTime(const struct Message *msg) { +static const time_t *tagTime(const struct Message *msg) { + static time_t time; + struct tm tm; if (!msg->tags[TagTime]) return NULL; - static struct tm time; - char *rest = strptime(msg->tags[TagTime], "%FT%T", &time); - time.tm_gmtoff = 0; - return (rest ? &time : NULL); + if (!strptime(msg->tags[TagTime], "%FT%T", &tm)) return NULL; + time = timegm(&tm); + return &time; } typedef void Handler(struct Message *msg); diff --git a/ui.c b/ui.c index 072ee84..e2746f1 100644 --- a/ui.c +++ b/ui.c @@ -372,7 +372,7 @@ void uiShowID(size_t id) { statusUpdate(); } -void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) { +void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { (void)time; struct Window *window = windowFor(id); waddch(window->pad, '\n'); @@ -387,7 +387,7 @@ void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) } void uiFormat( - size_t id, enum Heat heat, const struct tm *time, const char *format, ... + size_t id, enum Heat heat, const time_t *time, const char *format, ... ) { char buf[1024]; va_list ap; -- cgit 1.4.1-2-gfad0 From 43845c61156bf27955891d68c2e1a2504786b587 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 03:58:56 -0500 Subject: Add beginnings of input handling --- chat.c | 12 +++++++++++- chat.h | 1 + ui.c | 45 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/chat.c b/chat.c index 162f68f..3402621 100644 --- a/chat.c +++ b/chat.c @@ -15,7 +15,9 @@ */ #include +#include #include +#include #include #include #include @@ -98,8 +100,16 @@ int main(int argc, char *argv[]) { ircFormat("NICK :%s\r\n", nick); ircFormat("USER %s 0 * :%s\r\n", user, real); + struct pollfd fds[2] = { + { .events = POLLIN, .fd = STDIN_FILENO }, + { .events = POLLIN, .fd = irc }, + }; for (;;) { + int nfds = poll(fds, 2, -1); + if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); + + if (fds[0].revents) uiRead(); + if (fds[1].revents) ircRecv(); uiDraw(); - ircRecv(); } } diff --git a/chat.h b/chat.h index 9165c13..8de5df1 100644 --- a/chat.h +++ b/chat.h @@ -115,6 +115,7 @@ void uiShow(void); void uiHide(void); void uiDraw(void); void uiShowID(size_t id); +void uiRead(void); 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, ... diff --git a/ui.c b/ui.c index e2746f1..d69e706 100644 --- a/ui.c +++ b/ui.c @@ -14,6 +14,8 @@ * along with this program. If not, see . */ +#define _XOPEN_SOURCE_EXTENDED + #include #include #include @@ -192,7 +194,7 @@ void uiInit(void) { keypad(input, true); nodelay(input, true); windows.active = windowFor(Network); - //uiShow(); + uiShow(); } void uiDraw(void) { @@ -397,3 +399,44 @@ void uiFormat( assert((size_t)len < sizeof(buf)); uiWrite(id, heat, time, buf); } + +static void keyCode(int code) { + switch (code) { + break; case KEY_RESIZE:; // TODO + break; case KeyFocusIn:; // TODO + break; case KeyFocusOut: windows.active->mark = true; + break; case KeyPasteOn:; // TODO + break; case KeyPasteOff:; // TODO + } +} + +static void keyMeta(wchar_t ch) { + switch (ch) { + break; case L'm': uiWrite(windows.active->id, Cold, NULL, ""); + } +} + +static void keyChar(wchar_t ch) { + switch (ch) { + break; case CTRL(L'L'): clearok(curscr, true); + } +} + +void uiRead(void) { + int ret; + wint_t ch; + static bool meta; + while (ERR != (ret = wget_wch(input, &ch))) { + if (ret == KEY_CODE_YES) { + keyCode(ch); + } else if (ch == '\33') { + meta = true; + continue; + } else if (meta) { + keyMeta(ch); + } else { + keyChar(ch); + } + meta = false; + } +} -- cgit 1.4.1-2-gfad0 From d57e7868760e9962a4973a4b64f88dfe3cf4e363 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 04:09:54 -0500 Subject: Factor out unmark --- ui.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ui.c b/ui.c index d69e706..6a834cd 100644 --- a/ui.c +++ b/ui.c @@ -362,16 +362,20 @@ static void statusUpdate(void) { putp(from_status_line); } +static void unmark(void) { + windows.active->heat = Cold; + windows.active->unread = 0; + windows.active->mark = false; + statusUpdate(); +} + void uiShowID(size_t id) { - windows.active->mark = true; struct Window *window = windowFor(id); - window->heat = Cold; - window->unread = 0; - window->mark = false; + touchwin(window->pad); windows.other = windows.active; windows.active = window; - touchwin(window->pad); - statusUpdate(); + windows.other->mark = true; + unmark(); } void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { @@ -403,7 +407,7 @@ void uiFormat( static void keyCode(int code) { switch (code) { break; case KEY_RESIZE:; // TODO - break; case KeyFocusIn:; // TODO + break; case KeyFocusIn: unmark(); break; case KeyFocusOut: windows.active->mark = true; break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO -- cgit 1.4.1-2-gfad0 From a65841c3cb2f367448528242b187c699cb97e0a4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 04:41:11 -0500 Subject: Switch windows with M-0 through M-9 --- ui.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ui.c b/ui.c index 6a834cd..0c2a64e 100644 --- a/ui.c +++ b/ui.c @@ -369,8 +369,7 @@ static void unmark(void) { statusUpdate(); } -void uiShowID(size_t id) { - struct Window *window = windowFor(id); +static void windowShow(struct Window *window) { touchwin(window->pad); windows.other = windows.active; windows.active = window; @@ -378,6 +377,19 @@ void uiShowID(size_t id) { unmark(); } +void uiShowID(size_t id) { + windowShow(windowFor(id)); +} + +void uiShowNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowShow(window); +} + void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { (void)time; struct Window *window = windowFor(id); @@ -417,6 +429,9 @@ static void keyCode(int code) { static void keyMeta(wchar_t ch) { switch (ch) { break; case L'm': uiWrite(windows.active->id, Cold, NULL, ""); + break; default: { + if (ch >= L'0' && ch <= L'9') uiShowNum(ch - L'0'); + } } } -- cgit 1.4.1-2-gfad0 From 5e9863fa82f674ad8eb05148eade5c859a32c7ba Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 19:02:54 -0500 Subject: Handle signals in poll loop --- chat.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/chat.c b/chat.c index 3402621..3aa4ad2 100644 --- a/chat.c +++ b/chat.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,7 @@ char *idNames[IDCap] = { enum Color idColors[IDCap] = { [None] = Black, - [Debug] = Red, + [Debug] = Green, [Network] = Gray, }; @@ -42,6 +43,11 @@ size_t idNext = Network + 1; struct Self self; +static volatile sig_atomic_t signals[NSIG]; +static void signalHandler(int signal) { + signals[signal] = 1; +} + int main(int argc, char *argv[]) { setlocale(LC_CTYPE, ""); @@ -100,6 +106,11 @@ int main(int argc, char *argv[]) { ircFormat("NICK :%s\r\n", nick); ircFormat("USER %s 0 * :%s\r\n", user, real); + signal(SIGHUP, signalHandler); + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + sig_t cursesWinch = signal(SIGWINCH, signalHandler); + struct pollfd fds[2] = { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, @@ -108,8 +119,20 @@ int main(int argc, char *argv[]) { int nfds = poll(fds, 2, -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); + if (signals[SIGHUP] || signals[SIGINT] || signals[SIGTERM]) { + break; + } + if (signals[SIGWINCH]) { + signals[SIGWINCH] = 0; + cursesWinch(SIGWINCH); + fds[0].revents = POLLIN; + } + if (fds[0].revents) uiRead(); if (fds[1].revents) ircRecv(); uiDraw(); } + + ircFormat("QUIT\r\n"); + uiHide(); } -- cgit 1.4.1-2-gfad0 From f3fb466a31d78431a686981b7d9b718385591bce Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 19:06:54 -0500 Subject: Only check revents if nfds > 0 If an error occurs, poll leaves the array unmodified. --- chat.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat.c b/chat.c index 3aa4ad2..1656a53 100644 --- a/chat.c +++ b/chat.c @@ -125,11 +125,11 @@ int main(int argc, char *argv[]) { if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); - fds[0].revents = POLLIN; + uiRead(); } - if (fds[0].revents) uiRead(); - if (fds[1].revents) ircRecv(); + if (nfds > 0 && fds[0].revents) uiRead(); + if (nfds > 0 && fds[1].revents) ircRecv(); uiDraw(); } -- cgit 1.4.1-2-gfad0 From d57df09511a5e4136559ccdd01ab56e906827f96 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 19:50:23 -0500 Subject: Align word wrapping with tab character Also fixes handling whitespace directly after control codes. --- handle.c | 2 +- irc.c | 2 +- ui.c | 17 +++++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/handle.c b/handle.c index ef49f7c..29d1500 100644 --- a/handle.c +++ b/handle.c @@ -212,7 +212,7 @@ static void handlePrivmsg(struct Message *msg) { if (query && !network) idColors[id] = hash(msg->user); uiFormat( id, Warm, tagTime(msg), - "\3%d%s%s%s\3 %s", + "\3%d%s%s%s\3\t%s", hash(msg->user), (action ? "* " : notice ? "-" : "<"), msg->nick, diff --git a/irc.c b/irc.c index d8c6a21..2d6f00b 100644 --- a/irc.c +++ b/irc.c @@ -105,7 +105,7 @@ static void debug(char dir, const char *line) { if (!self.debug) return; size_t len = strcspn(line, "\r\n"); uiFormat( - Debug, Cold, NULL, "\3%d%c%c\3 %.*s", + Debug, Cold, NULL, "\3%d%c%c\3\t%.*s", Gray, dir, dir, (int)len, line ); if (!isatty(STDERR_FILENO)) { diff --git a/ui.c b/ui.c index 0c2a64e..e93c08c 100644 --- a/ui.c +++ b/ui.c @@ -287,14 +287,24 @@ static void styleAdd(WINDOW *win, const char *str, bool show) { getmaxyx(win, y, width); size_t len; + int align = 0; struct Style style = Reset; while (*str) { - if (*str == ' ') { + if (*str == '\t') { + waddch(win, ' '); + getyx(win, y, align); + str++; + } else if (*str == ' ') { getyx(win, y, x); const char *word = &str[strspn(str, " ")]; if (width - x - 1 <= wordWidth(word)) { waddch(win, '\n'); + getyx(win, y, x); + wmove(win, y, align); str = word; + } else { + waddch(win, ' '); + str++; } } @@ -313,9 +323,8 @@ static void styleAdd(WINDOW *win, const char *str, bool show) { if (str - code > 1) waddnstr(win, &code[1], str - &code[1]); } - size_t sp = strspn(str, " "); - sp += strcspn(&str[sp], " "); - if (sp < len) len = sp; + size_t ws = strcspn(str, "\t "); + if (ws < len) len = ws; wattr_set( win, -- cgit 1.4.1-2-gfad0 From 9944dc484bf8cc7d5f1c506610a0593202bb5f92 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 20:23:55 -0500 Subject: Split showing style codes and word wrapping --- Makefile | 1 + chat.c | 2 +- chat.h | 4 ++ edit.c | 30 ++++++++++++ ui.c | 168 +++++++++++++++++++++++++++++++++++++++++---------------------- 5 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 edit.c diff --git a/Makefile b/Makefile index 6ba0ba5..63f719d 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ CFLAGS += -std=c11 -Wall -Wextra -Wpedantic LDLIBS = -lcurses -lcrypto -ltls OBJS += chat.o +OBJS += edit.o OBJS += handle.o OBJS += irc.o OBJS += ui.o diff --git a/chat.c b/chat.c index 1656a53..545eca6 100644 --- a/chat.c +++ b/chat.c @@ -41,7 +41,7 @@ enum Color idColors[IDCap] = { size_t idNext = Network + 1; -struct Self self; +struct Self self = { .color = Default }; static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { diff --git a/chat.h b/chat.h index 8de5df1..c754357 100644 --- a/chat.h +++ b/chat.h @@ -72,6 +72,7 @@ extern struct Self { char *chanTypes; char *prefixes; char *nick; + enum Color color; } self; static inline void set(char **field, const char *value) { @@ -121,6 +122,9 @@ void uiFormat( size_t id, enum Heat heat, const time_t *time, const char *format, ... ) __attribute__((format(printf, 4, 5))); +const char *editHead(void); +const char *editTail(void); + static inline enum Color hash(const char *str) { if (*str == '~') str++; uint32_t hash = 0; diff --git a/edit.c b/edit.c new file mode 100644 index 0000000..446e0e9 --- /dev/null +++ b/edit.c @@ -0,0 +1,30 @@ +/* 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 "chat.h" + +const char *editHead(void) { + return "foo\0033bar"; +} + +const char *editTail(void) { + return "baz\3"; +} diff --git a/ui.c b/ui.c index e93c08c..568df8d 100644 --- a/ui.c +++ b/ui.c @@ -268,71 +268,18 @@ static void styleParse(struct Style *style, const char **str, size_t *len) { *len = strcspn(*str, "\2\3\17\26\35\37"); } -static int wordWidth(const char *str) { - size_t len = strcspn(str, " "); - int width = 0; - while (len) { - wchar_t wc; - int n = mbtowc(&wc, str, len); - if (n < 1) return width + len; - width += (iswprint(wc) ? wcwidth(wc) : 0); - str += n; - len -= n; - } - return width; -} - -static void styleAdd(WINDOW *win, const char *str, bool show) { - int y, x, width; - getmaxyx(win, y, width); - +static void statusAdd(const char *str) { size_t len; - int align = 0; struct Style style = Reset; while (*str) { - if (*str == '\t') { - waddch(win, ' '); - getyx(win, y, align); - str++; - } else if (*str == ' ') { - getyx(win, y, x); - const char *word = &str[strspn(str, " ")]; - if (width - x - 1 <= wordWidth(word)) { - waddch(win, '\n'); - getyx(win, y, x); - wmove(win, y, align); - str = word; - } else { - waddch(win, ' '); - str++; - } - } - - const char *code = str; styleParse(&style, &str, &len); - if (show) { - wattr_set(win, A_BOLD | A_REVERSE, 0, NULL); - switch (*code) { - break; case '\2': waddch(win, 'B'); - break; case '\3': waddch(win, 'C'); - break; case '\17': waddch(win, 'O'); - break; case '\26': waddch(win, 'R'); - break; case '\35': waddch(win, 'I'); - break; case '\37': waddch(win, 'U'); - } - if (str - code > 1) waddnstr(win, &code[1], str - &code[1]); - } - - size_t ws = strcspn(str, "\t "); - if (ws < len) len = ws; - wattr_set( - win, + status, style.attr | colorAttr(mapColor(style.fg)), colorPair(mapColor(style.fg), mapColor(style.bg)), NULL ); - waddnstr(win, str, len); + waddnstr(status, str, len); str += len; } } @@ -354,7 +301,7 @@ static void statusUpdate(void) { idColors[window->id] ); if (!window->unread) buf[unread] = '\0'; - styleAdd(status, buf, false); + statusAdd(buf); } wclrtoeol(status); @@ -399,6 +346,61 @@ void uiShowNum(size_t num) { windowShow(window); } +static int wordWidth(const char *str) { + size_t len = strcspn(str, " "); + int width = 0; + while (len) { + wchar_t wc; + int n = mbtowc(&wc, str, len); + if (n < 1) return width + len; + width += (iswprint(wc) ? wcwidth(wc) : 0); + str += n; + len -= n; + } + return width; +} + +static void wordWrap(WINDOW *win, const char *str) { + int y, x, width; + getmaxyx(win, y, width); + + size_t len; + int align = 0; + struct Style style = Reset; + while (*str) { + if (*str == '\t') { + waddch(win, ' '); + getyx(win, y, align); + str++; + } else if (*str == ' ') { + getyx(win, y, x); + const char *word = &str[strspn(str, " ")]; + if (width - x - 1 <= wordWidth(word)) { + waddch(win, '\n'); + getyx(win, y, x); + wmove(win, y, align); + str = word; + } else { + waddch(win, ' '); + str++; + } + } + + styleParse(&style, &str, &len); + size_t ws = strcspn(str, "\t "); + if (ws < len) len = ws; + + wattr_set( + win, + style.attr | colorAttr(mapColor(style.fg)), + colorPair(mapColor(style.fg), mapColor(style.bg)), + NULL + ); + waddnstr(win, str, len); + str += len; + } +} + void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { (void)time; struct Window *window = windowFor(id); @@ -410,7 +412,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { window->heat = heat; statusUpdate(); } - styleAdd(window->pad, str, false); + wordWrap(window->pad, str); } void uiFormat( @@ -425,6 +427,55 @@ void uiFormat( uiWrite(id, heat, time, buf); } +static void inputAdd(struct Style *style, const char *str) { + size_t len; + while (*str) { + const char *code = 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'); + } + if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); + wattr_set( + input, + style->attr | colorAttr(mapColor(style->fg)), + colorPair(mapColor(style->fg), mapColor(style->bg)), + NULL + ); + waddnstr(input, str, len); + str += len; + } +} + +static void inputUpdate(void) { + wmove(input, 0, 0); + wattr_set( + input, + colorAttr(mapColor(self.color)), + colorPair(mapColor(self.color), -1), + NULL + ); + if (self.nick) { + waddch(input, '<'); + waddstr(input, self.nick); + waddstr(input, "> "); + } + + int y, x; + struct Style style = Reset; + inputAdd(&style, editHead()); + getyx(input, y, x); + inputAdd(&style, editTail()); + wclrtoeol(input); + wmove(input, y, x); +} + static void keyCode(int code) { switch (code) { break; case KEY_RESIZE:; // TODO @@ -467,4 +518,5 @@ void uiRead(void) { } meta = false; } + inputUpdate(); } -- cgit 1.4.1-2-gfad0 From ea93c9a6d98904c2334f86358a261cc6e556bdf4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 20:27:18 -0500 Subject: Set self.color --- handle.c | 1 + 1 file changed, 1 insertion(+) diff --git a/handle.c b/handle.c index 29d1500..305d70d 100644 --- a/handle.c +++ b/handle.c @@ -184,6 +184,7 @@ static void handleJoin(struct Message *msg) { require(msg, true, 1); size_t id = idFor(msg->params[0]); if (self.nick && !strcmp(msg->nick, self.nick)) { + self.color = hash(msg->user); idColors[id] = hash(msg->params[0]); uiShowID(id); } -- cgit 1.4.1-2-gfad0 From de4c9df0748f8f3792f4dfc4db4eb7853679646d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 20:40:49 -0500 Subject: Align join messages after nick --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 305d70d..32f8939 100644 --- a/handle.c +++ b/handle.c @@ -190,7 +190,7 @@ static void handleJoin(struct Message *msg) { } uiFormat( id, Cold, tagTime(msg), - "\3%02d%s\3 arrives in \3%02d%s\3", + "\3%02d%s\3\tarrives in \3%02d%s\3", hash(msg->user), msg->nick, idColors[id], idNames[id] ); } -- cgit 1.4.1-2-gfad0 From 55757243f48da0f0c6d2382ffe57d364c7b803c9 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 20:46:16 -0500 Subject: Call inputUpdate when switching windows Because changing windows (to or ) will affect the prompt. --- ui.c | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/ui.c b/ui.c index 568df8d..172120b 100644 --- a/ui.c +++ b/ui.c @@ -325,27 +325,6 @@ static void unmark(void) { statusUpdate(); } -static void windowShow(struct Window *window) { - touchwin(window->pad); - windows.other = windows.active; - windows.active = window; - windows.other->mark = true; - unmark(); -} - -void uiShowID(size_t id) { - windowShow(windowFor(id)); -} - -void uiShowNum(size_t num) { - struct Window *window = windows.head; - for (size_t i = 0; i < num; ++i) { - window = window->next; - if (!window) return; - } - windowShow(window); -} - static int wordWidth(const char *str) { size_t len = strcspn(str, " "); int width = 0; @@ -462,6 +441,7 @@ static void inputUpdate(void) { NULL ); if (self.nick) { + // TODO: Check if input is command or action. waddch(input, '<'); waddstr(input, self.nick); waddstr(input, "> "); @@ -476,6 +456,28 @@ static void inputUpdate(void) { wmove(input, y, x); } +static void windowShow(struct Window *window) { + touchwin(window->pad); + windows.other = windows.active; + windows.active = window; + windows.other->mark = true; + inputUpdate(); + unmark(); +} + +void uiShowID(size_t id) { + windowShow(windowFor(id)); +} + +void uiShowNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowShow(window); +} + static void keyCode(int code) { switch (code) { break; case KEY_RESIZE:; // TODO -- cgit 1.4.1-2-gfad0 From 104b3ffd4fe23ab530e728a7ef855cfc7d3c5595 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 20:56:27 -0500 Subject: Model keyCtrl like keyMeta --- ui.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 172120b..8f813b4 100644 --- a/ui.c +++ b/ui.c @@ -497,9 +497,9 @@ static void keyMeta(wchar_t ch) { } } -static void keyChar(wchar_t ch) { +static void keyCtrl(wchar_t ch) { switch (ch) { - break; case CTRL(L'L'): clearok(curscr, true); + break; case L'L': clearok(curscr, true); } } @@ -515,8 +515,10 @@ void uiRead(void) { continue; } else if (meta) { keyMeta(ch); + } else if (iswcntrl(ch)) { + keyCtrl(ch ^ L'@'); } else { - keyChar(ch); + // TODO: Insert. } meta = false; } -- cgit 1.4.1-2-gfad0 From 0df8bd51aa70cfa66951e8f08a6a3dce3fd45dec Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 4 Feb 2020 21:27:52 -0500 Subject: Convert editHead and editTail from wchar_t --- edit.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/edit.c b/edit.c index 446e0e9..e142507 100644 --- a/edit.c +++ b/edit.c @@ -14,17 +14,31 @@ * along with this program. If not, see . */ -#include +#include +#include #include #include -#include +#include #include "chat.h" +enum { Cap = 512 }; +static wchar_t buf[Cap] = L"foo\0033bar\3baz"; +static size_t len = 12; +static size_t pos = 6; + const char *editHead(void) { - return "foo\0033bar"; + static char mbs[MB_LEN_MAX * Cap]; + const wchar_t *ptr = buf; + size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs), NULL); + assert(n != (size_t)-1); + return mbs; } const char *editTail(void) { - return "baz\3"; + static char mbs[MB_LEN_MAX * Cap]; + const wchar_t *ptr = &buf[pos]; + size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs), NULL); + assert(n != (size_t)-1); + return mbs; } -- cgit 1.4.1-2-gfad0 From 4cce893eab7403821ff211f64a7df05051fd6f52 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 00:20:39 -0500 Subject: Add extremely basic editing and message sending --- Makefile | 1 + chat.h | 12 ++++++++++-- command.c | 32 ++++++++++++++++++++++++++++++++ edit.c | 32 +++++++++++++++++++++++++------- ui.c | 5 ++++- 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 command.c diff --git a/Makefile b/Makefile index 63f719d..05f8bb8 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ CFLAGS += -std=c11 -Wall -Wextra -Wpedantic LDLIBS = -lcurses -lcrypto -ltls OBJS += chat.o +OBJS += command.o OBJS += edit.o OBJS += handle.o OBJS += irc.o diff --git a/chat.h b/chat.h index c754357..c8b31c2 100644 --- a/chat.h +++ b/chat.h @@ -20,6 +20,7 @@ #include #include #include +#include #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit @@ -109,6 +110,7 @@ void ircFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); void handle(struct Message msg); +void command(size_t id, char *input); enum Heat { Cold, Warm, Hot }; void uiInit(void); @@ -122,8 +124,14 @@ void uiFormat( size_t id, enum Heat heat, const time_t *time, const char *format, ... ) __attribute__((format(printf, 4, 5))); -const char *editHead(void); -const char *editTail(void); +enum Edit { + EditKill, + EditInsert, + EditEnter, +}; +void edit(size_t id, enum Edit op, wchar_t ch); +char *editHead(void); +char *editTail(void); static inline enum Color hash(const char *str) { if (*str == '~') str++; diff --git a/command.c b/command.c new file mode 100644 index 0000000..ab05587 --- /dev/null +++ b/command.c @@ -0,0 +1,32 @@ +/* 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 "chat.h" + +void command(size_t id, char *input) { + ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); + struct Message msg = { + .nick = self.nick, + // TODO: .user, + .cmd = "PRIVMSG", + .params[0] = idNames[id], + .params[1] = input, + }; + handle(msg); +} diff --git a/edit.c b/edit.c index e142507..68593d1 100644 --- a/edit.c +++ b/edit.c @@ -23,22 +23,40 @@ #include "chat.h" enum { Cap = 512 }; -static wchar_t buf[Cap] = L"foo\0033bar\3baz"; -static size_t len = 12; -static size_t pos = 6; +static wchar_t buf[Cap]; +static size_t len; +static size_t pos; -const char *editHead(void) { +char *editHead(void) { static char mbs[MB_LEN_MAX * Cap]; const wchar_t *ptr = buf; - size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs), NULL); + size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL); assert(n != (size_t)-1); + mbs[n] = '\0'; return mbs; } -const char *editTail(void) { +char *editTail(void) { static char mbs[MB_LEN_MAX * Cap]; const wchar_t *ptr = &buf[pos]; - size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs), NULL); + size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs) - 1, NULL); assert(n != (size_t)-1); + mbs[n] = '\0'; return mbs; } + +void edit(size_t id, enum Edit op, wchar_t ch) { + switch (op) { + break; case EditKill: len = pos = 0; + break; case EditInsert: { + if (len == Cap) break; + buf[pos++] = ch; + len++; + } + break; case EditEnter: { + pos = 0; + command(id, editTail()); + len = 0; + } + } +} diff --git a/ui.c b/ui.c index 8f813b4..b764f84 100644 --- a/ui.c +++ b/ui.c @@ -498,8 +498,11 @@ static void keyMeta(wchar_t ch) { } static void keyCtrl(wchar_t ch) { + size_t id = windows.active->id; switch (ch) { + break; case L'J': edit(id, EditEnter, 0); break; case L'L': clearok(curscr, true); + break; case L'U': edit(id, EditKill, 0); } } @@ -518,7 +521,7 @@ void uiRead(void) { } else if (iswcntrl(ch)) { keyCtrl(ch ^ L'@'); } else { - // TODO: Insert. + edit(windows.active->id, EditInsert, ch); } meta = false; } -- cgit 1.4.1-2-gfad0 From 37ec1e8232ab5aadd1989be1c7b518c98438cfd0 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 00:24:54 -0500 Subject: Align MOTD after - --- handle.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/handle.c b/handle.c index 32f8939..b5585ba 100644 --- a/handle.c +++ b/handle.c @@ -176,8 +176,11 @@ static void handleReplyISupport(struct Message *msg) { static void handleReplyMOTD(struct Message *msg) { require(msg, false, 2); char *line = msg->params[1]; - if (!strncmp(line, "- ", 2)) line += 2; - uiFormat(Network, Cold, tagTime(msg), "%s", line); + if (!strncmp(line, "- ", 2)) { + uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); + } else { + uiFormat(Network, Cold, tagTime(msg), "%s", line); + } } static void handleJoin(struct Message *msg) { -- cgit 1.4.1-2-gfad0 From 7414a8a11cd8d16fea47e30513b3a5eaeb232ba1 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 00:40:24 -0500 Subject: Save own username for message echoing --- chat.h | 1 + command.c | 2 +- handle.c | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chat.h b/chat.h index c8b31c2..90c7da8 100644 --- a/chat.h +++ b/chat.h @@ -73,6 +73,7 @@ extern struct Self { char *chanTypes; char *prefixes; char *nick; + char *user; enum Color color; } self; diff --git a/command.c b/command.c index ab05587..76d7d7b 100644 --- a/command.c +++ b/command.c @@ -23,7 +23,7 @@ void command(size_t id, char *input) { ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); struct Message msg = { .nick = self.nick, - // TODO: .user, + .user = self.user, .cmd = "PRIVMSG", .params[0] = idNames[id], .params[1] = input, diff --git a/handle.c b/handle.c index b5585ba..85783d7 100644 --- a/handle.c +++ b/handle.c @@ -187,7 +187,10 @@ static void handleJoin(struct Message *msg) { require(msg, true, 1); size_t id = idFor(msg->params[0]); if (self.nick && !strcmp(msg->nick, self.nick)) { - self.color = hash(msg->user); + if (!self.user) { + set(&self.user, msg->user); + self.color = hash(msg->user); + } idColors[id] = hash(msg->params[0]); uiShowID(id); } -- cgit 1.4.1-2-gfad0 From 42210e079bcdea26261af577d0802fdc4c3d03b6 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 02:03:21 -0500 Subject: Reflow text on window resize --- chat.c | 2 ++ ui.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/chat.c b/chat.c index 545eca6..a8b2fa2 100644 --- a/chat.c +++ b/chat.c @@ -122,6 +122,8 @@ int main(int argc, char *argv[]) { if (signals[SIGHUP] || signals[SIGINT] || signals[SIGTERM]) { break; } + // FIXME: Display doesn't update properly when receiving many of these + // until some input? if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); diff --git a/ui.c b/ui.c index b764f84..12205cc 100644 --- a/ui.c +++ b/ui.c @@ -35,6 +35,9 @@ #include "chat.h" +// Annoying stuff from : +#undef lines + #ifndef A_ITALIC #define A_ITALIC A_UNDERLINE #endif @@ -43,16 +46,28 @@ #define RIGHT (COLS - 1) #define WINDOW_LINES (LINES - 2) -enum { - InputCols = 512, - PadLines = 512, -}; - static WINDOW *status; static WINDOW *input; +enum { BufferCap = 512 }; +struct Buffer { + time_t times[BufferCap]; + char *lines[BufferCap]; + size_t len; +}; +static_assert(!(BufferCap & (BufferCap - 1)), "BufferCap is power of two"); + +static void bufferPush(struct Buffer *buffer, time_t time, const char *line) { + size_t i = buffer->len++ % BufferCap; + free(buffer->lines[i]); + buffer->times[i] = time; + buffer->lines[i] = strdup(line); + if (!buffer->lines[i]) err(EX_OSERR, "strdup"); +} + struct Window { size_t id; + struct Buffer buffer; WINDOW *pad; enum Heat heat; int unread; @@ -89,17 +104,15 @@ static struct Window *windowFor(size_t id) { for (window = windows.head; window; window = window->next) { if (window->id == id) return window; } - window = malloc(sizeof(*window)); + window = calloc(1, sizeof(*window)); if (!window) err(EX_OSERR, "malloc"); window->id = id; - window->pad = newpad(PadLines, COLS); - window->heat = Cold; - window->unread = 0; - window->scroll = PadLines; - window->mark = true; + window->pad = newpad(BufferCap, COLS); scrollok(window->pad, true); - wmove(window->pad, PadLines - 1, 0); + wmove(window->pad, BufferCap - 1, 0); + window->scroll = BufferCap; + window->mark = true; windowAdd(window); return window; @@ -190,7 +203,7 @@ void uiInit(void) { colorInit(); status = newwin(1, COLS, 0, 0); - input = newpad(1, InputCols); + input = newpad(1, 512); keypad(input, true); nodelay(input, true); windows.active = windowFor(Network); @@ -380,9 +393,11 @@ static void wordWrap(WINDOW *win, const char *str) { } } -void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) { - (void)time; +void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { struct Window *window = windowFor(id); + time_t clock = (src ? *src : time(NULL)); + bufferPush(&window->buffer, clock, str); + waddch(window->pad, '\n'); if (window->mark && heat > Cold) { if (!window->unread++) { @@ -406,6 +421,25 @@ void uiFormat( uiWrite(id, heat, time, buf); } +static void reflow(struct Window *window) { + werase(window->pad); + wmove(window->pad, BufferCap - 1, 0); + size_t len = window->buffer.len; + for (size_t i = (len > BufferCap ? len - BufferCap : 0); i < len; ++i) { + waddch(window->pad, '\n'); + wordWrap(window->pad, window->buffer.lines[i % BufferCap]); + } +} + +static void resize(void) { + // FIXME: Only reflow when COLS changes. + for (struct Window *window = windows.head; window; window = window->next) { + wresize(window->pad, BufferCap, COLS); + reflow(window); + } + statusUpdate(); +} + static void inputAdd(struct Style *style, const char *str) { size_t len; while (*str) { @@ -480,7 +514,7 @@ void uiShowNum(size_t num) { static void keyCode(int code) { switch (code) { - break; case KEY_RESIZE:; // TODO + break; case KEY_RESIZE: resize(); break; case KeyFocusIn: unmark(); break; case KeyFocusOut: windows.active->mark = true; break; case KeyPasteOn:; // TODO @@ -490,7 +524,7 @@ static void keyCode(int code) { static void keyMeta(wchar_t ch) { switch (ch) { - break; case L'm': uiWrite(windows.active->id, Cold, NULL, ""); + break; case L'm': waddch(windows.active->pad, '\n'); break; default: { if (ch >= L'0' && ch <= L'9') uiShowNum(ch - L'0'); } -- cgit 1.4.1-2-gfad0 From 1cf6e29fc465dc9e3633950ede758d4fd1eb644d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 03:42:04 -0500 Subject: Send input as raw IRC in --- command.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/command.c b/command.c index 76d7d7b..ef27888 100644 --- a/command.c +++ b/command.c @@ -20,6 +20,10 @@ #include "chat.h" void command(size_t id, char *input) { + if (id == Debug) { + ircFormat("%s\r\n", input); + return; + } ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); struct Message msg = { .nick = self.nick, -- cgit 1.4.1-2-gfad0 From 6e679bdf26fa4dae651aade985155bd424761a3a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 04:24:13 -0500 Subject: Modulo colors before comparing pairs Otherwise a new pair is allocated every time a high color is requested. --- ui.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 12205cc..894e04d 100644 --- a/ui.c +++ b/ui.c @@ -135,12 +135,14 @@ static attr_t colorAttr(short fg) { static short colorPair(short fg, short bg) { if (bg == -1) return 1 + fg; + fg %= COLORS; + bg %= COLORS; for (short pair = 17; pair < colorPairs; ++pair) { short f, b; pair_content(pair, &f, &b); if (f == fg && b == bg) return pair; } - init_pair(colorPairs, fg % COLORS, bg % COLORS); + init_pair(colorPairs, fg, bg); return colorPairs++; } -- cgit 1.4.1-2-gfad0 From 2d5f608cc5dde360be4acdf5b0006a78e2b433c8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 17:58:49 -0500 Subject: Fix SIGWINCH handling curses is dumb. --- chat.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chat.c b/chat.c index a8b2fa2..2d58b1e 100644 --- a/chat.c +++ b/chat.c @@ -122,11 +122,12 @@ int main(int argc, char *argv[]) { if (signals[SIGHUP] || signals[SIGINT] || signals[SIGTERM]) { break; } - // FIXME: Display doesn't update properly when receiving many of these - // until some input? if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); + // XXX: For some reason, calling uiDraw() here is the only way to + // get uiRead() to properly receive KEY_RESIZE. + uiDraw(); uiRead(); } -- cgit 1.4.1-2-gfad0 From eb91347308c90ec07b4680aeb2a693c475daea9d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 18:01:57 -0500 Subject: Only reflow text when COLS changes --- ui.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 894e04d..da28b76 100644 --- a/ui.c +++ b/ui.c @@ -434,7 +434,9 @@ static void reflow(struct Window *window) { } static void resize(void) { - // FIXME: Only reflow when COLS changes. + int height, width; + getmaxyx(windows.active->pad, height, width); + if (width == COLS) return; for (struct Window *window = windows.head; window; window = window->next) { wresize(window->pad, BufferCap, COLS); reflow(window); -- cgit 1.4.1-2-gfad0 From a7b0ed99079065f54db83160dfe651ce9d50a568 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 18:18:25 -0500 Subject: Scroll the input window I was wondering if I should instead make input wrap, but then wordWrap would need to both support showing formatting and persisting styles across to strings, and it would need to move the window pad up and down a bunch, etc. --- ui.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui.c b/ui.c index da28b76..12c8541 100644 --- a/ui.c +++ b/ui.c @@ -220,10 +220,11 @@ void uiDraw(void) { 1, 0, BOTTOM - 1, RIGHT ); - // TODO: Input scrolling. + int y, x; + getyx(input, y, x); pnoutrefresh( input, - 0, 0, + 0, (x > RIGHT ? x - RIGHT : 0), BOTTOM, 0, BOTTOM, RIGHT ); -- cgit 1.4.1-2-gfad0 From b2d35edcb22a9a41235229b41b180a50b51b5908 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 19:00:54 -0500 Subject: Change prompt depending on command --- chat.h | 3 +++ command.c | 21 +++++++++++++++++++++ ui.c | 23 ++++++++++++++++++----- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/chat.h b/chat.h index 90c7da8..b73cf40 100644 --- a/chat.h +++ b/chat.h @@ -112,6 +112,9 @@ void ircFormat(const char *format, ...) void handle(struct Message msg); void command(size_t id, char *input); +const char *commandIsPrivmsg(size_t id, const char *input); +const char *commandIsNotice(size_t id, const char *input); +const char *commandIsAction(size_t id, const char *input); enum Heat { Cold, Warm, Hot }; void uiInit(void); diff --git a/command.c b/command.c index ef27888..928f470 100644 --- a/command.c +++ b/command.c @@ -19,6 +19,27 @@ #include "chat.h" +const char *commandIsPrivmsg(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (input[0] != '/') return input; + const char *space = strchr(&input[1], ' '); + const char *slash = strchr(&input[1], '/'); + if (slash && (!space || slash < space)) return input; + return NULL; +} + +const char *commandIsNotice(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/notice ", 8)) return NULL; + return &input[8]; +} + +const char *commandIsAction(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/me ", 4)) return NULL; + return &input[4]; +} + void command(size_t id, char *input) { if (id == Debug) { ircFormat("%s\r\n", input); diff --git a/ui.c b/ui.c index 12c8541..daa6dec 100644 --- a/ui.c +++ b/ui.c @@ -479,16 +479,29 @@ static void inputUpdate(void) { colorPair(mapColor(self.color), -1), NULL ); + const char *head = editHead(); + const char *skip = NULL; if (self.nick) { - // TODO: Check if input is command or action. - waddch(input, '<'); - waddstr(input, self.nick); - waddstr(input, "> "); + size_t id = windows.active->id; + if (NULL != (skip = commandIsPrivmsg(id, head))) { + waddch(input, '<'); + waddstr(input, self.nick); + waddstr(input, "> "); + } else if (NULL != (skip = commandIsNotice(id, head))) { + waddch(input, '-'); + waddstr(input, self.nick); + waddstr(input, "- "); + } else if (NULL != (skip = commandIsAction(id, head))) { + waddstr(input, "* "); + waddstr(input, self.nick); + waddch(input, ' '); + } } + if (skip) head = skip; int y, x; struct Style style = Reset; - inputAdd(&style, editHead()); + inputAdd(&style, head); getyx(input, y, x); inputAdd(&style, editTail()); wclrtoeol(input); -- cgit 1.4.1-2-gfad0 From 7c0b60221bf22a8042b584c453bda0e3e87cd0ea Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 19:19:01 -0500 Subject: Add /me, /notice, /quote commands --- command.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/command.c b/command.c index 928f470..0843bd3 100644 --- a/command.c +++ b/command.c @@ -19,6 +19,57 @@ #include "chat.h" +typedef void Command(size_t id, char *params); + +static void commandQuote(size_t id, char *params) { + (void)id; + ircFormat("%s\r\n", params); +} + +static void commandPrivmsg(size_t id, char *params) { + ircFormat("PRIVMSG %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "PRIVMSG", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandNotice(size_t id, char *params) { + ircFormat("NOTICE %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "NOTICE", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandMe(size_t id, char *params) { + char buf[512]; + snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + commandPrivmsg(id, buf); +} + +static const struct Handler { + const char *cmd; + Command *fn; +} Commands[] = { + { "/me", commandMe }, + { "/notice", commandNotice }, + { "/quote", commandQuote }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + const char *commandIsPrivmsg(size_t id, const char *input) { if (id == Network || id == Debug) return NULL; if (input[0] != '/') return input; @@ -42,16 +93,20 @@ const char *commandIsAction(size_t id, const char *input) { void command(size_t id, char *input) { if (id == Debug) { - ircFormat("%s\r\n", input); + commandQuote(id, input); return; } - ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); - struct Message msg = { - .nick = self.nick, - .user = self.user, - .cmd = "PRIVMSG", - .params[0] = idNames[id], - .params[1] = input, - }; - handle(msg); + if (commandIsPrivmsg(id, input)) { + commandPrivmsg(id, input); + return; + } + char *cmd = strsep(&input, " "); + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (handler) { + handler->fn(id, input); + } else { + uiFormat(id, Hot, NULL, "No such command %s", cmd); + } } -- cgit 1.4.1-2-gfad0 From 7c0e9cf3d2e83814fab3bb4bb09f7b955c2afaca Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 21:57:23 -0500 Subject: Add /quit --- chat.c | 13 ++++++++----- chat.h | 1 + command.c | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/chat.c b/chat.c index 2d58b1e..1ad2833 100644 --- a/chat.c +++ b/chat.c @@ -115,13 +115,12 @@ int main(int argc, char *argv[]) { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, }; - for (;;) { + while (!self.quit) { int nfds = poll(fds, 2, -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); - if (signals[SIGHUP] || signals[SIGINT] || signals[SIGTERM]) { - break; - } + if (signals[SIGHUP]) self.quit = "zzz"; + if (signals[SIGINT] || signals[SIGTERM]) break; if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); @@ -136,6 +135,10 @@ int main(int argc, char *argv[]) { uiDraw(); } - ircFormat("QUIT\r\n"); + if (self.quit) { + ircFormat("QUIT :%s\r\n", self.quit); + } else { + ircFormat("QUIT\r\n"); + } uiHide(); } diff --git a/chat.h b/chat.h index b73cf40..5b3c01c 100644 --- a/chat.h +++ b/chat.h @@ -75,6 +75,7 @@ extern struct Self { char *nick; char *user; enum Color color; + char *quit; } self; static inline void set(char **field, const char *value) { diff --git a/command.c b/command.c index 0843bd3..7fb98af 100644 --- a/command.c +++ b/command.c @@ -56,12 +56,17 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandQuit(size_t id, char *params) { + set(&self.quit, (params ? params : "Goodbye")); +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { { "/me", commandMe }, { "/notice", commandNotice }, + { "/quit", commandQuit }, { "/quote", commandQuote }, }; -- cgit 1.4.1-2-gfad0 From b2cf8733048029354aeb794b14dd71d1bbb0b72d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:09:29 -0500 Subject: Add /window --- chat.h | 1 + command.c | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/chat.h b/chat.h index 5b3c01c..9317843 100644 --- a/chat.h +++ b/chat.h @@ -123,6 +123,7 @@ void uiShow(void); void uiHide(void); void uiDraw(void); void uiShowID(size_t id); +void uiShowNum(size_t num); 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 7fb98af..8471214 100644 --- a/command.c +++ b/command.c @@ -57,9 +57,16 @@ static void commandMe(size_t id, char *params) { } static void commandQuit(size_t id, char *params) { + (void)id; set(&self.quit, (params ? params : "Goodbye")); } +static void commandWindow(size_t id, char *params) { + (void)id; + if (!params) return; + uiShowNum(strtoul(params, NULL, 10)); +} + static const struct Handler { const char *cmd; Command *fn; @@ -68,6 +75,7 @@ static const struct Handler { { "/notice", commandNotice }, { "/quit", commandQuit }, { "/quote", commandQuote }, + { "/window", commandWindow }, }; static int compar(const void *cmd, const void *_handler) { @@ -97,21 +105,21 @@ const char *commandIsAction(size_t id, const char *input) { } void command(size_t id, char *input) { - if (id == Debug) { + if (id == Debug && input[0] != '/') { commandQuote(id, input); - return; - } - if (commandIsPrivmsg(id, input)) { + } else if (commandIsPrivmsg(id, input)) { commandPrivmsg(id, input); - return; - } - char *cmd = strsep(&input, " "); - const struct Handler *handler = bsearch( - cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar - ); - if (handler) { - handler->fn(id, input); + } else if (input[0] == '/' && isdigit(input[1])) { + commandWindow(id, &input[1]); } else { - uiFormat(id, Hot, NULL, "No such command %s", cmd); + char *cmd = strsep(&input, " "); + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (handler) { + handler->fn(id, input); + } else { + uiFormat(id, Hot, NULL, "No such command %s", cmd); + } } } -- cgit 1.4.1-2-gfad0 From 7cc64927bd223ed0a0197cf3285dbc85691fd32b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:15:08 -0500 Subject: Handle empty messages on privmsg, notice, action --- command.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/command.c b/command.c index 8471214..2a3df9b 100644 --- a/command.c +++ b/command.c @@ -27,6 +27,7 @@ static void commandQuote(size_t id, char *params) { } static void commandPrivmsg(size_t id, char *params) { + if (!params || !params[0]) return; ircFormat("PRIVMSG %s :%s\r\n", idNames[id], params); struct Message msg = { .nick = self.nick, @@ -39,6 +40,7 @@ static void commandPrivmsg(size_t id, char *params) { } static void commandNotice(size_t id, char *params) { + if (!params || !params[0]) return; ircFormat("NOTICE %s :%s\r\n", idNames[id], params); struct Message msg = { .nick = self.nick, @@ -52,7 +54,7 @@ static void commandNotice(size_t id, char *params) { static void commandMe(size_t id, char *params) { char buf[512]; - snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + snprintf(buf, sizeof(buf), "\1ACTION %s\1", (params ? params : "")); commandPrivmsg(id, buf); } -- cgit 1.4.1-2-gfad0 From 63b92672fe9ecfd400d9439343a068a3ae5224df Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:18:11 -0500 Subject: Handle empty params in /quote --- command.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/command.c b/command.c index 2a3df9b..f9362dc 100644 --- a/command.c +++ b/command.c @@ -23,7 +23,7 @@ typedef void Command(size_t id, char *params); static void commandQuote(size_t id, char *params) { (void)id; - ircFormat("%s\r\n", params); + if (params) ircFormat("%s\r\n", params); } static void commandPrivmsg(size_t id, char *params) { @@ -65,8 +65,7 @@ static void commandQuit(size_t id, char *params) { static void commandWindow(size_t id, char *params) { (void)id; - if (!params) return; - uiShowNum(strtoul(params, NULL, 10)); + if (params) uiShowNum(strtoul(params, NULL, 10)); } static const struct Handler { -- 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(-) 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(-) 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 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 839cc362a85e3cf463cd0cfe6c092ca0978b8c29 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 22:51:45 -0500 Subject: Handle errors from getopt --- chat.c | 1 + 1 file changed, 1 insertion(+) diff --git a/chat.c b/chat.c index c67d8a9..115fe38 100644 --- a/chat.c +++ b/chat.c @@ -97,6 +97,7 @@ int main(int argc, char *argv[]) { break; case 'u': user = optarg; break; case 'v': self.debug = true; break; case 'w': pass = optarg; + break; default: return EX_USAGE; } } if (!host) errx(EX_USAGE, "host required"); -- cgit 1.4.1-2-gfad0 From 3085779d86f3ed66bd24eccf1f64ffc1bd71afcd Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Wed, 5 Feb 2020 23:27:43 -0500 Subject: Handle ERROR --- handle.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/handle.c b/handle.c index 85783d7..89ebcb2 100644 --- a/handle.c +++ b/handle.c @@ -233,6 +233,11 @@ static void handlePing(struct Message *msg) { ircFormat("PONG :%s\r\n", msg->params[0]); } +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(EX_UNAVAILABLE, "%s", msg->params[0]); +} + static const struct Handler { const char *cmd; Handler *fn; @@ -248,6 +253,7 @@ static const struct Handler { { "906", handleErrorSASLFail }, { "AUTHENTICATE", handleAuthenticate }, { "CAP", handleCap }, + { "ERROR", handleError }, { "JOIN", handleJoin }, { "NOTICE", handlePrivmsg }, { "PING", handlePing }, -- cgit 1.4.1-2-gfad0 From db499dc5f50dba23c2ab218d439cfce51c41bc6b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 01:03:21 -0500 Subject: Send self.join without colon If someone is weird enough to use channel keys, they can -j '#foo key'. --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 89ebcb2..a111b16 100644 --- a/handle.c +++ b/handle.c @@ -149,7 +149,7 @@ static void handleErrorSASLFail(struct Message *msg) { static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); - if (self.join) ircFormat("JOIN :%s\r\n", self.join); + if (self.join) ircFormat("JOIN %s\r\n", self.join); } static void handleReplyISupport(struct Message *msg) { -- 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(-) 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 5fb492f8cda7598cbf1a977b0b3c66f9dc1b24f0 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 01:16:35 -0500 Subject: Handle PART --- handle.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/handle.c b/handle.c index a111b16..8a68c95 100644 --- a/handle.c +++ b/handle.c @@ -201,6 +201,18 @@ static void handleJoin(struct Message *msg) { ); } +static void handlePart(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tleaves \3%02d%s\3%s%s", + hash(msg->user), msg->nick, idColors[id], idNames[id], + (msg->params[1] ? ": " : ""), + (msg->params[1] ? msg->params[1] : "") + ); +} + static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -256,6 +268,7 @@ static const struct Handler { { "ERROR", handleError }, { "JOIN", handleJoin }, { "NOTICE", handlePrivmsg }, + { "PART", handlePart }, { "PING", handlePing }, { "PRIVMSG", handlePrivmsg }, }; -- cgit 1.4.1-2-gfad0 From 5e98d83f83f12f208cc9089d87a18c73a8a6fcfc Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 02:07:39 -0500 Subject: Handle TOPIC and replies --- handle.c | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/handle.c b/handle.c index 8a68c95..e636434 100644 --- a/handle.c +++ b/handle.c @@ -197,22 +197,57 @@ static void handleJoin(struct Message *msg) { uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tarrives in \3%02d%s\3", - hash(msg->user), msg->nick, idColors[id], idNames[id] + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] ); } static void handlePart(struct Message *msg) { require(msg, true, 1); - size_t id = idFor(msg->params[0]); uiFormat( - id, Cold, tagTime(msg), + idFor(msg->params[0]), Cold, tagTime(msg), "\3%02d%s\3\tleaves \3%02d%s\3%s%s", - hash(msg->user), msg->nick, idColors[id], idNames[id], + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], (msg->params[1] ? ": " : ""), (msg->params[1] ? msg->params[1] : "") ); } +static void handleReplyNoTopic(struct Message *msg) { + require(msg, false, 2); + uiFormat( + idFor(msg->params[1]), Cold, tagTime(msg), + "There is no sign in \3%02d%s\3", + hash(msg->params[1]), msg->params[1] + ); +} + +static void handleReplyTopic(struct Message *msg) { + require(msg, false, 3); + uiFormat( + idFor(msg->params[1]), Cold, tagTime(msg), + "The sign in \3%02d%s\3 reads: %s", + hash(msg->params[1]), msg->params[1], msg->params[2] + ); +} + +static void handleTopic(struct Message *msg) { + require(msg, true, 2); + if (msg->params[1][0]) { + uiFormat( + idFor(msg->params[0]), 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), + "\3%02d%s\3\tremoves the sign in \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + } +} + static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -256,6 +291,8 @@ static const struct Handler { } Handlers[] = { { "001", handleReplyWelcome }, { "005", handleReplyISupport }, + { "331", handleReplyNoTopic }, + { "332", handleReplyTopic }, { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, @@ -271,6 +308,7 @@ static const struct Handler { { "PART", handlePart }, { "PING", handlePing }, { "PRIVMSG", handlePrivmsg }, + { "TOPIC", handleTopic }, }; static int compar(const void *cmd, const void *_handler) { -- cgit 1.4.1-2-gfad0 From 8b3bf897c2b7a14ff6a4c096b9969eaeb695a9e0 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 02:21:04 -0500 Subject: Search for cert and priv in config dirs --- chat.c | 14 +++++++++++++- chat.h | 2 +- irc.c | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/chat.c b/chat.c index 115fe38..c487722 100644 --- a/chat.c +++ b/chat.c @@ -111,7 +111,19 @@ int main(int argc, char *argv[]) { set(&self.chanTypes, "#&"); set(&self.prefixes, "@+"); - ircConfig(insecure, cert, priv); + FILE *certFile = NULL; + FILE *privFile = NULL; + if (cert) { + certFile = configOpen(cert, "r"); + if (!certFile) err(EX_NOINPUT, "%s", cert); + } + if (priv) { + privFile = configOpen(priv, "r"); + if (!privFile) err(EX_NOINPUT, "%s", priv); + } + ircConfig(insecure, certFile, privFile); + if (certFile) fclose(certFile); + if (privFile) fclose(privFile); uiInit(); uiShowID(Network); diff --git a/chat.h b/chat.h index 57d4ba6..112530d 100644 --- a/chat.h +++ b/chat.h @@ -105,7 +105,7 @@ struct Message { char *params[ParamCap]; }; -void ircConfig(bool insecure, const char *cert, const char *priv); +void ircConfig(bool insecure, FILE *cert, FILE *priv); int ircConnect(const char *host, const char *port); void ircRecv(void); void ircSend(const char *ptr, size_t len); diff --git a/irc.c b/irc.c index 2d6f00b..05f8f9d 100644 --- a/irc.c +++ b/irc.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -31,7 +32,22 @@ struct tls *client; -void ircConfig(bool insecure, const char *cert, const char *priv) { +static byte *readFile(size_t *len, FILE *file) { + struct stat stat; + int error = fstat(fileno(file), &stat); + if (error) err(EX_IOERR, "fstat"); + + byte *buf = malloc(stat.st_size); + if (!buf) err(EX_OSERR, "malloc"); + + rewind(file); + *len = fread(buf, 1, stat.st_size, file); + if (ferror(file)) err(EX_IOERR, "fread"); + + return buf; +} + +void ircConfig(bool insecure, FILE *cert, FILE *priv) { struct tls_config *config = tls_config_new(); if (!config) errx(EX_SOFTWARE, "tls_config_new"); @@ -49,13 +65,28 @@ void ircConfig(bool insecure, const char *cert, const char *priv) { } if (cert) { - error = tls_config_set_keypair_file(config, cert, (priv ? priv : cert)); + size_t len; + byte *buf = readFile(&len, cert); + error = tls_config_set_cert_mem(config, buf, len); + if (error) { + errx( + EX_CONFIG, "tls_config_set_cert_mem: %s", + tls_config_error(config) + ); + } + if (priv) { + free(buf); + buf = readFile(&len, priv); + } + error = tls_config_set_key_mem(config, buf, len); if (error) { errx( - EX_SOFTWARE, "tls_config_set_keypair_file: %s", + EX_CONFIG, "tls_config_set_key_mem: %s", tls_config_error(config) ); } + explicit_bzero(buf, len); + free(buf); } client = tls_client(); -- cgit 1.4.1-2-gfad0 From a5a162b9c6f9c3672c6c1e0882d70191610943c8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 02:25:58 -0500 Subject: Disable SUSP Frees up C-z and suspending an IRC client is silly anyway. --- ui.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ui.c b/ui.c index daa6dec..048eade 100644 --- a/ui.c +++ b/ui.c @@ -176,6 +176,7 @@ static void disableFlowControl(void) { 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[VDISCARD] = _POSIX_VDISABLE; error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term); if (error) err(EX_OSERR, "tcsetattr"); -- 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(-) 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 1e6e533538f5c18adc64f21155cb76277dcb2a9b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 03:44:49 -0500 Subject: Send CAP END if CAP LS doesn't list anything good --- handle.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/handle.c b/handle.c index e636434..cb080e9 100644 --- a/handle.c +++ b/handle.c @@ -98,7 +98,11 @@ static void handleCap(struct Message *msg) { enum Cap caps = capParse(msg->params[2]); if (!strcmp(msg->params[1], "LS")) { caps &= ~CapSASL; - ircFormat("CAP REQ :%s\r\n", capList(caps)); + if (caps) { + ircFormat("CAP REQ :%s\r\n", capList(caps)); + } else { + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } } else if (!strcmp(msg->params[1], "ACK")) { self.caps |= caps; if (caps & CapSASL) { -- cgit 1.4.1-2-gfad0 From 30b3780e57e532e928d445b882b218decb88af55 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 04:01:11 -0500 Subject: Route own query messages correctly --- handle.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/handle.c b/handle.c index cb080e9..9244e66 100644 --- a/handle.c +++ b/handle.c @@ -263,11 +263,20 @@ static bool isAction(struct Message *msg) { static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); bool query = !strchr(self.chanTypes, msg->params[0][0]); - bool network = query && strchr(msg->nick, '.'); + bool network = strchr(msg->nick, '.'); + bool mine = self.nick && !strcmp(msg->nick, self.nick); + size_t id; + if (query && network) { + id = Network; + } else if (query && !mine) { + id = idFor(msg->nick); + idColors[id] = hash(msg->user); + } else { + id = idFor(msg->params[0]); + } + bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); - size_t id = (network ? Network : idFor(query ? msg->nick : msg->params[0])); - if (query && !network) idColors[id] = hash(msg->user); uiFormat( id, Warm, tagTime(msg), "\3%d%s%s%s\3\t%s", -- cgit 1.4.1-2-gfad0 From 32ec697092a8d9b4925e64519643c9005f2d408c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 04:18:15 -0500 Subject: Handle mentions --- handle.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/handle.c b/handle.c index 9244e66..01e2e5e 100644 --- a/handle.c +++ b/handle.c @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -260,6 +261,21 @@ static bool isAction(struct Message *msg) { return true; } +static bool isMention(const struct Message *msg) { + if (!self.nick) return false; + size_t len = strlen(self.nick); + const char *match = msg->params[1]; + while (NULL != (match = strcasestr(match, self.nick))) { + char a = (match > msg->params[1] ? match[-1] : ' '); + char b = (match[len] ? match[len] : ' '); + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + return true; + } + match = &match[len]; + } + return false; +} + static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); bool query = !strchr(self.chanTypes, msg->params[0][0]); @@ -277,9 +293,11 @@ static void handlePrivmsg(struct Message *msg) { bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); + bool mention = !mine && isMention(msg); uiFormat( - id, Warm, tagTime(msg), - "\3%d%s%s%s\3\t%s", + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\3%d%s%s%s\17\t%s", + (mention ? "\26" : ""), hash(msg->user), (action ? "* " : notice ? "-" : "<"), msg->nick, -- cgit 1.4.1-2-gfad0 From e9394bfff9dff32b98890b89ecdc884ffb4cba79 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 04:19:56 -0500 Subject: Set id color to Default on allocation --- chat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/chat.h b/chat.h index 112530d..8279d1b 100644 --- a/chat.h +++ b/chat.h @@ -51,6 +51,7 @@ static inline size_t idFor(const char *name) { if (id) return id; idNames[idNext] = strdup(name); if (!idNames[idNext]) err(EX_OSERR, "strdup"); + idColors[idNext] = Default; return idNext++; } -- cgit 1.4.1-2-gfad0 From 68440d50c624a6cfa52027635bcd9207b48504e6 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 04:23:49 -0500 Subject: Beep on hot --- ui.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ui.c b/ui.c index 048eade..b0e30b9 100644 --- a/ui.c +++ b/ui.c @@ -411,6 +411,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { statusUpdate(); } wordWrap(window->pad, str); + if (heat > Warm) beep(); } void uiFormat( -- cgit 1.4.1-2-gfad0 From e1f10958c9954592a81b6b52693e3f63304d22d8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 04:37:28 -0500 Subject: Never consider notices hot --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 01e2e5e..49011f5 100644 --- a/handle.c +++ b/handle.c @@ -295,7 +295,7 @@ static void handlePrivmsg(struct Message *msg) { bool action = isAction(msg); bool mention = !mine && isMention(msg); uiFormat( - id, (mention || query ? Hot : Warm), tagTime(msg), + id, (!notice && (mention || query) ? Hot : Warm), tagTime(msg), "%s\3%d%s%s%s\17\t%s", (mention ? "\26" : ""), hash(msg->user), -- cgit 1.4.1-2-gfad0 From 34514cf2ee6dd020ca812653ce3a23e737d2bf62 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 18:48:49 -0500 Subject: Render actions in italic Also render italic as normal if it's unsupported, as that is what would happen anyway if curses has A_ITALIC but the terminal has no sitm. That format string is kinda bad. --- handle.c | 8 +++++--- ui.c | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/handle.c b/handle.c index 49011f5..ab482fd 100644 --- a/handle.c +++ b/handle.c @@ -294,14 +294,16 @@ static void handlePrivmsg(struct Message *msg) { bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); bool mention = !mine && isMention(msg); + const char *italic = (action ? "\35" : ""); + const char *reverse = (mention ? "\26" : ""); uiFormat( id, (!notice && (mention || query) ? Hot : Warm), tagTime(msg), - "%s\3%d%s%s%s\17\t%s", - (mention ? "\26" : ""), - hash(msg->user), + "%s%s\3%d%s%s%s\3%s\t%s", + italic, reverse, hash(msg->user), (action ? "* " : notice ? "-" : "<"), msg->nick, (action ? "" : notice ? "-" : ">"), + reverse, msg->params[1] ); } diff --git a/ui.c b/ui.c index b0e30b9..9f0bb88 100644 --- a/ui.c +++ b/ui.c @@ -39,7 +39,7 @@ #undef lines #ifndef A_ITALIC -#define A_ITALIC A_UNDERLINE +#define A_ITALIC A_NORMAL #endif #define BOTTOM (LINES - 1) -- cgit 1.4.1-2-gfad0 From 9cff026b5a7ed15e5e34f51e796908e77a3bc3b4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 19:05:51 -0500 Subject: Show input in italics for actions and set Debug prompt --- ui.c | 58 +++++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/ui.c b/ui.c index 9f0bb88..96f722a 100644 --- a/ui.c +++ b/ui.c @@ -474,36 +474,44 @@ static void inputAdd(struct Style *style, const char *str) { } static void inputUpdate(void) { - wmove(input, 0, 0); - wattr_set( - input, - colorAttr(mapColor(self.color)), - colorPair(mapColor(self.color), -1), - NULL - ); + size_t id = windows.active->id; + const char *nick = self.nick; const char *head = editHead(); const char *skip = NULL; - if (self.nick) { - size_t id = windows.active->id; - if (NULL != (skip = commandIsPrivmsg(id, head))) { - waddch(input, '<'); - waddstr(input, self.nick); - waddstr(input, "> "); - } else if (NULL != (skip = commandIsNotice(id, head))) { - waddch(input, '-'); - waddstr(input, self.nick); - waddstr(input, "- "); - } else if (NULL != (skip = commandIsAction(id, head))) { - waddstr(input, "* "); - waddstr(input, self.nick); - waddch(input, ' '); - } + const char *pre = ""; + const char *suf = " "; + struct Style style = { .fg = self.color, .bg = Default }; + if (NULL != (skip = commandIsPrivmsg(id, head))) { + pre = "<"; + suf = "> "; + } else if (NULL != (skip = commandIsNotice(id, head))) { + pre = "-"; + suf = "- "; + } else if (NULL != (skip = commandIsAction(id, head))) { + style.attr |= A_ITALIC; + pre = "* "; + } else if (id == Debug) { + skip = head; + style.fg = Gray; + pre = "<<"; + nick = NULL; } - if (skip) head = skip; int y, x; - struct Style style = Reset; - inputAdd(&style, head); + wmove(input, 0, 0); + if (skip) { + wattr_set( + input, + style.attr | colorAttr(mapColor(style.fg)), + colorPair(mapColor(style.fg), mapColor(style.bg)), + NULL + ); + waddstr(input, pre); + if (nick) waddstr(input, nick); + waddstr(input, suf); + } + style.fg = Default; + inputAdd(&style, (skip ? skip : head)); getyx(input, y, x); inputAdd(&style, editTail()); wclrtoeol(input); -- cgit 1.4.1-2-gfad0 From 87e42cc62768435dea48a86a60729cd5696f67f1 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 22:59:49 -0500 Subject: Color notices LightGray by default --- handle.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/handle.c b/handle.c index ab482fd..fb49206 100644 --- a/handle.c +++ b/handle.c @@ -294,18 +294,26 @@ static void handlePrivmsg(struct Message *msg) { bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); bool mention = !mine && isMention(msg); - const char *italic = (action ? "\35" : ""); - const char *reverse = (mention ? "\26" : ""); - uiFormat( - id, (!notice && (mention || query) ? Hot : Warm), tagTime(msg), - "%s%s\3%d%s%s%s\3%s\t%s", - italic, reverse, hash(msg->user), - (action ? "* " : notice ? "-" : "<"), - msg->nick, - (action ? "" : notice ? "-" : ">"), - reverse, - msg->params[1] - ); + if (notice) { + uiFormat( + id, Warm, tagTime(msg), + "%s\3%d-%s-\17\3%d\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + LightGray, msg->params[1] + ); + } else if (action) { + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\35\3%d* %s\17\35\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] + ); + } else { + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\3%d<%s>\17\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] + ); + } } static void handlePing(struct Message *msg) { -- cgit 1.4.1-2-gfad0 From ea7e919a1d4816486da7ee9a77767652b475bb10 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 23:11:35 -0500 Subject: Color notices LightGray in input --- ui.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 96f722a..b0f95aa 100644 --- a/ui.c +++ b/ui.c @@ -481,15 +481,18 @@ static void inputUpdate(void) { const char *pre = ""; const char *suf = " "; struct Style style = { .fg = self.color, .bg = Default }; + struct Style reset = Reset; if (NULL != (skip = commandIsPrivmsg(id, head))) { pre = "<"; suf = "> "; } else if (NULL != (skip = commandIsNotice(id, head))) { pre = "-"; suf = "- "; + reset.fg = LightGray; } else if (NULL != (skip = commandIsAction(id, head))) { style.attr |= A_ITALIC; pre = "* "; + reset.attr |= A_ITALIC; } else if (id == Debug) { skip = head; style.fg = Gray; @@ -510,7 +513,7 @@ static void inputUpdate(void) { if (nick) waddstr(input, nick); waddstr(input, suf); } - style.fg = Default; + style = reset; inputAdd(&style, (skip ? skip : head)); getyx(input, y, x); inputAdd(&style, editTail()); -- cgit 1.4.1-2-gfad0 From 273207b19f4ff1302ec0c95bbb60d49f2af3415c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 23:11:48 -0500 Subject: Flush stdout after using putp --- ui.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui.c b/ui.c index b0f95aa..ffe8748 100644 --- a/ui.c +++ b/ui.c @@ -163,6 +163,7 @@ static const char *ExitPasteMode = "\33[?2004l"; void uiShow(void) { putp(EnterFocusMode); putp(EnterPasteMode); + fflush(stdout); } void uiHide(void) { @@ -333,6 +334,7 @@ static void statusUpdate(void) { putp(to_status_line); putp(buf); putp(from_status_line); + fflush(stdout); } static void unmark(void) { -- cgit 1.4.1-2-gfad0 From 5470254fa5fd0f6108b1f075d9ac2dd24afa7fdc Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Thu, 6 Feb 2020 23:49:27 -0500 Subject: Add simple configure script Mostly motivated by wanting to build with the ncurses in pkgsrc because it supports italics. --- .gitignore | 1 + Makefile | 8 +++----- configure | 11 +++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100755 configure diff --git a/.gitignore b/.gitignore index 64b2b13..4cc4220 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.o catgirl +config.mk tags diff --git a/Makefile b/Makefile index 213ecb5..5380d20 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,7 @@ -LIBRESSL_PREFIX = /usr/local -CFLAGS += -I${LIBRESSL_PREFIX}/include -LDFLAGS += -L${LIBRESSL_PREFIX}/lib - CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -LDLIBS = -lcurses -lcrypto -ltls +LDLIBS = -lcrypto -ltls -lncursesw + +-include config.mk OBJS += chat.o OBJS += command.o diff --git a/configure b/configure new file mode 100755 index 0000000..90e1173 --- /dev/null +++ b/configure @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +libs='libcrypto libtls ncursesw' +pkg-config --print-errors $libs + +cat >config.mk < Cap) return; + memmove(&buf[index + count], &buf[index], sizeof(*buf) * (len - index)); + len += count; +} + +static void delete(size_t index, size_t count) { + if (index + count > len) return; + memmove( + &buf[index], &buf[index + count], sizeof(*buf) * (len - index - count) + ); + len -= count; +} + void edit(size_t id, enum Edit op, wchar_t ch) { switch (op) { - break; case EditKill: len = pos = 0; + 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 EditInsert: { - if (len == Cap) break; - buf[pos++] = ch; - len++; + reserve(pos, 1); + if (pos < Cap) buf[pos++] = ch; } break; case EditEnter: { pos = 0; diff --git a/ui.c b/ui.c index ffe8748..f73020a 100644 --- a/ui.c +++ b/ui.c @@ -546,12 +546,20 @@ void uiShowNum(size_t num) { } static void keyCode(int code) { + size_t id = windows.active->id; switch (code) { break; case KEY_RESIZE: resize(); break; case KeyFocusIn: unmark(); break; case KeyFocusOut: windows.active->mark = true; break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO + + break; case KEY_BACKSPACE: edit(id, EditErase, 0); + break; case KEY_END: edit(id, EditEnd, 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); } } @@ -567,6 +575,10 @@ static void keyMeta(wchar_t ch) { static void keyCtrl(wchar_t ch) { size_t id = windows.active->id; switch (ch) { + break; case L'?': edit(id, EditErase, 0); + break; case L'A': edit(id, EditHome, 0); + break; case L'E': edit(id, EditEnd, 0); + break; case L'H': edit(id, EditErase, 0); break; case L'J': edit(id, EditEnter, 0); break; case L'L': clearok(curscr, true); break; case L'U': edit(id, EditKill, 0); -- 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(-) 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(-) 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 aed762368d8500fdb364e316e63486f801a453f7 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 20:28:22 -0500 Subject: Show one cell to the right of the input cursor --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index d4ef241..f71672b 100644 --- a/ui.c +++ b/ui.c @@ -226,7 +226,7 @@ void uiDraw(void) { getyx(input, y, x); pnoutrefresh( input, - 0, (x > RIGHT ? x - RIGHT : 0), + 0, (x + 1 > RIGHT ? x + 1 - RIGHT : 0), BOTTOM, 0, BOTTOM, RIGHT ); -- cgit 1.4.1-2-gfad0 From 5881a96638b63475bf48d506dbb659c481279790 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 20:29:32 -0500 Subject: Only treat the first tab as the alignment point --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index f71672b..f556719 100644 --- a/ui.c +++ b/ui.c @@ -368,7 +368,7 @@ static void wordWrap(WINDOW *win, const char *str) { int align = 0; struct Style style = Reset; while (*str) { - if (*str == '\t') { + if (*str == '\t' && !align) { waddch(win, ' '); getyx(win, y, align); str++; -- cgit 1.4.1-2-gfad0 From a26c9ae0bc4b74b250031492b6c5a0bee39dd4d8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 20:41:27 -0500 Subject: Use define_key for meta keys This will allow distinguishing meta from escape via ESCDELAY (which should probably be set to something quite a lot shorter than its default). --- ui.c | 60 +++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/ui.c b/ui.c index f556719..147381e 100644 --- a/ui.c +++ b/ui.c @@ -146,13 +146,6 @@ static short colorPair(short fg, short bg) { return colorPairs++; } -enum { - KeyFocusIn = KEY_MAX + 1, - KeyFocusOut, - KeyPasteOn, - KeyPasteOff, -}; - // XXX: Assuming terminals will be fine with these even if they're unsupported, // since they're "private" modes. static const char *EnterFocusMode = "\33[?1004h"; @@ -188,6 +181,30 @@ static void errExit(int eval) { reset_shell_mode(); } +#define ENUM_KEY \ + X(KeyMeta0, "\0330") \ + X(KeyMeta1, "\0331") \ + X(KeyMeta2, "\0332") \ + X(KeyMeta3, "\0333") \ + X(KeyMeta4, "\0334") \ + X(KeyMeta5, "\0335") \ + X(KeyMeta6, "\0336") \ + X(KeyMeta7, "\0337") \ + X(KeyMeta8, "\0338") \ + X(KeyMeta9, "\0339") \ + X(KeyMetaM, "\33m") \ + X(KeyFocusIn, "\33[I") \ + X(KeyFocusOut, "\33[O") \ + X(KeyPasteOn, "\33[200~") \ + X(KeyPasteOff, "\33[201~") + +enum { + KeyMax = KEY_MAX, +#define X(id, seq) id, + ENUM_KEY +#undef X +}; + void uiInit(void) { initscr(); cbreak(); @@ -200,10 +217,9 @@ void uiInit(void) { to_status_line = "\33]2;"; from_status_line = "\7"; } - define_key("\33[I", KeyFocusIn); - define_key("\33[O", KeyFocusOut); - define_key("\33[200~", KeyPasteOn); - define_key("\33[201~", KeyPasteOff); +#define X(id, seq) define_key(seq, id); + ENUM_KEY +#undef X colorInit(); status = newwin(1, COLS, 0, 0); @@ -556,20 +572,19 @@ static void keyCode(int code) { break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO + break; case KeyMetaM: waddch(windows.active->pad, '\n'); + break; case KEY_BACKSPACE: edit(id, EditErase, 0); break; case KEY_END: edit(id, EditEnd, 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); - } -} - -static void keyMeta(wchar_t ch) { - switch (ch) { - break; case L'm': waddch(windows.active->pad, '\n'); + break; default: { - if (ch >= L'0' && ch <= L'9') uiShowNum(ch - L'0'); + if (code >= KeyMeta0 && code <= KeyMeta9) { + uiShowNum(code - KeyMeta0); + } } } } @@ -602,15 +617,10 @@ static void keyStyle(wchar_t ch) { void uiRead(void) { int ret; wint_t ch; - static bool meta, style; + static bool style; while (ERR != (ret = wget_wch(input, &ch))) { if (ret == KEY_CODE_YES) { keyCode(ch); - } else if (ch == '\33') { - meta = true; - continue; - } else if (meta) { - keyMeta(ch); } else if (ch == (L'Z' ^ L'@')) { style = true; continue; @@ -621,7 +631,7 @@ void uiRead(void) { } else { edit(windows.active->id, EditInsert, ch); } - meta = style = false; + style = false; } inputUpdate(); } -- cgit 1.4.1-2-gfad0 From fe5fd897052abd1909d1536056936a0417666459 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 21:30:25 -0500 Subject: Populate completion with commands --- Makefile | 1 + chat.c | 1 + chat.h | 8 +++++ command.c | 6 ++++ complete.c | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ edit.c | 3 ++ ui.c | 1 + 7 files changed, 130 insertions(+) create mode 100644 complete.c diff --git a/Makefile b/Makefile index 5380d20..48aba7b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ LDLIBS = -lcrypto -ltls -lncursesw OBJS += chat.o OBJS += command.o +OBJS += complete.o OBJS += config.o OBJS += edit.o OBJS += handle.o diff --git a/chat.c b/chat.c index c487722..91da6e3 100644 --- a/chat.c +++ b/chat.c @@ -110,6 +110,7 @@ int main(int argc, char *argv[]) { set(&self.network, host); set(&self.chanTypes, "#&"); set(&self.prefixes, "@+"); + commandComplete(); FILE *certFile = NULL; FILE *privFile = NULL; diff --git a/chat.h b/chat.h index a327620..f164e7a 100644 --- a/chat.h +++ b/chat.h @@ -118,6 +118,7 @@ void command(size_t id, char *input); const char *commandIsPrivmsg(size_t id, const char *input); const char *commandIsNotice(size_t id, const char *input); const char *commandIsAction(size_t id, const char *input); +void commandComplete(void); enum Heat { Cold, Warm, Hot }; void uiInit(void); @@ -140,12 +141,19 @@ enum Edit { EditKill, EditErase, EditInsert, + EditComplete, EditEnter, }; void edit(size_t id, enum Edit op, wchar_t ch); char *editHead(void); char *editTail(void); +const char *complete(size_t id, const char *prefix); +void completeAccept(void); +void completeReject(void); +void completeAdd(size_t id, const char *str, enum Color color); +void completeTouch(size_t id, const char *str, enum Color color); + 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 3215322..41aacc9 100644 --- a/command.c +++ b/command.c @@ -136,3 +136,9 @@ void command(size_t id, char *input) { } } } + +void commandComplete(void) { + for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { + completeAdd(None, Commands[i].cmd, Default); + } +} diff --git a/complete.c b/complete.c new file mode 100644 index 0000000..b8f2dfc --- /dev/null +++ b/complete.c @@ -0,0 +1,110 @@ +/* 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 "chat.h" + +struct Node { + size_t id; + char *str; + enum Color color; + struct Node *prev; + struct Node *next; +}; + +static struct Node *alloc(size_t id, const char *str, enum Color color) { + struct Node *node = malloc(sizeof(*node)); + if (!node) err(EX_OSERR, "malloc"); + node->id = id; + node->str = strdup(str); + if (!node->str) err(EX_OSERR, "strdup"); + node->color = color; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *head; +static struct Node *tail; + +static struct Node *detach(struct Node *node) { + if (node->prev) node->prev->next = node->next; + if (node->next) node->next->prev = node->prev; + if (head == node) head = node->next; + if (tail == node) tail = node->prev; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *prepend(struct Node *node) { + node->prev = NULL; + node->next = head; + if (head) head->prev = node; + head = node; + if (!tail) tail = node; + return node; +} + +static struct Node *append(struct Node *node) { + node->next = NULL; + node->prev = tail; + if (tail) tail->next = node; + tail = node; + if (!head) head = node; + return node; +} + +static struct Node *find(size_t id, const char *str) { + for (struct Node *node = head; node; node = node->next) { + if (node->id == id && !strcmp(node->str, str)) return node; + } + return NULL; +} + +void completeAdd(size_t id, const char *str, enum Color color) { + if (!find(id, str)) append(alloc(id, str, color)); +} + +void completeTouch(size_t id, const char *str, enum Color color) { + struct Node *node = find(id, str); + prepend(node ? detach(node) : alloc(id, str, color)); +} + +static struct Node *match; + +const char *complete(size_t id, const char *prefix) { + for (match = (match ? match->next : head); match; match = match->next) { + if (match->id && match->id != id) continue; + if (strncasecmp(match->str, prefix, strlen(prefix))) continue; + return match->str; + } + return NULL; +} + +void completeAccept(void) { + if (match) prepend(detach(match)); + match = NULL; +} + +void completeReject(void) { + match = NULL; +} diff --git a/edit.c b/edit.c index b6edb98..0c50f33 100644 --- a/edit.c +++ b/edit.c @@ -73,6 +73,9 @@ void edit(size_t id, enum Edit op, wchar_t ch) { reserve(pos, 1); if (pos < Cap) buf[pos++] = ch; } + break; case EditComplete: { + // TODO + } break; case EditEnter: { pos = 0; command(id, editTail()); diff --git a/ui.c b/ui.c index 147381e..5a8f155 100644 --- a/ui.c +++ b/ui.c @@ -596,6 +596,7 @@ static void keyCtrl(wchar_t ch) { break; case L'A': edit(id, EditHome, 0); break; case L'E': edit(id, EditEnd, 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); break; case L'L': clearok(curscr, true); break; case L'U': edit(id, EditKill, 0); -- 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(-) 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 09579052a9affcb847e46d5241c8fd8978711b0a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 22:05:34 -0500 Subject: Update color in completeTouch --- complete.c | 1 + 1 file changed, 1 insertion(+) diff --git a/complete.c b/complete.c index b8f2dfc..8247149 100644 --- a/complete.c +++ b/complete.c @@ -86,6 +86,7 @@ void completeAdd(size_t id, const char *str, enum Color color) { void completeTouch(size_t id, const char *str, enum Color color) { struct Node *node = find(id, str); + if (node && node->color != color) node->color = color; prepend(node ? detach(node) : alloc(id, str, color)); } -- cgit 1.4.1-2-gfad0 From ef9bea6d601742b8e91eda59b914f8653463ef24 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 22:25:09 -0500 Subject: Use atexit instead of err_set_exit Unsurprisingly, err_set_exit doesn't exist in GNU's err.h, but since it's safe to call reset_shell_mode on any kind of exit, just use atexit. --- ui.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 5a8f155..d9b067b 100644 --- a/ui.c +++ b/ui.c @@ -176,8 +176,7 @@ static void disableFlowControl(void) { if (error) err(EX_OSERR, "tcsetattr"); } -static void errExit(int eval) { - (void)eval; +static void errExit(void) { reset_shell_mode(); } @@ -211,7 +210,7 @@ void uiInit(void) { noecho(); disableFlowControl(); def_prog_mode(); - err_set_exit(errExit); + atexit(errExit); if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; -- cgit 1.4.1-2-gfad0 From d314523b90f41cfdbca867ad0ad48f2f68f66c58 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 23:33:23 -0500 Subject: Update completion on join, part, privmsg --- chat.h | 1 + complete.c | 13 +++++++++++++ handle.c | 8 +++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/chat.h b/chat.h index f164e7a..aec5a68 100644 --- a/chat.h +++ b/chat.h @@ -153,6 +153,7 @@ void completeAccept(void); void completeReject(void); void completeAdd(size_t id, const char *str, enum Color color); void completeTouch(size_t id, const char *str, enum Color color); +void completeRemove(size_t id, const char *str); FILE *configOpen(const char *path, const char *mode); int getopt_config( diff --git a/complete.c b/complete.c index 8247149..5067512 100644 --- a/complete.c +++ b/complete.c @@ -109,3 +109,16 @@ void completeAccept(void) { void completeReject(void) { match = NULL; } + +void completeRemove(size_t id, const char *str) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = next) { + next = node->next; + if (id && node->id != id) continue; + if (strcmp(node->str, str)) continue; + if (match == node) match = NULL; + detach(node); + free(node->str); + free(node); + } +} diff --git a/handle.c b/handle.c index fb49206..4faabba 100644 --- a/handle.c +++ b/handle.c @@ -154,6 +154,7 @@ static void handleErrorSASLFail(struct Message *msg) { static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); + completeTouch(None, self.nick, Default); if (self.join) ircFormat("JOIN %s\r\n", self.join); } @@ -197,8 +198,10 @@ static void handleJoin(struct Message *msg) { self.color = hash(msg->user); } idColors[id] = hash(msg->params[0]); + completeTouch(None, msg->params[0], idColors[id]); uiShowID(id); } + completeTouch(id, msg->nick, hash(msg->user)); uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tarrives in \3%02d%s\3", @@ -208,8 +211,10 @@ static void handleJoin(struct Message *msg) { static void handlePart(struct Message *msg) { require(msg, true, 1); + size_t id = idFor(msg->params[0]); + completeRemove(id, msg->nick); uiFormat( - idFor(msg->params[0]), Cold, tagTime(msg), + id, Cold, tagTime(msg), "\3%02d%s\3\tleaves \3%02d%s\3%s%s", hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], (msg->params[1] ? ": " : ""), @@ -294,6 +299,7 @@ static void handlePrivmsg(struct Message *msg) { bool notice = (msg->cmd[0] == 'N'); bool action = isAction(msg); bool mention = !mine && isMention(msg); + if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user)); if (notice) { uiFormat( id, Warm, tagTime(msg), -- cgit 1.4.1-2-gfad0 From 8b7cc1a0ed95e8a3ff413fa77eb12a3dca7fccb4 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 23:44:03 -0500 Subject: Clear completion for ID on self-part --- chat.h | 1 + complete.c | 12 ++++++++++++ handle.c | 3 +++ 3 files changed, 16 insertions(+) diff --git a/chat.h b/chat.h index aec5a68..6eeed60 100644 --- a/chat.h +++ b/chat.h @@ -154,6 +154,7 @@ void completeReject(void); void completeAdd(size_t id, const char *str, enum Color color); void completeTouch(size_t id, const char *str, enum Color color); void completeRemove(size_t id, const char *str); +void completeClear(size_t id); FILE *configOpen(const char *path, const char *mode); int getopt_config( diff --git a/complete.c b/complete.c index 5067512..437bb7d 100644 --- a/complete.c +++ b/complete.c @@ -122,3 +122,15 @@ void completeRemove(size_t id, const char *str) { free(node); } } + +void completeClear(size_t id) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = next) { + next = node->next; + if (node->id != id) continue; + if (match == node) match = NULL; + detach(node); + free(node->str); + free(node); + } +} diff --git a/handle.c b/handle.c index 4faabba..b73d200 100644 --- a/handle.c +++ b/handle.c @@ -212,6 +212,9 @@ static void handleJoin(struct Message *msg) { static void handlePart(struct Message *msg) { require(msg, true, 1); size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + completeClear(id); + } completeRemove(id, msg->nick); uiFormat( id, Cold, tagTime(msg), -- cgit 1.4.1-2-gfad0 From 71b05365368b400593c974a4e69a9369c4496036 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 23:55:46 -0500 Subject: Revert "Only treat the first tab as the alignment point" This reverts commit 5881a96638b63475bf48d506dbb659c481279790. --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index d9b067b..374fa93 100644 --- a/ui.c +++ b/ui.c @@ -383,7 +383,7 @@ static void wordWrap(WINDOW *win, const char *str) { int align = 0; struct Style style = Reset; while (*str) { - if (*str == '\t' && !align) { + if (*str == '\t') { waddch(win, ' '); getyx(win, y, align); str++; -- cgit 1.4.1-2-gfad0 From 0705f0931094706a3b758f33f487cf25a6f03cab Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Fri, 7 Feb 2020 23:56:41 -0500 Subject: Only treat the first tab as the alignment point --- ui.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 374fa93..e9ca3ef 100644 --- a/ui.c +++ b/ui.c @@ -384,9 +384,14 @@ static void wordWrap(WINDOW *win, const char *str) { struct Style style = Reset; while (*str) { if (*str == '\t') { - waddch(win, ' '); - getyx(win, y, align); - str++; + if (align) { + waddch(win, '\t'); + str++; + } else { + waddch(win, ' '); + getyx(win, y, align); + str++; + } } else if (*str == ' ') { getyx(win, y, x); const char *word = &str[strspn(str, " ")]; -- cgit 1.4.1-2-gfad0 From 58e1d5b4e2fabead1aae356dd060bfc9748bdd5e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 00:01:59 -0500 Subject: Handle NICK --- chat.h | 2 ++ complete.c | 21 +++++++++++++++++++++ handle.c | 17 +++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/chat.h b/chat.h index 6eeed60..413cee4 100644 --- a/chat.h +++ b/chat.h @@ -151,8 +151,10 @@ char *editTail(void); const char *complete(size_t id, const char *prefix); void completeAccept(void); void completeReject(void); +size_t completeID(const char *str); void completeAdd(size_t id, const char *str, enum Color color); void completeTouch(size_t id, const char *str, enum Color color); +void completeReplace(size_t id, const char *old, const char *new); void completeRemove(size_t id, const char *str); void completeClear(size_t id); diff --git a/complete.c b/complete.c index 437bb7d..c194536 100644 --- a/complete.c +++ b/complete.c @@ -110,6 +110,27 @@ void completeReject(void) { match = NULL; } +size_t completeID(const char *str) { + for (match = (match ? match->next : head); match; match = match->next) { + if (match->id && !strcmp(match->str, str)) return match->id; + } + return None; +} + +void completeReplace(size_t id, const char *old, const char *new) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = node->next) { + next = node->next; + if (id && node->id != id) continue; + if (strcmp(node->str, old)) continue; + if (match == node) match = NULL; + free(node->str); + node->str = strdup(new); + if (!node->str) err(EX_OSERR, "strdup"); + prepend(detach(node)); + } +} + void completeRemove(size_t id, const char *str) { struct Node *next = NULL; for (struct Node *node = head; node; node = next) { diff --git a/handle.c b/handle.c index b73d200..fe64f33 100644 --- a/handle.c +++ b/handle.c @@ -261,6 +261,22 @@ static void handleTopic(struct Message *msg) { } } +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (self.nick && !strcmp(msg->nick, self.nick)) { + set(&self.nick, msg->params[0]); + } + size_t id; + completeReplace(None, msg->nick, msg->params[0]); + while (None != (id = completeID(msg->params[0]))) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->user), msg->params[0] + ); + } +} + static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -354,6 +370,7 @@ static const struct Handler { { "CAP", handleCap }, { "ERROR", handleError }, { "JOIN", handleJoin }, + { "NICK", handleNick }, { "NOTICE", handlePrivmsg }, { "PART", handlePart }, { "PING", handlePing }, -- 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(+) 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 7ebfeea33041f3d438e2de84760af950000393db Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 00:25:09 -0500 Subject: Handle NAMES reply --- handle.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/handle.c b/handle.c index fe64f33..44329ff 100644 --- a/handle.c +++ b/handle.c @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -225,6 +226,32 @@ static void handlePart(struct Message *msg) { ); } +static void handleReplyNames(struct Message *msg) { + require(msg, false, 4); + size_t id = idFor(msg->params[2]); + char buf[1024]; + size_t len = 0; + while (msg->params[3]) { + char *name = strsep(&msg->params[3], " "); + name += strspn(name, self.prefixes); + char *nick = strsep(&name, "!"); + char *user = strsep(&name, "@"); + enum Color color = (user ? hash(user) : Default); + completeAdd(id, nick, color); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), color, nick + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + uiFormat( + id, Cold, tagTime(msg), + "In \3%02d%s\3 are %s", + hash(msg->params[2]), msg->params[2], buf + ); +} + static void handleReplyNoTopic(struct Message *msg) { require(msg, false, 2); uiFormat( @@ -359,6 +386,7 @@ static const struct Handler { { "005", handleReplyISupport }, { "331", handleReplyNoTopic }, { "332", handleReplyTopic }, + { "353", handleReplyNames }, { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, -- cgit 1.4.1-2-gfad0 From f14175ebede46eb9e1fbf239a5c3b349951d34fc Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 00:36:23 -0500 Subject: Handle QUIT --- handle.c | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/handle.c b/handle.c index 44329ff..de9e73a 100644 --- a/handle.c +++ b/handle.c @@ -226,6 +226,37 @@ static void handlePart(struct Message *msg) { ); } +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (self.nick && !strcmp(msg->nick, self.nick)) { + set(&self.nick, msg->params[0]); + } + size_t id; + while (None != (id = completeID(msg->nick))) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->user), msg->params[0] + ); + } + completeReplace(None, msg->nick, msg->params[0]); +} + +static void handleQuit(struct Message *msg) { + require(msg, true, 0); + size_t id; + while (None != (id = completeID(msg->nick))) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tleaves%s%s", + hash(msg->user), msg->nick, + (msg->params[0] ? ": " : ""), + (msg->params[0] ? msg->params[0] : "") + ); + } + completeRemove(None, msg->nick); +} + static void handleReplyNames(struct Message *msg) { require(msg, false, 4); size_t id = idFor(msg->params[2]); @@ -288,22 +319,6 @@ static void handleTopic(struct Message *msg) { } } -static void handleNick(struct Message *msg) { - require(msg, true, 1); - if (self.nick && !strcmp(msg->nick, self.nick)) { - set(&self.nick, msg->params[0]); - } - size_t id; - completeReplace(None, msg->nick, msg->params[0]); - while (None != (id = completeID(msg->params[0]))) { - uiFormat( - id, Cold, tagTime(msg), - "\3%02d%s\3\tis now known as \3%02d%s\3", - hash(msg->user), msg->nick, hash(msg->user), msg->params[0] - ); - } -} - static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -403,6 +418,7 @@ static const struct Handler { { "PART", handlePart }, { "PING", handlePing }, { "PRIVMSG", handlePrivmsg }, + { "QUIT", handleQuit }, { "TOPIC", handleTopic }, }; -- cgit 1.4.1-2-gfad0 From b5707af4b842f521104c5fba07e5685612ff91f2 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 00:58:17 -0500 Subject: Handle KICK See I knew the color cache in complete would be useful in at least one place! --- chat.h | 3 ++- complete.c | 5 +++++ handle.c | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/chat.h b/chat.h index 413cee4..bd36d27 100644 --- a/chat.h +++ b/chat.h @@ -151,12 +151,13 @@ char *editTail(void); const char *complete(size_t id, const char *prefix); void completeAccept(void); void completeReject(void); -size_t completeID(const char *str); void completeAdd(size_t id, const char *str, enum Color color); void completeTouch(size_t id, const char *str, enum Color color); void completeReplace(size_t id, const char *old, const char *new); void completeRemove(size_t id, const char *str); void completeClear(size_t id); +size_t completeID(const char *str); +enum Color completeColor(size_t id, const char *str); FILE *configOpen(const char *path, const char *mode); int getopt_config( diff --git a/complete.c b/complete.c index c194536..2f5275f 100644 --- a/complete.c +++ b/complete.c @@ -90,6 +90,11 @@ void completeTouch(size_t id, const char *str, enum Color color) { prepend(node ? detach(node) : alloc(id, str, color)); } +enum Color completeColor(size_t id, const char *str) { + struct Node *node = find(id, str); + return (node ? node->color : Default); +} + static struct Node *match; const char *complete(size_t id, const char *prefix) { diff --git a/handle.c b/handle.c index de9e73a..8ebc3b1 100644 --- a/handle.c +++ b/handle.c @@ -226,6 +226,25 @@ static void handlePart(struct Message *msg) { ); } +static void handleKick(struct Message *msg) { + require(msg, true, 2); + size_t id = idFor(msg->params[0]); + bool kicked = self.nick && !strcmp(msg->params[1], self.nick); + completeTouch(id, msg->nick, hash(msg->user)); + 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", + (kicked ? "\26" : ""), + hash(msg->user), msg->nick, + completeColor(id, msg->params[1]), msg->params[1], + hash(msg->params[0]), msg->params[0], + (msg->params[2] ? ": " : ""), + (msg->params[2] ? msg->params[2] : "") + ); + completeRemove(id, msg->params[1]); + if (kicked) completeClear(id); +} + static void handleNick(struct Message *msg) { require(msg, true, 1); if (self.nick && !strcmp(msg->nick, self.nick)) { @@ -413,6 +432,7 @@ static const struct Handler { { "CAP", handleCap }, { "ERROR", handleError }, { "JOIN", handleJoin }, + { "KICK", handleKick }, { "NICK", handleNick }, { "NOTICE", handlePrivmsg }, { "PART", handlePart }, -- cgit 1.4.1-2-gfad0 From fda510b8761f487c36988eb70a6d95bd0d583aed Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 01:14:41 -0500 Subject: Handle ID overflow idk just shove it all in . --- chat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/chat.h b/chat.h index bd36d27..081370e 100644 --- a/chat.h +++ b/chat.h @@ -49,6 +49,7 @@ static inline size_t idFind(const char *name) { static inline size_t idFor(const char *name) { size_t id = idFind(name); if (id) return id; + if (idNext == IDCap) return Network; idNames[idNext] = strdup(name); if (!idNames[idNext]) err(EX_OSERR, "strdup"); idColors[idNext] = Default; -- 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(-) 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(+) 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 55e721da42bb57833e9c99e2b87cf50d6c035f07 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 02:13:02 -0500 Subject: Check return values of newwin/newpad --- ui.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index e9ca3ef..6d1338b 100644 --- a/ui.c +++ b/ui.c @@ -109,6 +109,7 @@ static struct Window *windowFor(size_t id) { window->id = id; window->pad = newpad(BufferCap, COLS); + if (!window->pad) err(EX_OSERR, "newpad"); scrollok(window->pad, true); wmove(window->pad, BufferCap - 1, 0); window->scroll = BufferCap; @@ -211,20 +212,25 @@ void uiInit(void) { disableFlowControl(); def_prog_mode(); atexit(errExit); + colorInit(); if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; from_status_line = "\7"; } + #define X(id, seq) define_key(seq, id); ENUM_KEY #undef X - colorInit(); status = newwin(1, COLS, 0, 0); + if (!status) err(EX_OSERR, "newwin"); + input = newpad(1, 512); + if (!input) err(EX_OSERR, "newpad"); keypad(input, true); nodelay(input, true); + windows.active = windowFor(Network); uiShow(); } -- 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(+) 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(+) 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 b6bf6d62b0bb6d203ce41e4b375c415ca8fde719 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 03:15:17 -0500 Subject: Only show expected topic/names replies --- chat.h | 5 +++++ command.c | 8 ++++++++ handle.c | 23 ++++++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/chat.h b/chat.h index 9daa38c..34e1812 100644 --- a/chat.h +++ b/chat.h @@ -114,6 +114,11 @@ void ircSend(const char *ptr, size_t len); void ircFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); +extern struct Replies { + size_t topic; + size_t names; +} replies; + void handle(struct Message msg); void command(size_t id, char *input); const char *commandIsPrivmsg(size_t id, const char *input); diff --git a/command.c b/command.c index 1d1c756..9879dbe 100644 --- a/command.c +++ b/command.c @@ -71,7 +71,15 @@ static void commandMe(size_t id, char *params) { } static void commandJoin(size_t id, char *params) { + size_t count = 1; + if (params) { + for (char *ch = params; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + } ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); + replies.topic += count; + replies.names += count; } static void commandPart(size_t id, char *params) { diff --git a/handle.c b/handle.c index 8ebc3b1..0780767 100644 --- a/handle.c +++ b/handle.c @@ -25,6 +25,8 @@ #include "chat.h" +struct Replies replies; + static const char *CapNames[] = { #define X(name, id) [id##Bit] = name, ENUM_CAP @@ -156,7 +158,15 @@ static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); completeTouch(None, self.nick, Default); - if (self.join) ircFormat("JOIN %s\r\n", self.join); + if (self.join) { + size_t count = 1; + for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", self.join); + replies.topic += count; + replies.names += count; + } } static void handleReplyISupport(struct Message *msg) { @@ -278,6 +288,7 @@ static void handleQuit(struct Message *msg) { static void handleReplyNames(struct Message *msg) { require(msg, false, 4); + if (!replies.names) return; size_t id = idFor(msg->params[2]); char buf[1024]; size_t len = 0; @@ -302,8 +313,15 @@ static void handleReplyNames(struct Message *msg) { ); } +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies.names) replies.names--; +} + static void handleReplyNoTopic(struct Message *msg) { require(msg, false, 2); + if (!replies.topic) return; + replies.topic--; uiFormat( idFor(msg->params[1]), Cold, tagTime(msg), "There is no sign in \3%02d%s\3", @@ -313,6 +331,8 @@ static void handleReplyNoTopic(struct Message *msg) { static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); + if (!replies.topic) return; + replies.topic--; uiFormat( idFor(msg->params[1]), Cold, tagTime(msg), "The sign in \3%02d%s\3 reads: %s", @@ -421,6 +441,7 @@ static const struct Handler { { "331", handleReplyNoTopic }, { "332", handleReplyTopic }, { "353", handleReplyNames }, + { "366", handleReplyEndOfNames }, { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, -- 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(+) 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(+) 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 29bd788660af90855f6acce411506aeaf14f8808 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 16:56:49 -0500 Subject: Simplify(?) reflow buffer loop --- ui.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index c738617..29062be 100644 --- a/ui.c +++ b/ui.c @@ -459,10 +459,12 @@ void uiFormat( static void reflow(struct Window *window) { werase(window->pad); wmove(window->pad, BufferCap - 1, 0); - size_t len = window->buffer.len; - for (size_t i = (len > BufferCap ? len - BufferCap : 0); i < len; ++i) { + struct Buffer *buffer = &window->buffer; + for (size_t i = 0; i < BufferCap; ++i) { + char *line = buffer->lines[(buffer->len + i) % BufferCap]; + if (!line) continue; waddch(window->pad, '\n'); - wordWrap(window->pad, window->buffer.lines[i % BufferCap]); + wordWrap(window->pad, line); } } -- cgit 1.4.1-2-gfad0 From 72d8749454820cf025f05a8ab9cc1cff4c8c5b6e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 17:04:25 -0500 Subject: Check signals after file descriptors If a signal happens while processing an FD, it should be handled immediately, rather than waiting for another poll return. --- chat.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat.c b/chat.c index 91da6e3..b3e6825 100644 --- a/chat.c +++ b/chat.c @@ -150,6 +150,8 @@ int main(int argc, char *argv[]) { while (!self.quit) { int nfds = poll(fds, 2, -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); + if (nfds > 0 && fds[0].revents) uiRead(); + if (nfds > 0 && fds[1].revents) ircRecv(); if (signals[SIGHUP]) self.quit = "zzz"; if (signals[SIGINT] || signals[SIGTERM]) break; @@ -162,8 +164,6 @@ int main(int argc, char *argv[]) { uiRead(); } - if (nfds > 0 && fds[0].revents) uiRead(); - if (nfds > 0 && fds[1].revents) ircRecv(); uiDraw(); } -- cgit 1.4.1-2-gfad0 From 8128edc7eb1e7a86e82d5936ec1100e1f9912f54 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 17:22:51 -0500 Subject: Handle SIGCHLD --- chat.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/chat.c b/chat.c index b3e6825..372cbbd 100644 --- a/chat.c +++ b/chat.c @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include @@ -141,6 +143,7 @@ int main(int argc, char *argv[]) { signal(SIGHUP, signalHandler); signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); + signal(SIGCHLD, signalHandler); sig_t cursesWinch = signal(SIGWINCH, signalHandler); struct pollfd fds[2] = { @@ -155,6 +158,25 @@ int main(int argc, char *argv[]) { if (signals[SIGHUP]) self.quit = "zzz"; if (signals[SIGINT] || signals[SIGTERM]) break; + + if (signals[SIGCHLD]) { + int status; + while (0 < waitpid(-1, &status, WNOHANG)) { + if (WIFEXITED(status) && WEXITSTATUS(status)) { + uiFormat( + Network, Warm, NULL, + "Process exits with status %d", WEXITSTATUS(status) + ); + } else if (WIFSIGNALED(status)) { + uiFormat( + Network, Warm, NULL, + "Process terminates from %s", + strsignal(WTERMSIG(status)) + ); + } + } + } + if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); -- cgit 1.4.1-2-gfad0 From 156282c95d523b0c19f5409eb15cd53fc3211894 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 17:42:19 -0500 Subject: Add procPipe for subprocesses --- chat.c | 30 ++++++++++++++++++++++++++---- chat.h | 2 ++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/chat.c b/chat.c index 372cbbd..ca35d7d 100644 --- a/chat.c +++ b/chat.c @@ -45,6 +45,21 @@ size_t idNext = Network + 1; struct Self self = { .color = Default }; +int procPipe[2] = { -1, -1 }; + +static void pipeRead(void) { + char buf[1024]; + ssize_t len = read(procPipe[0], buf, sizeof(buf) - 1); + if (len < 0) err(EX_IOERR, "read"); + if (!len) return; + buf[len - 1] = '\0'; + char *ptr = buf; + while (ptr) { + char *line = strsep(&ptr, "\n"); + uiFormat(Network, Warm, NULL, "%s", line); + } +} + static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { signals[signal] = 1; @@ -146,15 +161,22 @@ int main(int argc, char *argv[]) { signal(SIGCHLD, signalHandler); sig_t cursesWinch = signal(SIGWINCH, signalHandler); - struct pollfd fds[2] = { + int error = pipe(procPipe); + if (error) err(EX_OSERR, "pipe"); + + struct pollfd fds[3] = { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, + { .events = POLLIN, .fd = procPipe[0] }, }; while (!self.quit) { - int nfds = poll(fds, 2, -1); + int nfds = poll(fds, ARRAY_LEN(fds), -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); - if (nfds > 0 && fds[0].revents) uiRead(); - if (nfds > 0 && fds[1].revents) ircRecv(); + if (nfds > 0) { + if (fds[0].revents) uiRead(); + if (fds[1].revents) ircRecv(); + if (fds[2].revents) pipeRead(); + } if (signals[SIGHUP]) self.quit = "zzz"; if (signals[SIGINT] || signals[SIGTERM]) break; diff --git a/chat.h b/chat.h index 34e1812..909527e 100644 --- a/chat.h +++ b/chat.h @@ -28,6 +28,8 @@ typedef unsigned char byte; +int procPipe[2]; + enum Color { White, Black, Blue, Green, Red, Brown, Magenta, Orange, Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, -- 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 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 9b9794df33488acc82529ecb9cfe478441f5f48a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 19:04:25 -0500 Subject: Implement URL opening --- url.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/url.c b/url.c index 7790461..1396765 100644 --- a/url.c +++ b/url.c @@ -14,12 +14,15 @@ * along with this program. If not, see . */ +#include #include +#include #include #include #include #include #include +#include #include "chat.h" @@ -55,26 +58,31 @@ static void compile(void) { errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern); } +struct URL { + size_t id; + char *nick; + char *url; +}; + enum { Cap = 32 }; static struct { - size_t ids[Cap]; - char *nicks[Cap]; - char *urls[Cap]; + struct URL urls[Cap]; size_t len; } ring; +static_assert(!(Cap & (Cap - 1)), "Cap is power of two"); -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; +static void push(size_t id, const char *nick, const char *str, size_t len) { + struct URL *url = &ring.urls[ring.len++ % Cap]; + free(url->nick); + free(url->url); + url->id = id; + url->nick = NULL; if (nick) { - ring.nicks[i] = strdup(nick); - if (!ring.nicks[i]) err(EX_OSERR, "strdup"); + url->nick = strdup(nick); + if (!url->nick) err(EX_OSERR, "strdup"); } - ring.urls[i] = strndup(url, len); - if (!ring.urls[i]) err(EX_OSERR, "strndup"); + url->url = strndup(str, len); + if (!url->url) err(EX_OSERR, "strndup"); } void urlScan(size_t id, const char *nick, const char *mesg) { @@ -87,10 +95,45 @@ void urlScan(size_t id, const char *nick, const char *mesg) { } } +static const char *OpenBins[] = { "open", "xdg-open" }; + +static void urlOpen(const char *url) { + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + 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 (errno != ENOENT) { + warn("%s", OpenBins[i]); + _exit(EX_CONFIG); + } + } + warnx("no open utility found"); + _exit(EX_CONFIG); +} + void urlOpenCount(size_t id, size_t count) { - // TODO + 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; + urlOpen(url->url); + if (!--count) break; + } } void urlOpenMatch(size_t id, const char *str) { - // TODO + 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 ((url->nick && !strcmp(url->nick, str)) || strstr(url->url, str)) { + urlOpen(url->url); + break; + } + } } -- cgit 1.4.1-2-gfad0 From 362d779b61adc3f59ef7b4617b3bb7a19f024048 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 19:12:05 -0500 Subject: Set FDs CLOEXEC --- chat.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chat.c b/chat.c index ca35d7d..c0c2d28 100644 --- a/chat.c +++ b/chat.c @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -164,6 +165,10 @@ int main(int argc, char *argv[]) { int error = pipe(procPipe); if (error) err(EX_OSERR, "pipe"); + fcntl(irc, F_SETFD, FD_CLOEXEC); + fcntl(procPipe[0], F_SETFD, FD_CLOEXEC); + fcntl(procPipe[1], F_SETFD, FD_CLOEXEC); + struct pollfd fds[3] = { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, -- 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(-) 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(-) 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 af14947103775fa0251a1a1d96a9e8cae73141c9 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 21:50:29 -0500 Subject: Trim whitespace from both ends of command params --- command.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command.c b/command.c index feb52b7..f88a6d5 100644 --- a/command.c +++ b/command.c @@ -223,6 +223,12 @@ void command(size_t id, char *input) { cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); if (handler) { + if (input) { + input += strspn(input, " "); + size_t len = strlen(input); + while (input[len - 1] == ' ') input[--len] = '\0'; + if (!input[0]) input = NULL; + } if (input && !input[0]) input = NULL; handler->fn(id, input); } else { -- cgit 1.4.1-2-gfad0 From 9cbec9ca7ee5a730f92587274bb6d7713c5671bf Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 22:51:13 -0500 Subject: Color mentions Sort of like Textual does, but only in the first part of the messaage, either before a colon or before a space. Hopefully this makes it less costly than it would be, and prevents false positives on people with common nouns for nicks. --- handle.c | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/handle.c b/handle.c index f919fcb..4ec0a90 100644 --- a/handle.c +++ b/handle.c @@ -389,6 +389,53 @@ static bool isMention(const struct Message *msg) { return false; } +static const char *colorMentions(size_t id, struct Message *msg) { + char *mention; + char final; + if (strchr(msg->params[1], ':')) { + mention = strsep(&msg->params[1], ":"); + final = ':'; + } else if (strchr(msg->params[1], ' ')) { + mention = strsep(&msg->params[1], " "); + final = ' '; + } else { + mention = msg->params[1]; + msg->params[1] = ""; + final = '\0'; + } + + static char buf[1024]; + size_t len = 0; + while (*mention) { + size_t skip = strspn(mention, ", "); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%.*s", (int)skip, mention + ); + assert(n >= 0 && len + n < sizeof(buf)); + len += n; + mention += skip; + + size_t word = strcspn(mention, ", "); + char punct = mention[word]; + mention[word] = '\0'; + + n = snprintf( + &buf[len], sizeof(buf) - len, + "\3%02d%s\3", completeColor(id, mention), mention + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + + mention[word] = punct; + mention += word; + } + assert(len + 1 < sizeof(buf)); + buf[len++] = final; + buf[len] = '\0'; + return buf; +} + static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); bool query = !strchr(self.chanTypes, msg->params[0][0]); @@ -423,10 +470,12 @@ static void handlePrivmsg(struct Message *msg) { (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] ); } else { + const char *mentions = colorMentions(id, msg); uiFormat( id, (mention || query ? Hot : Warm), tagTime(msg), - "%s\3%d<%s>\17\t%s", - (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] + "%s\3%d<%s>\17\t%s%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + mentions, msg->params[1] ); } } -- cgit 1.4.1-2-gfad0 From ec73174c4c90ada7cad983f91ce42f27faa9749c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 22:55:11 -0500 Subject: Use unexpected NAMES replies to populate complete --- handle.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 4ec0a90..b4601e4 100644 --- a/handle.c +++ b/handle.c @@ -292,7 +292,6 @@ static void handleQuit(struct Message *msg) { static void handleReplyNames(struct Message *msg) { require(msg, false, 4); - if (!replies.names) return; size_t id = idFor(msg->params[2]); char buf[1024]; size_t len = 0; @@ -303,6 +302,7 @@ static void handleReplyNames(struct Message *msg) { char *user = strsep(&name, "@"); enum Color color = (user ? hash(user) : Default); completeAdd(id, nick, color); + if (!replies.names) continue; int n = snprintf( &buf[len], sizeof(buf) - len, "%s\3%02d%s\3", (len ? ", " : ""), color, nick @@ -310,6 +310,7 @@ static void handleReplyNames(struct Message *msg) { assert(n > 0 && len + n < sizeof(buf)); len += n; } + if (!replies.names) return; uiFormat( id, Cold, tagTime(msg), "In \3%02d%s\3 are %s", -- cgit 1.4.1-2-gfad0 From b30b93f67212c8d4c172972fd3399cabb8d1be6e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sat, 8 Feb 2020 23:29:32 -0500 Subject: Use fmemopen to build colored mentions string --- handle.c | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/handle.c b/handle.c index b4601e4..cf0e853 100644 --- a/handle.c +++ b/handle.c @@ -406,34 +406,25 @@ static const char *colorMentions(size_t id, struct Message *msg) { } static char buf[1024]; - size_t len = 0; + FILE *str = fmemopen(buf, sizeof(buf), "w"); + if (!str) err(EX_OSERR, "fmemopen"); + while (*mention) { size_t skip = strspn(mention, ", "); - int n = snprintf( - &buf[len], sizeof(buf) - len, - "%.*s", (int)skip, mention - ); - assert(n >= 0 && len + n < sizeof(buf)); - len += n; + fwrite(mention, skip, 1, str); mention += skip; - size_t word = strcspn(mention, ", "); - char punct = mention[word]; - mention[word] = '\0'; - - n = snprintf( - &buf[len], sizeof(buf) - len, - "\3%02d%s\3", completeColor(id, mention), mention - ); - assert(n > 0 && len + n < sizeof(buf)); - len += n; - - mention[word] = punct; - mention += word; + size_t len = strcspn(mention, ", "); + char punct = mention[len]; + mention[len] = '\0'; + fprintf(str, "\3%02d%s\3", completeColor(id, mention), mention); + mention[len] = punct; + mention += len; } - assert(len + 1 < sizeof(buf)); - buf[len++] = final; - buf[len] = '\0'; + fputc(final, str); + + fclose(str); + buf[sizeof(buf) - 1] = '\0'; return buf; } -- cgit 1.4.1-2-gfad0 From e0714a9b7e88bc2de9ec4b4c0d0a74dc3012b847 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 00:39:09 -0500 Subject: Switch to "other" window if closing active window --- ui.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 29062be..23bf929 100644 --- a/ui.c +++ b/ui.c @@ -578,7 +578,11 @@ void uiShowNum(size_t num) { 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 && windows.other != window) { + windowShow(windows.other); + } else { + windowShow(window->prev ? window->prev : window->next); + } } if (windows.other == window) windows.other = NULL; windowRemove(window); -- 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(-) 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 2d62ea9e30e7249e7f3e5bc3e60f5c8e97b3e2cc Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 01:28:24 -0500 Subject: Simplify edit buffer conversion and input rendering --- chat.h | 3 +-- edit.c | 25 +++++++++++----------- ui.c | 76 ++++++++++++++++++++++++++++++++++++------------------------------ 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/chat.h b/chat.h index 896549e..914fde6 100644 --- a/chat.h +++ b/chat.h @@ -156,8 +156,7 @@ enum Edit { EditEnter, }; void edit(size_t id, enum Edit op, wchar_t ch); -char *editHead(void); -char *editTail(void); +char *editBuffer(size_t *pos); const char *complete(size_t id, const char *prefix); void completeAccept(void); diff --git a/edit.c b/edit.c index 0c50f33..38dadcd 100644 --- a/edit.c +++ b/edit.c @@ -27,21 +27,22 @@ static wchar_t buf[Cap]; static size_t len; static size_t pos; -char *editHead(void) { +char *editBuffer(size_t *mbsPos) { static char mbs[MB_LEN_MAX * Cap]; + const wchar_t *ptr = buf; - size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL); - assert(n != (size_t)-1); - mbs[n] = '\0'; - return mbs; -} + size_t mbsLen = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL); + assert(mbsLen != (size_t)-1); + if (mbsPos) *mbsPos = mbsLen; -char *editTail(void) { - static char mbs[MB_LEN_MAX * Cap]; - const wchar_t *ptr = &buf[pos]; - size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs) - 1, NULL); + ptr = &buf[pos]; + size_t n = wcsnrtombs( + &mbs[mbsLen], &ptr, len - pos, sizeof(mbs) - mbsLen - 1, NULL + ); assert(n != (size_t)-1); - mbs[n] = '\0'; + mbsLen += n; + + mbs[mbsLen] = '\0'; return mbs; } @@ -78,7 +79,7 @@ void edit(size_t id, enum Edit op, wchar_t ch) { } break; case EditEnter: { pos = 0; - command(id, editTail()); + command(id, editBuffer(NULL)); len = 0; } } diff --git a/ui.c b/ui.c index 23bf929..c342339 100644 --- a/ui.c +++ b/ui.c @@ -507,48 +507,54 @@ static void inputAdd(struct Style *style, const char *str) { static void inputUpdate(void) { size_t id = windows.active->id; - const char *nick = self.nick; - const char *head = editHead(); + size_t pos; + char *buf = editBuffer(&pos); + const char *skip = NULL; - const char *pre = ""; - const char *suf = " "; - struct Style style = { .fg = self.color, .bg = Default }; - struct Style reset = Reset; - if (NULL != (skip = commandIsPrivmsg(id, head))) { - pre = "<"; - suf = "> "; - } else if (NULL != (skip = commandIsNotice(id, head))) { - pre = "-"; - suf = "- "; - reset.fg = LightGray; - } else if (NULL != (skip = commandIsAction(id, head))) { - style.attr |= A_ITALIC; - pre = "* "; - reset.attr |= A_ITALIC; + struct Style init = { .fg = self.color, .bg = Default }; + struct Style rest = Reset; + const char *prefix = ""; + const char *prompt = (self.nick ? self.nick : ""); + const char *suffix = ""; + if (NULL != (skip = commandIsPrivmsg(id, buf))) { + prefix = "<"; suffix = "> "; + } else if (NULL != (skip = commandIsNotice(id, buf))) { + prefix = "-"; suffix = "- "; + rest.fg = LightGray; + } else if (NULL != (skip = commandIsAction(id, buf))) { + init.attr |= A_ITALIC; + prefix = "* "; suffix = " "; + rest.attr |= A_ITALIC; } else if (id == Debug) { - skip = head; - style.fg = Gray; - pre = "<<"; - nick = NULL; + skip = buf; + init.fg = Gray; + prompt = "<< "; + } else { + prompt = ""; + } + if (skip && skip > &buf[pos]) { + skip = NULL; + prefix = prompt = suffix = ""; } int y, x; wmove(input, 0, 0); - if (skip) { - wattr_set( - input, - style.attr | colorAttr(mapColor(style.fg)), - colorPair(mapColor(style.fg), mapColor(style.bg)), - NULL - ); - waddstr(input, pre); - if (nick) waddstr(input, nick); - waddstr(input, suf); - } - style = reset; - inputAdd(&style, (skip ? skip : head)); + wattr_set( + input, + init.attr | colorAttr(mapColor(init.fg)), + colorPair(mapColor(init.fg), mapColor(init.bg)), + NULL + ); + waddstr(input, prefix); + waddstr(input, prompt); + waddstr(input, suffix); + struct Style style = rest; + char p = buf[pos]; + buf[pos] = '\0'; + inputAdd(&style, (skip ? skip : buf)); getyx(input, y, x); - inputAdd(&style, editTail()); + buf[pos] = p; + inputAdd(&style, &buf[pos]); wclrtoeol(input); wmove(input, y, x); } -- cgit 1.4.1-2-gfad0 From 82cf44585831ea5a237f3b603a9d8ffd2d350b54 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 02:16:17 -0500 Subject: Add self.nick to completion in Network, not None --- handle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handle.c b/handle.c index 0297595..2cc7a25 100644 --- a/handle.c +++ b/handle.c @@ -157,7 +157,7 @@ static void handleErrorSASLFail(struct Message *msg) { static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); - completeTouch(None, self.nick, Default); + completeTouch(Network, self.nick, Default); if (self.join) { size_t count = 1; for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { -- cgit 1.4.1-2-gfad0 From 40e362f505b2b48656791b25193d67ff4af9a11b Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 02:33:53 -0500 Subject: Hook up tab-complete --- edit.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/edit.c b/edit.c index 38dadcd..f058f0a 100644 --- a/edit.c +++ b/edit.c @@ -60,7 +60,78 @@ static void delete(size_t index, size_t count) { len -= count; } +static struct { + size_t pos; + size_t pre; + size_t len; +} tab; + +static void tabComplete(size_t id) { + if (!tab.len) { + tab.pos = pos; + while (tab.pos && buf[tab.pos - 1] != L' ') tab.pos--; + if (tab.pos == pos) return; + tab.pre = pos - tab.pos; + tab.len = tab.pre; + } + + char mbs[MB_LEN_MAX * tab.pre + 1]; + const wchar_t *ptr = &buf[tab.pos]; + size_t n = wcsnrtombs(mbs, &ptr, tab.pre, sizeof(mbs) - 1, NULL); + assert(n != (size_t)-1); + mbs[n] = '\0'; + + const char *comp = complete(id, mbs); + if (!comp) comp = complete(id, mbs); + if (!comp) { + tab.len = 0; + return; + } + + wchar_t wcs[strlen(comp) + 1]; + n = mbstowcs(wcs, comp, sizeof(wcs)); + assert(n != (size_t)-1); + if (tab.pos + n + 2 > Cap) { + completeReject(); + tab.len = 0; + return; + } + + delete(tab.pos, tab.len); + if (wcs[0] != L'/' && !tab.pos) { + tab.len = n + 2; + reserve(tab.pos, tab.len); + buf[tab.pos + n + 0] = L':'; + buf[tab.pos + n + 1] = L' '; + } else if ( + tab.pos >= 2 && (buf[tab.pos - 2] == L':' || buf[tab.pos - 2] == L',') + ) { + tab.len = n + 2; + reserve(tab.pos, tab.len); + buf[tab.pos - 2] = L','; + buf[tab.pos + n + 0] = L':'; + buf[tab.pos + n + 1] = L' '; + } else { + tab.len = n + 1; + reserve(tab.pos, tab.len); + buf[tab.pos + n] = L' '; + } + memcpy(&buf[tab.pos], wcs, sizeof(*wcs) * n); + pos = tab.pos + tab.len; +} + +static void tabAccept(void) { + completeAccept(); + tab.len = 0; +} + +static void tabReject(void) { + completeReject(); + tab.len = 0; +} + 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; @@ -75,12 +146,20 @@ void edit(size_t id, enum Edit op, wchar_t ch) { if (pos < Cap) buf[pos++] = ch; } break; case EditComplete: { - // TODO + tabComplete(id); + return; } break; case EditEnter: { - pos = 0; + tabAccept(); command(id, editBuffer(NULL)); - len = 0; + len = pos = 0; + return; } } + + if (pos < init) { + tabReject(); + } else { + tabAccept(); + } } -- 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(+) 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 282de9af30793f9935fe521ad95ffc253bd4f474 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 03:45:44 -0500 Subject: Add C-d --- chat.h | 1 + edit.c | 5 +++-- ui.c | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chat.h b/chat.h index 914fde6..d6d9e1c 100644 --- a/chat.h +++ b/chat.h @@ -151,6 +151,7 @@ enum Edit { EditRight, EditKill, EditErase, + EditDelete, EditInsert, EditComplete, EditEnter, diff --git a/edit.c b/edit.c index f058f0a..c30e725 100644 --- a/edit.c +++ b/edit.c @@ -138,8 +138,9 @@ void edit(size_t id, enum Edit op, wchar_t ch) { 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 EditKill: len = pos = 0; + break; case EditErase: if (pos) delete(--pos, 1); + break; case EditDelete: delete(pos, 1); break; case EditInsert: { reserve(pos, 1); diff --git a/ui.c b/ui.c index 4478478..8e502ca 100644 --- a/ui.c +++ b/ui.c @@ -625,6 +625,7 @@ 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_ENTER: edit(id, EditEnter, 0); break; case KEY_HOME: edit(id, EditHome, 0); @@ -645,6 +646,7 @@ static void keyCtrl(wchar_t ch) { 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); -- 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(-) 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(+) 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 3cd830681e25022a8a3936ca9fe58d149fbe493a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 04:22:41 -0500 Subject: Rename kill ops --- chat.h | 4 ++-- edit.c | 4 ++-- ui.c | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chat.h b/chat.h index 6b68eae..ac56f51 100644 --- a/chat.h +++ b/chat.h @@ -151,8 +151,8 @@ enum Edit { EditNext, EditPrevWord, EditNextWord, - EditKillPrev, - EditKillNext, + EditDeleteHead, + EditDeleteTail, EditDeletePrev, EditDeleteNext, EditInsert, diff --git a/edit.c b/edit.c index 38b2dea..7b20079 100644 --- a/edit.c +++ b/edit.c @@ -148,8 +148,8 @@ void edit(size_t id, enum Edit op, wchar_t ch) { 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 EditDeleteHead: delete(0, pos); pos = 0; + break; case EditDeleteTail: delete(pos, len - pos); break; case EditInsert: { reserve(pos, 1); diff --git a/ui.c b/ui.c index 7811e88..e3b9cb5 100644 --- a/ui.c +++ b/ui.c @@ -656,9 +656,9 @@ static void keyCtrl(wchar_t ch) { 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'K': edit(id, EditDeleteTail, 0); break; case L'L': clearok(curscr, true); - break; case L'U': edit(id, EditKillPrev, 0); + break; case L'U': edit(id, EditDeleteHead, 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(-) 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(+) 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(-) 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 02ca8e972bbf1b3a5b66df9ba0bd0e77903ba6d8 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 07:32:35 -0500 Subject: Use iswspace for word movement --- edit.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/edit.c b/edit.c index 16fa910..fe79e76 100644 --- a/edit.c +++ b/edit.c @@ -20,6 +20,7 @@ #include #include #include +#include #include "chat.h" @@ -150,11 +151,11 @@ void edit(size_t id, enum Edit op, wchar_t ch) { break; case EditNext: if (pos < len) pos++; break; case EditPrevWord: { if (pos) pos--; - while (pos && buf[pos - 1] != L' ') pos--; + while (pos && !iswspace(buf[pos - 1])) pos--; } break; case EditNextWord: { if (pos < len) pos++; - while (pos < len && buf[pos] != L' ') pos++; + while (pos < len && !iswspace(buf[pos])) pos++; } break; case EditDeleteHead: delete(0, pos); pos = 0; @@ -164,14 +165,14 @@ void edit(size_t id, enum Edit op, wchar_t ch) { break; case EditDeletePrevWord: { if (!pos) break; size_t word = pos - 1; - while (word && buf[word - 1] != L' ') word--; + while (word && !iswspace(buf[word - 1])) 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++; + while (word < len && !iswspace(buf[word])) word++; delete(pos, word - pos); } break; case EditPaste: { -- 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(+) 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(+) 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(+) 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 8451543b98c086daf7436c0f6d192f1d665680c2 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 08:52:17 -0500 Subject: Implement scrolling! --- ui.c | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/ui.c b/ui.c index 33d6a15..b4a2dee 100644 --- a/ui.c +++ b/ui.c @@ -367,13 +367,22 @@ static void statusUpdate(void) { fflush(stdout); } -static void unmark(void) { - windows.active->heat = Cold; - windows.active->unread = 0; - windows.active->mark = false; +static void unmark(struct Window *window) { + if (window->scroll < BufferCap) return; + window->heat = Cold; + window->unread = 0; + window->mark = false; statusUpdate(); } +static void windowScroll(struct Window *window, int n) { + if (window->scroll == BufferCap) window->mark = true; + window->scroll += n; + if (window->scroll < WINDOW_LINES) window->scroll = WINDOW_LINES; + if (window->scroll > BufferCap) window->scroll = BufferCap; + if (window->scroll == BufferCap) unmark(window); +} + static int wordWidth(const char *str) { size_t len = strcspn(str, " "); int width = 0; @@ -388,11 +397,12 @@ static int wordWidth(const char *str) { return width; } -static void wordWrap(WINDOW *win, const char *str) { +static int wordWrap(WINDOW *win, const char *str) { int y, x, width; getmaxyx(win, y, width); size_t len; + int lines = 0; int align = 0; struct Style style = Reset; while (*str) { @@ -409,6 +419,7 @@ static void wordWrap(WINDOW *win, const char *str) { getyx(win, y, x); const char *word = &str[strspn(str, " ")]; if (width - x - 1 <= wordWidth(word)) { + lines++; waddch(win, '\n'); getyx(win, y, x); wmove(win, y, align); @@ -432,6 +443,7 @@ static void wordWrap(WINDOW *win, const char *str) { waddnstr(win, str, len); str += len; } + return lines; } void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { @@ -439,15 +451,20 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { time_t clock = (src ? *src : time(NULL)); bufferPush(&window->buffer, clock, str); + int lines = 1; waddch(window->pad, '\n'); if (window->mark && heat > Cold) { if (!window->unread++) { + lines++; waddch(window->pad, '\n'); } window->heat = heat; statusUpdate(); } - wordWrap(window->pad, str); + lines += wordWrap(window->pad, str); + if (window->scroll < BufferCap) { + windowScroll(window, -lines); + } if (heat > Warm) beep(); } @@ -573,7 +590,7 @@ static void windowShow(struct Window *window) { windows.active = window; windows.other->mark = true; inputUpdate(); - unmark(); + unmark(windows.active); } void uiShowID(size_t id) { @@ -645,11 +662,12 @@ static void showAuto(void) { } static void keyCode(int code) { - size_t id = windows.active->id; + struct Window *window = windows.active; + size_t id = window->id; switch (code) { break; case KEY_RESIZE: resize(); - break; case KeyFocusIn: unmark(); - break; case KeyFocusOut: windows.active->mark = true; + break; case KeyFocusIn: unmark(window); + break; case KeyFocusOut: window->mark = true; break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO @@ -659,15 +677,19 @@ 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 KeyMetaM: waddch(windows.active->pad, '\n'); + break; case KeyMetaM: waddch(window->pad, '\n'); break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); break; case KEY_DC: edit(id, EditDeleteNext, 0); + break; case KEY_DOWN: windowScroll(window, +1); break; case KEY_END: edit(id, EditTail, 0); break; case KEY_ENTER: edit(id, EditEnter, 0); break; case KEY_HOME: edit(id, EditHead, 0); break; case KEY_LEFT: edit(id, EditPrev, 0); + break; case KEY_NPAGE: windowScroll(window, +(WINDOW_LINES - 2)); + break; case KEY_PPAGE: windowScroll(window, -(WINDOW_LINES - 2)); break; case KEY_RIGHT: edit(id, EditNext, 0); + break; case KEY_UP: windowScroll(window, -1); break; default: { if (code >= KeyMeta0 && code <= KeyMeta9) { -- cgit 1.4.1-2-gfad0 From 11f2de1a29ce3df9f661898436b3db2868137bf9 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 09:18:26 -0500 Subject: Add The Scroll Bar --- ui.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/ui.c b/ui.c index b4a2dee..28749c8 100644 --- a/ui.c +++ b/ui.c @@ -48,6 +48,7 @@ static WINDOW *status; static WINDOW *input; +static WINDOW *scrollBar; enum { BufferCap = 512 }; struct Buffer { @@ -119,6 +120,10 @@ static struct Window *windowFor(size_t id) { return window; } +static bool windowScrolled(struct Window *window) { + return window->scroll < BufferCap; +} + static short colorPairs; static void colorInit(void) { @@ -238,18 +243,27 @@ void uiInit(void) { keypad(input, true); nodelay(input, true); + scrollBar = newwin(1, COLS, LINES - 2, 0); + short fg = 8 + COLOR_BLACK; + wbkgd(scrollBar, '~' | colorAttr(fg) | COLOR_PAIR(colorPair(fg, -1))); + windows.active = windowFor(Network); uiShow(); } void uiDraw(void) { wnoutrefresh(status); + int scrolled = windowScrolled(windows.active); pnoutrefresh( windows.active->pad, - windows.active->scroll - WINDOW_LINES, 0, + windows.active->scroll - WINDOW_LINES + scrolled, 0, 1, 0, - BOTTOM - 1, RIGHT + BOTTOM - 1 - scrolled, RIGHT ); + if (scrolled) { + touchwin(scrollBar); + wnoutrefresh(scrollBar); + } int y, x; getyx(input, y, x); pnoutrefresh( @@ -368,10 +382,11 @@ static void statusUpdate(void) { } static void unmark(struct Window *window) { - if (window->scroll < BufferCap) return; - window->heat = Cold; - window->unread = 0; - window->mark = false; + if (!windowScrolled(window)) { + window->heat = Cold; + window->unread = 0; + window->mark = false; + } statusUpdate(); } @@ -462,7 +477,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { statusUpdate(); } lines += wordWrap(window->pad, str); - if (window->scroll < BufferCap) { + if (windowScrolled(window)) { windowScroll(window, -lines); } if (heat > Warm) beep(); -- cgit 1.4.1-2-gfad0 From 347fabc2fef5890d4ce16e83eb820ed828789163 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 11:50:56 -0500 Subject: Invert the direction of window->scroll --- ui.c | 58 +++++++++++++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/ui.c b/ui.c index 28749c8..050ec2e 100644 --- a/ui.c +++ b/ui.c @@ -44,11 +44,11 @@ #define BOTTOM (LINES - 1) #define RIGHT (COLS - 1) -#define WINDOW_LINES (LINES - 2) +#define PAGE_LINES (LINES - 2) static WINDOW *status; +static WINDOW *marker; static WINDOW *input; -static WINDOW *scrollBar; enum { BufferCap = 512 }; struct Buffer { @@ -66,6 +66,7 @@ static void bufferPush(struct Buffer *buffer, time_t time, const char *line) { if (!buffer->lines[i]) err(EX_OSERR, "strdup"); } +enum { WindowLines = BufferCap }; struct Window { size_t id; struct Buffer buffer; @@ -109,21 +110,16 @@ static struct Window *windowFor(size_t id) { if (!window) err(EX_OSERR, "malloc"); window->id = id; - window->pad = newpad(BufferCap, COLS); + window->pad = newpad(WindowLines, COLS); if (!window->pad) err(EX_OSERR, "newpad"); scrollok(window->pad, true); wmove(window->pad, BufferCap - 1, 0); - window->scroll = BufferCap; window->mark = true; windowAdd(window); return window; } -static bool windowScrolled(struct Window *window) { - return window->scroll < BufferCap; -} - static short colorPairs; static void colorInit(void) { @@ -238,31 +234,31 @@ void uiInit(void) { status = newwin(1, COLS, 0, 0); if (!status) err(EX_OSERR, "newwin"); + marker = newwin(1, COLS, LINES - 2, 0); + short fg = 8 + COLOR_BLACK; + wbkgd(marker, '~' | colorAttr(fg) | COLOR_PAIR(colorPair(fg, -1))); + input = newpad(1, 512); if (!input) err(EX_OSERR, "newpad"); keypad(input, true); nodelay(input, true); - scrollBar = newwin(1, COLS, LINES - 2, 0); - short fg = 8 + COLOR_BLACK; - wbkgd(scrollBar, '~' | colorAttr(fg) | COLOR_PAIR(colorPair(fg, -1))); - windows.active = windowFor(Network); uiShow(); } void uiDraw(void) { wnoutrefresh(status); - int scrolled = windowScrolled(windows.active); + struct Window *window = windows.active; pnoutrefresh( - windows.active->pad, - windows.active->scroll - WINDOW_LINES + scrolled, 0, + window->pad, + WindowLines - window->scroll - PAGE_LINES + !!window->scroll, 0, 1, 0, - BOTTOM - 1 - scrolled, RIGHT + BOTTOM - 1 - !!window->scroll, RIGHT ); - if (scrolled) { - touchwin(scrollBar); - wnoutrefresh(scrollBar); + if (window->scroll) { + touchwin(marker); + wnoutrefresh(marker); } int y, x; getyx(input, y, x); @@ -382,7 +378,7 @@ static void statusUpdate(void) { } static void unmark(struct Window *window) { - if (!windowScrolled(window)) { + if (!window->scroll) { window->heat = Cold; window->unread = 0; window->mark = false; @@ -391,11 +387,13 @@ static void unmark(struct Window *window) { } static void windowScroll(struct Window *window, int n) { - if (window->scroll == BufferCap) window->mark = true; + if (!window->scroll) window->mark = true; window->scroll += n; - if (window->scroll < WINDOW_LINES) window->scroll = WINDOW_LINES; - if (window->scroll > BufferCap) window->scroll = BufferCap; - if (window->scroll == BufferCap) unmark(window); + if (window->scroll > WindowLines - PAGE_LINES) { + window->scroll = WindowLines - PAGE_LINES; + } + if (window->scroll < 0) window->scroll = 0; + if (!window->scroll) unmark(window); } static int wordWidth(const char *str) { @@ -477,9 +475,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { statusUpdate(); } lines += wordWrap(window->pad, str); - if (windowScrolled(window)) { - windowScroll(window, -lines); - } + if (window->scroll) windowScroll(window, lines); if (heat > Warm) beep(); } @@ -696,15 +692,15 @@ static void keyCode(int code) { break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); break; case KEY_DC: edit(id, EditDeleteNext, 0); - break; case KEY_DOWN: windowScroll(window, +1); + break; case KEY_DOWN: windowScroll(window, -1); break; case KEY_END: edit(id, EditTail, 0); break; case KEY_ENTER: edit(id, EditEnter, 0); break; case KEY_HOME: edit(id, EditHead, 0); break; case KEY_LEFT: edit(id, EditPrev, 0); - break; case KEY_NPAGE: windowScroll(window, +(WINDOW_LINES - 2)); - break; case KEY_PPAGE: windowScroll(window, -(WINDOW_LINES - 2)); + break; case KEY_NPAGE: windowScroll(window, -(PAGE_LINES - 2)); + break; case KEY_PPAGE: windowScroll(window, +(PAGE_LINES - 2)); break; case KEY_RIGHT: edit(id, EditNext, 0); - break; case KEY_UP: windowScroll(window, -1); + break; case KEY_UP: windowScroll(window, +1); break; default: { if (code >= KeyMeta0 && code <= KeyMeta9) { -- 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(-) 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(+) 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 0dd8ac36f4f02a9c50fe4919719ca02955e61782 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 14:17:04 -0500 Subject: Avoid VLAs in tab complete --- edit.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edit.c b/edit.c index fe79e76..d90d558 100644 --- a/edit.c +++ b/edit.c @@ -87,7 +87,7 @@ static void tabComplete(size_t id) { tab.len = tab.pre; } - char mbs[MB_LEN_MAX * tab.pre + 1]; + char mbs[MB_LEN_MAX * Cap]; const wchar_t *ptr = &buf[tab.pos]; size_t n = wcsnrtombs(mbs, &ptr, tab.pre, sizeof(mbs) - 1, NULL); assert(n != (size_t)-1); @@ -100,7 +100,7 @@ static void tabComplete(size_t id) { return; } - wchar_t wcs[strlen(comp) + 1]; + wchar_t wcs[Cap]; n = mbstowcs(wcs, comp, sizeof(wcs)); assert(n != (size_t)-1); if (tab.pos + n + 2 > Cap) { -- cgit 1.4.1-2-gfad0 From 3a5ce4d10f1d0b4e2bcf1d958d42665d5cb0db96 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 15:02:34 -0500 Subject: Remove unnecessary uiDraw --- ui.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ui.c b/ui.c index 66a9c59..b3b98dd 100644 --- a/ui.c +++ b/ui.c @@ -163,7 +163,6 @@ void uiShow(void) { putp(EnterPasteMode); fflush(stdout); hidden = false; - uiDraw(); } void uiHide(void) { -- cgit 1.4.1-2-gfad0 From 1d26c880ed305951715a548f934eb231b8fc9317 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 15:02:47 -0500 Subject: Add install target --- Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Makefile b/Makefile index bcbb0d8..89af9b3 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +PREFIX = /usr/local +MANDIR = ${PREFIX}/share/man + CFLAGS += -std=c11 -Wall -Wextra -Wpedantic LDLIBS = -lcrypto -ltls -lncursesw @@ -27,3 +30,11 @@ tags: *.h *.c clean: rm -f tags catgirl ${OBJS} + +install: catgirl catgirl.1 + install -d ${PREFIX}/bin ${MANDIR}/man1 + install catgirl ${PREFIX}/bin + gzip -c catgirl.1 > ${MANDIR}/man1/catgirl.1.gz + +uninstall: + rm -f ${PREFIX}/bin/catgirl ${MANDIR}/man1/catgirl.1.gz -- cgit 1.4.1-2-gfad0 From edd9cb297e5e43f3f3aebc793a5503285c86533e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Sun, 9 Feb 2020 15:25:28 -0500 Subject: Add copy of GPL --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. -- 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(+) 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(+) 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(-) 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 f3fa88ef920c5fdfd5b4fd6cc0b00a38d9d25c53 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 01:09:03 -0500 Subject: Fix M-a so it properly cycles back to where it started --- ui.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ui.c b/ui.c index 66c695f..d94351a 100644 --- a/ui.c +++ b/ui.c @@ -697,12 +697,10 @@ void uiCloseNum(size_t num) { } static void showAuto(void) { - static bool origin; - if (!origin) { - windows.other = windows.active; - origin = true; + static struct Window *other; + if (windows.other != other) { + other = windows.active; } - struct Window *other = windows.other; for (struct Window *window = windows.head; window; window = window->next) { if (window->heat < Hot) continue; windowShow(window); @@ -716,7 +714,6 @@ static void showAuto(void) { return; } windowShow(windows.other); - origin = false; } static void keyCode(int code) { -- cgit 1.4.1-2-gfad0 From 665b6912dcdd2c8dbc893bea12329134f174764e Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 01:23:19 -0500 Subject: Match URLs surrounded by parentheses But include a pair of parentheses in the URL itself. --- url.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/url.c b/url.c index 7ab1e53..1ccc206 100644 --- a/url.c +++ b/url.c @@ -43,7 +43,7 @@ static const char *Pattern = { "telnet|" "vnc" ")" - ":[^[:space:]>\"]+" + ":([^[:space:]>\"()]|[(][^)]*[)])+" }; static regex_t Regex; -- cgit 1.4.1-2-gfad0 From fabb89077d445e6b682f0e38305de7387d07af24 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 01:29:30 -0500 Subject: Update prompt when own nick changes --- handle.c | 1 + 1 file changed, 1 insertion(+) diff --git a/handle.c b/handle.c index 2ef2477..708acc7 100644 --- a/handle.c +++ b/handle.c @@ -269,6 +269,7 @@ static void handleNick(struct Message *msg) { require(msg, true, 1); if (self.nick && !strcmp(msg->nick, self.nick)) { set(&self.nick, msg->params[0]); + uiRead(); // Update prompt. } size_t id; while (None != (id = completeID(msg->nick))) { -- cgit 1.4.1-2-gfad0 From 05fc01b2483dfa203a4f905f294b04325e4111eb Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 01:59:08 -0500 Subject: Simplify mark, heat, unread tracking --- ui.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/ui.c b/ui.c index d94351a..c331a7c 100644 --- a/ui.c +++ b/ui.c @@ -71,11 +71,11 @@ struct Window { size_t id; struct Buffer buffer; WINDOW *pad; + int scroll; bool mark; enum Heat heat; int unreadCount; int unreadLines; - int scroll; struct Window *prev; struct Window *next; }; @@ -114,7 +114,7 @@ static struct Window *windowFor(size_t id) { window->pad = newpad(WindowLines, COLS); if (!window->pad) err(EX_OSERR, "newpad"); scrollok(window->pad, true); - wmove(window->pad, BufferCap - 1, 0); + wmove(window->pad, WindowLines - 1, 0); window->mark = true; windowAdd(window); @@ -356,7 +356,7 @@ static void statusUpdate(void) { int num; const struct Window *window; for (num = 0, window = windows.head; window; ++num, window = window->next) { - if (!window->unreadCount && window != windows.active) continue; + if (!window->heat && window != windows.active) continue; int unread; char buf[256]; snprintf( @@ -367,7 +367,7 @@ static void statusUpdate(void) { window->unreadCount, idColors[window->id] ); - if (!window->unreadCount) buf[unread] = '\0'; + if (!window->mark || !window->unreadCount) buf[unread] = '\0'; statusAdd(buf); } wclrtoeol(status); @@ -379,30 +379,38 @@ static void statusUpdate(void) { self.network, idNames[windows.active->id], &unread, windows.active->unreadCount ); - if (!windows.active->unreadCount) buf[unread] = '\0'; + if (!windows.active->mark || !windows.active->unreadCount) { + buf[unread] = '\0'; + } putp(to_status_line); putp(buf); putp(from_status_line); fflush(stdout); } +static void mark(struct Window *window) { + if (window->scroll) return; + window->mark = true; + window->unreadCount = 0; + window->unreadLines = 0; +} + static void unmark(struct Window *window) { if (!window->scroll) { - window->heat = Cold; - window->unreadCount = 0; window->mark = false; + window->heat = Cold; } statusUpdate(); } static void windowScroll(struct Window *window, int n) { - if (!window->scroll) window->mark = true; + mark(window); window->scroll += n; if (window->scroll > WindowLines - PAGE_LINES) { window->scroll = WindowLines - PAGE_LINES; } if (window->scroll < 0) window->scroll = 0; - if (!window->scroll) unmark(window); + unmark(window); } static void windowScrollUnread(struct Window *window) { @@ -480,7 +488,6 @@ 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->unreadCount++) { lines++; @@ -646,9 +653,9 @@ static void windowShow(struct Window *window) { touchwin(window->pad); windows.other = windows.active; windows.active = window; - windows.other->mark = true; - inputUpdate(); + mark(windows.other); unmark(windows.active); + inputUpdate(); } void uiShowID(size_t id) { @@ -722,7 +729,7 @@ static void keyCode(int code) { switch (code) { break; case KEY_RESIZE: resize(); break; case KeyFocusIn: unmark(window); - break; case KeyFocusOut: window->mark = true; + break; case KeyFocusOut: mark(window); break; case KeyPasteOn:; // TODO break; case KeyPasteOff:; // TODO -- cgit 1.4.1-2-gfad0 From b6061a70d78cfce7318b503891fd0290ad0cccaf Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 02:50:32 -0500 Subject: Update line count for words longer than lines --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index c331a7c..7c933f6 100644 --- a/ui.c +++ b/ui.c @@ -454,7 +454,7 @@ static int wordWrap(WINDOW *win, const char *str) { getyx(win, y, x); const char *word = &str[strspn(str, " ")]; if (width - x - 1 <= wordWidth(word)) { - lines++; + lines += 1 + (align + wordWidth(word)) / width; waddch(win, '\n'); getyx(win, y, x); wmove(win, y, align); -- cgit 1.4.1-2-gfad0 From 3c898576524a8384a81a21aa58d0ba1b3d354322 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 02:55:21 -0500 Subject: Move scroll marker on resize --- ui.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ui.c b/ui.c index 7c933f6..d35c02d 100644 --- a/ui.c +++ b/ui.c @@ -527,6 +527,7 @@ static void reflow(struct Window *window) { } static void resize(void) { + mvwin(marker, LINES - 2, 0); int height, width; getmaxyx(windows.active->pad, height, width); if (width == COLS) return; -- cgit 1.4.1-2-gfad0 From 7a8024ae3dbe0c2c52b6536fb73dd33071e00637 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 03:10:08 -0500 Subject: Always increase unreadLines So that if you switch to a window and some new activity happens before you press M-u, it'll still jump to the right place. --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index d35c02d..1d1e46c 100644 --- a/ui.c +++ b/ui.c @@ -497,7 +497,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; + window->unreadLines += lines; if (window->scroll) windowScroll(window, lines); if (heat > Warm) beep(); } -- cgit 1.4.1-2-gfad0 From 7957ca0ecd5c3e512c836a1b19dbd75628c439fb Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 03:29:38 -0500 Subject: Only make windows hotter A warm message shouldn't clear a window's hotness. --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 1d1e46c..7f13e5b 100644 --- a/ui.c +++ b/ui.c @@ -493,7 +493,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { lines++; waddch(window->pad, '\n'); } - window->heat = heat; + if (window->heat < heat) window->heat = heat; statusUpdate(); } lines += wordWrap(window->pad, str); -- cgit 1.4.1-2-gfad0 From 2c9ff1717b00b9373240dad8200bb8a766ac05cb Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 03:37:17 -0500 Subject: Recalculate unreadLines on reflow --- ui.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 7f13e5b..666a93d 100644 --- a/ui.c +++ b/ui.c @@ -415,7 +415,7 @@ static void windowScroll(struct Window *window, int n) { static void windowScrollUnread(struct Window *window) { window->scroll = 0; - windowScroll(window, window->unreadLines - PAGE_LINES); + windowScroll(window, window->unreadLines - PAGE_LINES + 1); } static int wordWidth(const char *str) { @@ -516,13 +516,18 @@ void uiFormat( static void reflow(struct Window *window) { werase(window->pad); - wmove(window->pad, BufferCap - 1, 0); + 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]; if (!line) continue; waddch(window->pad, '\n'); - wordWrap(window->pad, line); + if (i >= (size_t)(BufferCap - window->unreadCount)) { + window->unreadLines += 1 + wordWrap(window->pad, line); + } else { + wordWrap(window->pad, line); + } } } -- cgit 1.4.1-2-gfad0 From 8e55c049b50e7a08dae819a5d7f704bdfaf4966c Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 03:58:00 -0500 Subject: Avoid coloring mentions if there are control codes This was breaking leading color codes. --- handle.c | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/handle.c b/handle.c index 708acc7..fd2a67f 100644 --- a/handle.c +++ b/handle.c @@ -490,19 +490,16 @@ static bool isMention(const struct Message *msg) { } static const char *colorMentions(size_t id, struct Message *msg) { - char *mention; - char final; - if (strchr(msg->params[1], ':')) { - mention = strsep(&msg->params[1], ":"); - final = ':'; - } else if (strchr(msg->params[1], ' ')) { - mention = strsep(&msg->params[1], " "); - final = ' '; - } else { - mention = msg->params[1]; - msg->params[1] = ""; - final = '\0'; + char *split = strchr(msg->params[1], ':'); + if (!split) split = strchr(msg->params[1], ' '); + if (!split) split = &msg->params[1][strlen(msg->params[1])]; + for (char *ch = msg->params[1]; ch < split; ++ch) { + if (iscntrl(*ch)) return ""; } + char delimit = *split; + char *mention = msg->params[1]; + msg->params[1] = (delimit ? &split[1] : split); + *split = '\0'; static char buf[1024]; FILE *str = fmemopen(buf, sizeof(buf), "w"); @@ -520,7 +517,7 @@ static const char *colorMentions(size_t id, struct Message *msg) { mention[len] = punct; mention += len; } - fputc(final, str); + fputc(delimit, str); fclose(str); buf[sizeof(buf) - 1] = '\0'; -- cgit 1.4.1-2-gfad0 From 218bfbac3257f8484e62d2f047b46417be7aff94 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 04:17:07 -0500 Subject: Support all 99 IRC colors Corresponding ANSI colors from the table on ircdocs. --- ui.c | 73 ++++++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/ui.c b/ui.c index 666a93d..e36585a 100644 --- a/ui.c +++ b/ui.c @@ -133,11 +133,11 @@ static void colorInit(void) { } static attr_t colorAttr(short fg) { - return (fg >= COLORS ? A_BOLD : A_NORMAL); + return (fg >= COLORS && fg < 16 ? A_BOLD : A_NORMAL); } static short colorPair(short fg, short bg) { - if (bg == -1) return 1 + fg; + if (bg == -1 && fg < 16) return 1 + fg; fg %= COLORS; bg %= COLORS; for (short pair = 17; pair < colorPairs; ++pair) { @@ -286,27 +286,32 @@ struct Style { }; static const struct Style Reset = { A_NORMAL, Default, Default }; -static short mapColor(enum Color color) { - switch (color) { - break; case White: return 8 + COLOR_WHITE; - break; case Black: return 0 + COLOR_BLACK; - break; case Blue: return 0 + COLOR_BLUE; - break; case Green: return 0 + COLOR_GREEN; - break; case Red: return 8 + COLOR_RED; - break; case Brown: return 0 + COLOR_RED; - break; case Magenta: return 0 + COLOR_MAGENTA; - break; case Orange: return 0 + COLOR_YELLOW; - break; case Yellow: return 8 + COLOR_YELLOW; - break; case LightGreen: return 8 + COLOR_GREEN; - break; case Cyan: return 0 + COLOR_CYAN; - break; case LightCyan: return 8 + COLOR_CYAN; - break; case LightBlue: return 8 + COLOR_BLUE; - break; case Pink: return 8 + COLOR_MAGENTA; - break; case Gray: return 8 + COLOR_BLACK; - break; case LightGray: return 0 + COLOR_WHITE; - break; default: return -1; - } -} +static const short Colors[100] = { + [Default] = -1, + [White] = 8 + COLOR_WHITE, + [Black] = 0 + COLOR_BLACK, + [Blue] = 0 + COLOR_BLUE, + [Green] = 0 + COLOR_GREEN, + [Red] = 8 + COLOR_RED, + [Brown] = 0 + COLOR_RED, + [Magenta] = 0 + COLOR_MAGENTA, + [Orange] = 0 + COLOR_YELLOW, + [Yellow] = 8 + COLOR_YELLOW, + [LightGreen] = 8 + COLOR_GREEN, + [Cyan] = 0 + COLOR_CYAN, + [LightCyan] = 8 + COLOR_CYAN, + [LightBlue] = 8 + COLOR_BLUE, + [Pink] = 8 + COLOR_MAGENTA, + [Gray] = 8 + COLOR_BLACK, + [LightGray] = 0 + COLOR_WHITE, + 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, + 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, + 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, + 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198, + 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, + 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, + 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, +}; enum { B = '\2', C = '\3', O = '\17', R = '\26', I = '\35', U = '\37' }; @@ -342,8 +347,8 @@ static void statusAdd(const char *str) { styleParse(&style, &str, &len); wattr_set( status, - style.attr | colorAttr(mapColor(style.fg)), - colorPair(mapColor(style.fg), mapColor(style.bg)), + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), NULL ); waddnstr(status, str, len); @@ -471,8 +476,8 @@ static int wordWrap(WINDOW *win, const char *str) { wattr_set( win, - style.attr | colorAttr(mapColor(style.fg)), - colorPair(mapColor(style.fg), mapColor(style.bg)), + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), NULL ); waddnstr(win, str, len); @@ -555,7 +560,7 @@ static void bufferList(struct Buffer *buffer) { 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); + vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL); printf("%s\t", buf); size_t len; @@ -563,8 +568,8 @@ static void bufferList(struct Buffer *buffer) { while (*line) { styleParse(&style, &line, &len); vid_attr( - style.attr | colorAttr(mapColor(style.fg)), - colorPair(mapColor(style.fg), mapColor(style.bg)), + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), NULL ); if (len) printf("%.*s", (int)len, line); @@ -591,8 +596,8 @@ static void inputAdd(struct Style *style, const char *str) { if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); wattr_set( input, - style->attr | colorAttr(mapColor(style->fg)), - colorPair(mapColor(style->fg), mapColor(style->bg)), + style->attr | colorAttr(Colors[style->fg]), + colorPair(Colors[style->fg], Colors[style->bg]), NULL ); waddnstr(input, str, len); @@ -636,8 +641,8 @@ static void inputUpdate(void) { wmove(input, 0, 0); wattr_set( input, - init.attr | colorAttr(mapColor(init.fg)), - colorPair(mapColor(init.fg), mapColor(init.bg)), + init.attr | colorAttr(Colors[init.fg]), + colorPair(Colors[init.fg], Colors[init.bg]), NULL ); waddstr(input, prefix); -- cgit 1.4.1-2-gfad0 From b9a6d35b659e5eba69897d27cee0091821fbe897 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 05:33:31 -0500 Subject: Improve color fudging Prevent fudged colors from ever being pure black. Distribute fudged colors between normal and bold if COLORS is 8. Fudge colors before checking if it's a pre-allocated pair. --- ui.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui.c b/ui.c index e36585a..3f730d2 100644 --- a/ui.c +++ b/ui.c @@ -133,13 +133,15 @@ static void colorInit(void) { } static attr_t colorAttr(short fg) { - return (fg >= COLORS && fg < 16 ? A_BOLD : A_NORMAL); + if (fg != COLOR_BLACK && fg % COLORS == COLOR_BLACK) return A_BOLD; + if (COLORS > 8) return A_NORMAL; + return (fg / COLORS & 1 ? A_BOLD : A_NORMAL); } static short colorPair(short fg, short bg) { - if (bg == -1 && fg < 16) return 1 + fg; fg %= COLORS; bg %= COLORS; + if (bg == -1 && fg < 16) return 1 + fg; for (short pair = 17; pair < colorPairs; ++pair) { short f, b; pair_content(pair, &f, &b); -- cgit 1.4.1-2-gfad0 From a91f975e9b86859f3605c75e42d52e73b1511101 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 05:37:47 -0500 Subject: Hash to colors in the range 2-75 Colors 76-87 seem too light, some almost appearing white. Colors 88-98 are shades of gray. --- chat.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/chat.h b/chat.h index f79cc70..e7bb9cc 100644 --- a/chat.h +++ b/chat.h @@ -198,7 +198,16 @@ static inline enum Color hash(const char *str) { hash ^= *str; hash *= 0x27220A95; } - return 2 + hash % 14; + static const enum Color colors[] = { + Blue, Green, Red, Brown, Magenta, Orange, Yellow, + LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, + }; + return colors[hash % ARRAY_LEN(colors)]; } #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) -- 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(-) 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 c8a6564670839cc526073a90e573939ec30cfcce Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 17:03:13 -0500 Subject: Eliminate array in hash I expected to have to remove some arbitrary colors, but it seems like just the range 2-75 works fine. --- chat.h | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/chat.h b/chat.h index 16cc683..13319da 100644 --- a/chat.h +++ b/chat.h @@ -199,16 +199,7 @@ static inline enum Color hash(const char *str) { hash ^= *str; hash *= 0x27220A95; } - static const enum Color colors[] = { - Blue, Green, Red, Brown, Magenta, Orange, Yellow, - LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, - 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, - }; - return colors[hash % ARRAY_LEN(colors)]; + return 2 + hash % 74; } #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) -- cgit 1.4.1-2-gfad0 From 65603d5138b54efc53c04e756631ab8eeddee7fb Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 17:54:16 -0500 Subject: Show heat and other unread in title --- ui.c | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/ui.c b/ui.c index 3f730d2..9a070a6 100644 --- a/ui.c +++ b/ui.c @@ -359,38 +359,45 @@ static void statusAdd(const char *str) { } static void statusUpdate(void) { + int otherUnread = 0; + enum Heat otherHeat = Cold; wmove(status, 0, 0); + int num; const struct Window *window; for (num = 0, window = windows.head; window; ++num, window = window->next) { if (!window->heat && window != windows.active) continue; - int unread; + if (window != windows.active) { + otherUnread += window->unreadCount; + if (window->heat > otherHeat) otherHeat = window->heat; + } + int trunc; char buf[256]; snprintf( buf, sizeof(buf), "\3%d%s %d %s %n(\3%02d%d\3%d) ", idColors[window->id], (window == windows.active ? "\26" : ""), num, idNames[window->id], - &unread, (window->heat > Warm ? White : idColors[window->id]), + &trunc, (window->heat > Warm ? White : idColors[window->id]), window->unreadCount, idColors[window->id] ); - if (!window->mark || !window->unreadCount) buf[unread] = '\0'; + if (!window->mark || !window->unreadCount) buf[trunc] = '\0'; statusAdd(buf); } wclrtoeol(status); + if (!to_status_line) return; - int unread; - char buf[256]; - snprintf( - buf, sizeof(buf), "%s %s%n (%d)", - self.network, idNames[windows.active->id], - &unread, windows.active->unreadCount - ); - if (!windows.active->mark || !windows.active->unreadCount) { - buf[unread] = '\0'; - } + window = windows.active; putp(to_status_line); - putp(buf); + printf("%s %s", self.network, idNames[window->id]); + if (window->mark && window->unreadCount) { + printf( + " (%d%s)", window->unreadCount, (window->heat > Warm ? "!" : "") + ); + } + if (otherUnread) { + printf(" (+%d%s)", otherUnread, (otherHeat > Warm ? "!" : "")); + } putp(from_status_line); fflush(stdout); } -- 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(-) 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 e6c18403e22bde5a785b7172903f640d03cb8a35 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 19:44:35 -0500 Subject: Leave a blank line after loaded buffer --- ui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 57ef322..ecf7e60 100644 --- a/ui.c +++ b/ui.c @@ -1008,7 +1008,7 @@ void uiLoad(const char *name) { bufferPush(&window->buffer, time, buf); } reflow(window); - // TODO: Place some marker of end of save. + waddch(window->pad, '\n'); } free(buf); -- cgit 1.4.1-2-gfad0 From 99480a42e56e70707822934ffeb56f0454afc127 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 19:57:10 -0500 Subject: Factor out XDG base directory code And add warnings to configOpen, since that's the only way to be accurate if a weird error occurs. --- Makefile | 1 + chat.c | 4 +- chat.h | 2 + config.c | 41 +------------------ ui.c | 67 -------------------------------- xdg.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 109 deletions(-) create mode 100644 xdg.c diff --git a/Makefile b/Makefile index 89af9b3..b1ffede 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ OBJS += handle.o OBJS += irc.o OBJS += ui.o OBJS += url.o +OBJS += xdg.o dev: tags all diff --git a/chat.c b/chat.c index e8713bb..1ae7090 100644 --- a/chat.c +++ b/chat.c @@ -154,11 +154,11 @@ int main(int argc, char *argv[]) { FILE *privFile = NULL; if (cert) { certFile = configOpen(cert, "r"); - if (!certFile) err(EX_NOINPUT, "%s", cert); + if (!certFile) return EX_NOINPUT; } if (priv) { privFile = configOpen(priv, "r"); - if (!privFile) err(EX_NOINPUT, "%s", priv); + if (!privFile) return EX_NOINPUT; } ircConfig(insecure, certFile, privFile); if (certFile) fclose(certFile); diff --git a/chat.h b/chat.h index 47a6163..03a0a50 100644 --- a/chat.h +++ b/chat.h @@ -189,6 +189,8 @@ void urlOpenMatch(size_t id, const char *str); void urlCopyMatch(size_t id, const char *str); FILE *configOpen(const char *path, const char *mode); +FILE *dataOpen(const char *path, const char *mode); + int getopt_config( int argc, char *const *argv, const char *optstring, const struct option *longopts, int *longindex diff --git a/config.c b/config.c index 3bf56c0..3a87948 100644 --- a/config.c +++ b/config.c @@ -24,42 +24,6 @@ #include "chat.h" -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/" XDG_SUBDIR "/%s", configHome, path); - } else { - if (!home) goto local; - snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%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/" XDG_SUBDIR "/%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; @@ -92,10 +56,7 @@ int getopt_config( num = 0; path = argv[optind++]; file = configOpen(path, "r"); - if (!file) { - warn("%s", path); - return clean('?'); - } + if (!file) return clean('?'); } else { return clean(-1); } diff --git a/ui.c b/ui.c index ecf7e60..9601aaa 100644 --- a/ui.c +++ b/ui.c @@ -21,13 +21,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -858,71 +856,6 @@ 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, }; diff --git a/xdg.c b/xdg.c new file mode 100644 index 0000000..6e33210 --- /dev/null +++ b/xdg.c @@ -0,0 +1,134 @@ +/* Copyright (C) 2019, 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 + +#include "chat.h" + +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/" XDG_SUBDIR "/%s", configHome, path); + } else { + if (!home) goto local; + snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path); + } + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + + if (!configDirs) configDirs = "/etc/xdg"; + while (*configDirs) { + size_t len = strcspn(configDirs, ":"); + snprintf( + buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s", + (int)len, configDirs, path + ); + file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + configDirs += len; + if (*configDirs) configDirs++; + } + +local: + file = fopen(path, mode); + if (!file) warn("%s", path); + return file; +} + +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; +} -- cgit 1.4.1-2-gfad0 From babd3b0a6c4715bba573ffdf7bd07eac62b8cd0f Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:06:25 -0500 Subject: Synthesize a QUIT message to handle on exit So that the end of a saved buffer contains the self quit. --- chat.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chat.c b/chat.c index 1ae7090..f854a33 100644 --- a/chat.c +++ b/chat.c @@ -247,5 +247,13 @@ int main(int argc, char *argv[]) { } else { ircFormat("QUIT\r\n"); } + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "QUIT", + .params[0] = self.quit, + }; + handle(msg); + uiHide(); } -- cgit 1.4.1-2-gfad0 From 00f0f94fc80ebecff531388e38d0fb121e3f4e74 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:17:21 -0500 Subject: Delegate to commandPrivmsg from commandMsg --- command.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.c b/command.c index 3e201cc..cab1d26 100644 --- a/command.c +++ b/command.c @@ -75,7 +75,7 @@ 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); + commandPrivmsg(idFor(nick), params); } static void commandJoin(size_t id, char *params) { -- cgit 1.4.1-2-gfad0 From 80a79467efca8f17e440cb63009c60dd8e78cc63 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:24:07 -0500 Subject: Only automatically switch to expected joins --- chat.h | 1 + command.c | 1 + handle.c | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/chat.h b/chat.h index 03a0a50..f47b244 100644 --- a/chat.h +++ b/chat.h @@ -120,6 +120,7 @@ void ircFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); extern struct Replies { + size_t join; size_t topic; size_t names; size_t whois; diff --git a/command.c b/command.c index cab1d26..5cb43cf 100644 --- a/command.c +++ b/command.c @@ -86,6 +86,7 @@ static void commandJoin(size_t id, char *params) { } } ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); + replies.join += count; replies.topic += count; replies.names += count; } diff --git a/handle.c b/handle.c index fd2a67f..0db7fd9 100644 --- a/handle.c +++ b/handle.c @@ -164,6 +164,7 @@ static void handleReplyWelcome(struct Message *msg) { if (*ch == ',') count++; } ircFormat("JOIN %s\r\n", self.join); + replies.join += count; replies.topic += count; replies.names += count; } @@ -211,7 +212,10 @@ static void handleJoin(struct Message *msg) { } idColors[id] = hash(msg->params[0]); completeTouch(None, msg->params[0], idColors[id]); - uiShowID(id); + if (replies.join) { + uiShowID(id); + replies.join--; + } } completeTouch(id, msg->nick, hash(msg->user)); if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { -- 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(+) 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 From 2c2839e6c18aabbb76fa624767e248ed86d63326 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:44:37 -0500 Subject: Replace alignment tabs with spaces in bufferList --- ui.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 5f912b7..1e7eb26 100644 --- a/ui.c +++ b/ui.c @@ -38,6 +38,7 @@ // Annoying stuff from : #undef lines +#undef tab #ifndef A_ITALIC #define A_ITALIC A_NORMAL @@ -577,12 +578,20 @@ static void bufferList(const struct Buffer *buffer) { char buf[sizeof("[00:00:00]")]; strftime(buf, sizeof(buf), "[%T]", tm); vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL); - printf("%s\t", buf); + printf("%s ", buf); size_t len; + bool align = false; struct Style style = Reset; while (*line) { + if (*line == '\t') { + printf("%c", (align ? '\t' : ' ')); + align = true; + line++; + } styleParse(&style, &line, &len); + size_t tab = strcspn(line, "\t"); + if (tab < len) len = tab; vid_attr( style.attr | colorAttr(Colors[style.fg]), colorPair(Colors[style.fg], Colors[style.bg]), -- cgit 1.4.1-2-gfad0 From 36e0bbc4cd783a826313de57fe77edf35e912d49 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 20:58:14 -0500 Subject: Split on <> in colorMentions This allows it to color the nick in the common case of pasting " something they said" into the chat. Technically it should color the brackets too but that would be too much work. --- handle.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handle.c b/handle.c index 0db7fd9..ce56a51 100644 --- a/handle.c +++ b/handle.c @@ -510,11 +510,11 @@ static const char *colorMentions(size_t id, struct Message *msg) { if (!str) err(EX_OSERR, "fmemopen"); while (*mention) { - size_t skip = strspn(mention, ", "); + size_t skip = strspn(mention, ",<> "); fwrite(mention, skip, 1, str); mention += skip; - size_t len = strcspn(mention, ", "); + size_t len = strcspn(mention, ",<> "); char punct = mention[len]; mention[len] = '\0'; fprintf(str, "\3%02d%s\3", completeColor(id, mention), mention); -- cgit 1.4.1-2-gfad0 From 47a0bf7fc2b9a462a6b1d6e76f0b9a137cbef791 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 21:09:32 -0500 Subject: Manually raise SIGINT from C-c This allows it to still work, but makes C-z C-c insert the color code rather than exit, and in the future, will allow pasting in text with color codes. --- ui.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 1e7eb26..903c4af 100644 --- a/ui.c +++ b/ui.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -184,12 +185,13 @@ void uiHide(void) { endwin(); } -// Gain use of C-q, C-s, C-z, C-y, C-o. +// Gain use of C-q, C-s, C-c, 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[VINTR] = _POSIX_VDISABLE; term.c_cc[VSUSP] = _POSIX_VDISABLE; term.c_cc[VDSUSP] = _POSIX_VDISABLE; term.c_cc[VDISCARD] = _POSIX_VDISABLE; @@ -805,6 +807,7 @@ static void keyCtrl(wchar_t ch) { 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'C': raise(SIGINT); break; case L'D': edit(id, EditDeleteNext, 0); break; case L'E': edit(id, EditTail, 0); break; case L'F': edit(id, EditNext, 0); -- cgit 1.4.1-2-gfad0 From 90eff04eda86e0a75b0e81960e2166193c366be7 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 21:24:30 -0500 Subject: Only write out title on uiDraw --- ui.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ui.c b/ui.c index 903c4af..297fd7b 100644 --- a/ui.c +++ b/ui.c @@ -268,6 +268,8 @@ void uiInit(void) { uiShow(); } +static char title[256]; + void uiDraw(void) { if (hidden) return; wnoutrefresh(status); @@ -291,6 +293,12 @@ void uiDraw(void) { BOTTOM, RIGHT ); doupdate(); + + if (!to_status_line) return; + putp(to_status_line); + putp(title); + putp(from_status_line); + fflush(stdout); } struct Style { @@ -396,21 +404,21 @@ static void statusUpdate(void) { statusAdd(buf); } wclrtoeol(status); - if (!to_status_line) return; window = windows.active; - putp(to_status_line); - printf("%s %s", self.network, idNames[window->id]); + snprintf(title, sizeof(title), "%s %s", self.network, idNames[window->id]); if (window->mark && window->unreadCount) { - printf( + snprintf( + &title[strlen(title)], sizeof(title) - strlen(title), " (%d%s)", window->unreadCount, (window->heat > Warm ? "!" : "") ); } if (otherUnread) { - printf(" (+%d%s)", otherUnread, (otherHeat > Warm ? "!" : "")); + snprintf( + &title[strlen(title)], sizeof(title) - strlen(title), + " (+%d%s)", otherUnread, (otherHeat > Warm ? "!" : "") + ); } - putp(from_status_line); - fflush(stdout); } static void mark(struct Window *window) { -- cgit 1.4.1-2-gfad0 From 66fe89b84b8acc9c34ab03eeb17ca7b7f6eff1e5 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 21:34:23 -0500 Subject: Only write out title if it has changed --- ui.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index 297fd7b..dcfb607 100644 --- a/ui.c +++ b/ui.c @@ -293,8 +293,12 @@ void uiDraw(void) { BOTTOM, RIGHT ); doupdate(); - if (!to_status_line) return; + + static char prevTitle[sizeof(title)]; + if (!strcmp(title, prevTitle)) return; + strcpy(prevTitle, title); + putp(to_status_line); putp(title); putp(from_status_line); -- cgit 1.4.1-2-gfad0 From bf86a4749f93de47d45309028d97ea3a0b7f0c7a Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Mon, 10 Feb 2020 22:05:02 -0500 Subject: Invalidate title on uiShow --- ui.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ui.c b/ui.c index dcfb607..6c9606d 100644 --- a/ui.c +++ b/ui.c @@ -168,23 +168,6 @@ static const char *ExitFocusMode = "\33[?1004l"; static const char *EnterPasteMode = "\33[?2004h"; static const char *ExitPasteMode = "\33[?2004l"; -static bool hidden; -static bool waiting; - -void uiShow(void) { - putp(EnterFocusMode); - putp(EnterPasteMode); - fflush(stdout); - hidden = false; -} - -void uiHide(void) { - hidden = true; - putp(ExitFocusMode); - putp(ExitPasteMode); - endwin(); -} - // Gain use of C-q, C-s, C-c, C-z, C-y, C-o. static void acquireKeys(void) { struct termios term; @@ -268,7 +251,11 @@ void uiInit(void) { uiShow(); } +static bool hidden; +static bool waiting; + static char title[256]; +static char prevTitle[sizeof(title)]; void uiDraw(void) { if (hidden) return; @@ -293,18 +280,31 @@ void uiDraw(void) { BOTTOM, RIGHT ); doupdate(); - if (!to_status_line) return; - static char prevTitle[sizeof(title)]; + if (!to_status_line) return; if (!strcmp(title, prevTitle)) return; strcpy(prevTitle, title); - putp(to_status_line); putp(title); putp(from_status_line); fflush(stdout); } +void uiShow(void) { + prevTitle[0] = '\0'; + putp(EnterFocusMode); + putp(EnterPasteMode); + fflush(stdout); + hidden = false; +} + +void uiHide(void) { + hidden = true; + putp(ExitFocusMode); + putp(ExitPasteMode); + endwin(); +} + struct Style { attr_t attr; enum Color fg, bg; -- cgit 1.4.1-2-gfad0 From 7104c267cc3359748565c5e0b5844ccaba97072d Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 11 Feb 2020 02:31:08 -0500 Subject: Add README Still missing: build requirements and instructions. --- README.7 | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 README.7 diff --git a/README.7 b/README.7 new file mode 100644 index 0000000..fd3b030 --- /dev/null +++ b/README.7 @@ -0,0 +1,110 @@ +.Dd February 11, 2020 +.Dt README 7 +.Os "Causal Agency" +. +.Sh NAME +.Nm catgirl +.Nd IRC client +. +.Sh DESCRIPTION +.Nm +is a TLS-only curses IRC client. +. +.Ss Notable Features +.Bl -bullet +.It +Tab complete: +most recently seen or mentioned nicks +are completed first. +Commas are inserted between multple nicks. +.It +Indicators: +the prompt clearly shows whether input +will be interpreted as a command +or sent as a message. +An indicator appears when scrolled up +in the chat history. +.It +Nick coloring: +color generation based on usernames +remains stable across nick changes. +Mentions of users in messages are colored. +.It +URL detection: +recent URLs from a particular user +or matching a substring +can be opened or copied. +.It +History: +window contents can be saved +and restored on startup. +.El +. +.Ss Non-features +.Bl -bullet +.It +Dynamic configuration: +all configuration happens +in a simple text file +or on the command line. +.It +Multi-network: +a terminal multiplexer such as +.Xr screen 1 +or +.Xr tmux 1 +(or just your regular terminal emulator tabs) +can be used to connect +.Nm +to multiple networks. +.It +Reconnection: +when the connection to the server is lost, +.Nm +exits. +It can be run in a loop +or connected to a bouncer, +such as +.Lk https://git.causal.agency/pounce "pounce" . +.It +Cleartext IRC: +TLS is now ubiquitous +and certificates are easy to obtain. +.El +. +.Sh FILES +.Bl -tag -width "complete.c" -compact +.It Pa chat.h +global state and declarations +.It Pa chat.c +startup and event loop +.It Pa irc.c +IRC connection and parsing +.It Pa ui.c +curses interface +.It Pa handle.c +IRC message handling +.It Pa command.c +input command handling +.It Pa edit.c +line editing +.It Pa complete.c +tab complete +.It Pa url.c +URL detection +.It Pa config.c +configuration parsing +.It Pa xdg.c +XDG base directories +.El +. +.Sh CONTRIBUTING +The upstream URL of this project is +.Aq Lk https://git.causal.agency/catgirl . +I'm happy to receive contributions in any form at +.Aq Mt june@causal.agency . +For sending patches by email, see +.Aq Lk https://git-send-email.io . +. +.Sh SEE ALSO +.Xr catgirl 1 -- cgit 1.4.1-2-gfad0 From 1bb60065c36c97d9dcad853c0c79a836ceed24e3 Mon Sep 17 00:00:00 2001 From: C. McEnroe Date: Tue, 11 Feb 2020 02:41:20 -0500 Subject: Add INSTALLING section to README --- README.7 | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.7 b/README.7 index fd3b030..1478722 100644 --- a/README.7 +++ b/README.7 @@ -7,8 +7,8 @@ .Nd IRC client . .Sh DESCRIPTION -.Nm -is a TLS-only curses IRC client. +.Xr catgirl 1 +is a TLS-only terminal IRC client. . .Ss Notable Features .Bl -bullet @@ -72,6 +72,22 @@ TLS is now ubiquitous and certificates are easy to obtain. .El . +.Sh INSTALLING +.Nm +requires LibreSSL +.Pq Fl ltls +and ncurses +.Pq Fl lncursesw . +It primarily targets +.Fx +and macOS, +as well as Linux. +.Bd -literal -offset indent +\&./configure +make all +sudo make install PREFIX=/usr/local +.Ed +. .Sh FILES .Bl -tag -width "complete.c" -compact .It Pa chat.h -- cgit 1.4.1-2-gfad0