diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 98 | ||||
-rw-r--r-- | README.7 | 108 | ||||
-rw-r--r-- | buffer.c | 233 | ||||
-rw-r--r-- | catgirl.1 | 747 | ||||
-rw-r--r-- | chat.c | 258 | ||||
-rw-r--r-- | chat.h | 326 | ||||
-rw-r--r-- | command.c | 407 | ||||
-rw-r--r-- | compat_readpassphrase.c | 206 | ||||
-rw-r--r-- | complete.c | 163 | ||||
-rw-r--r-- | config.c | 22 | ||||
-rwxr-xr-x | configure | 29 | ||||
-rw-r--r-- | edit.c | 442 | ||||
-rw-r--r-- | edit.h | 74 | ||||
-rw-r--r-- | filter.c | 132 | ||||
-rw-r--r-- | handle.c | 740 | ||||
-rw-r--r-- | ignore.c | 89 | ||||
-rw-r--r-- | input.c | 629 | ||||
-rw-r--r-- | irc.c | 121 | ||||
-rw-r--r-- | log.c | 104 | ||||
-rw-r--r-- | sandman.1 (renamed from scripts/sandman.1) | 2 | ||||
-rw-r--r-- | sandman.m (renamed from scripts/sandman.m) | 2 | ||||
-rw-r--r-- | scripts/.gitignore | 1 | ||||
-rw-r--r-- | scripts/build-chroot.sh | 74 | ||||
-rw-r--r-- | scripts/chat.tmux.conf | 64 | ||||
-rw-r--r-- | scripts/chroot-prompt.sh | 3 | ||||
-rw-r--r-- | ui.c | 1049 | ||||
-rw-r--r-- | url.c | 58 | ||||
-rw-r--r-- | window.c | 659 | ||||
-rw-r--r-- | xdg.c | 172 |
30 files changed, 4719 insertions, 2295 deletions
diff --git a/.gitignore b/.gitignore index e96e0c1..519791d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.o +*.t catgirl chroot.tar config.mk root/ +sandman tags diff --git a/Makefile b/Makefile index 08e90ab..66fb408 100644 --- a/Makefile +++ b/Makefile @@ -1,93 +1,87 @@ PREFIX ?= /usr/local -MANDIR ?= ${PREFIX}/share/man +BINDIR ?= ${PREFIX}/bin +MANDIR ?= ${PREFIX}/man CEXTS = gnu-case-range gnu-conditional-omitted-operand -CFLAGS += -std=c11 -Wall -Wextra -Wpedantic ${CEXTS:%=-Wno-%} -LDLIBS = -lncursesw -ltls +CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -Wmissing-prototypes +CFLAGS += ${CEXTS:%=-Wno-%} +LDADD.libtls = -ltls +LDADD.ncursesw = -lncursesw + +BINS = catgirl +MANS = ${BINS:=.1} -include config.mk +LDLIBS = ${LDADD.libtls} ${LDADD.ncursesw} +LDLIBS.sandman = -framework Cocoa + +OBJS += buffer.o OBJS += chat.o OBJS += command.o OBJS += complete.o OBJS += config.o OBJS += edit.o +OBJS += filter.o OBJS += handle.o -OBJS += ignore.o +OBJS += input.o OBJS += irc.o OBJS += log.o OBJS += ui.o OBJS += url.o +OBJS += window.o OBJS += xdg.o -dev: tags all +OBJS.sandman = sandman.o + +TESTS += edit.t -all: catgirl +dev: tags all check + +all: ${BINS} catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ ${OBJS}: chat.h -tags: *.h *.c - ctags -w *.h *.c +edit.o edit.t input.o: edit.h -clean: - rm -f tags catgirl ${OBJS} +sandman: ${OBJS.sandman} + ${CC} ${LDFLAGS} ${OBJS.$@} ${LDLIBS.$@} -o $@ -install: catgirl catgirl.1 - install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1 - install catgirl ${DESTDIR}${PREFIX}/bin - install -m 644 catgirl.1 ${DESTDIR}${MANDIR}/man1 +check: ${TESTS} -uninstall: - rm -f ${DESTDIR}${PREFIX}/bin/catgirl ${DESTDIR}${MANDIR}/man1/catgirl.1 +.SUFFIXES: .t + +.c.t: + ${CC} ${CFLAGS} -DTEST ${LDFLAGS} $< ${LDLIBS} -o $@ + ./$@ || rm $@ -scripts/sandman: scripts/sandman.o - ${CC} ${LDFLAGS} scripts/sandman.o -framework Cocoa -o $@ +tags: *.[ch] + ctags -w *.[ch] -install-sandman: scripts/sandman scripts/sandman.1 - install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1 - install scripts/sandman ${DESTDIR}${PREFIX}/bin - install -m 644 scripts/sandman.1 ${DESTDIR}${MANDIR}/man1 +clean: + rm -f ${BINS} ${OBJS} ${OBJS.sandman} ${TESTS} tags -uninstall-sandman: - rm -f ${DESTDIR}${PREFIX}/bin/sandman ${DESTDIR}${MANDIR}/man1/sandman.1 +install: ${BINS} ${MANS} + install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 + install ${BINS} ${DESTDIR}${BINDIR} + install -m 644 ${MANS} ${DESTDIR}${MANDIR}/man1 + +uninstall: + rm -f ${BINS:%=${DESTDIR}${BINDIR}/%} + rm -f ${MANS:%=${DESTDIR}${MANDIR}/man1/%} CHROOT_USER = chat CHROOT_GROUP = ${CHROOT_USER} chroot.tar: catgirl catgirl.1 scripts/chroot-prompt.sh scripts/chroot-man.sh - install -d -o root -g wheel \ - root \ - root/bin \ - root/etc \ - root/home \ - root/lib \ - root/libexec \ - root/usr/bin \ - root/usr/local/etc/ssl \ - root/usr/share/man \ - root/usr/share/misc - install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} \ - root/home/${CHROOT_USER} \ - root/home/${CHROOT_USER}/.local/share - cp -fp /libexec/ld-elf.so.1 root/libexec - ldd -f '%p\n' catgirl /usr/bin/mandoc /usr/bin/less \ - | sort -u | xargs -t -J % cp -fp % root/lib - chflags noschg root/libexec/* root/lib/* - cp -fp /etc/hosts /etc/resolv.conf root/etc - cp -fp /usr/local/etc/ssl/cert.pem root/usr/local/etc/ssl - cp -af /usr/share/locale root/usr/share - cp -fp /usr/share/misc/termcap.db root/usr/share/misc - cp -fp /rescue/sh /usr/bin/mandoc /usr/bin/less root/bin - ${MAKE} install DESTDIR=root PREFIX=/usr - install scripts/chroot-prompt.sh root/usr/bin/catgirl-prompt - install scripts/chroot-man.sh root/usr/bin/man - tar -c -f chroot.tar -C root bin etc home lib libexec usr +chroot.tar: scripts/build-chroot.sh + sh scripts/build-chroot.sh ${CHROOT_USER} ${CHROOT_GROUP} install-chroot: chroot.tar - tar -x -f chroot.tar -C /home/${CHROOT_USER} + tar -px -f chroot.tar -C /home/${CHROOT_USER} clean-chroot: rm -fr chroot.tar root diff --git a/README.7 b/README.7 index 6842fd3..a26d270 100644 --- a/README.7 +++ b/README.7 @@ -1,7 +1,7 @@ -.Dd August 4, 2020 +.\" To view this file: $ man ./README.7 +.Dd July 9, 2023 .Dt README 7 .Os "Causal Agency" -.\" To view this file, run: man ./README.7 . .Sh NAME .Nm catgirl @@ -9,7 +9,15 @@ . .Sh DESCRIPTION .Xr catgirl 1 -is a TLS-only terminal IRC client. +is a terminal IRC client. +. +.Pp +Screenshot: +imagine, +if you will, +text on a screen, +next to names +in a selection of colours. . .Ss Notable Features .Bl -bullet @@ -17,7 +25,7 @@ is a TLS-only terminal IRC client. Tab complete: most recently seen or mentioned nicks are completed first. -Commas are inserted between multple nicks. +Commas are inserted between multiple nicks. .It Prompt: the prompt clearly shows whether input @@ -37,9 +45,25 @@ color generation based on usernames remains stable across nick changes. Mentions of users in messages are colored. .It +Topic diffing: +the modified portion +of a channel topic change +is highlighted. +.It Ignore: visibility of filtered messages can be toggled. +.It +Security: +on +.Fx +and +.Ox , +the +.Cm restrict +option enables tight sandboxing. +Sandboxing is always used on +.Ox . .El . .Ss Non-features @@ -75,6 +99,11 @@ apart from this protocol is useless at best and enables abuse at worst. .It +Protocol coverage: +IRCv3 extensions are implemented only +where they contribute to +the intended user experience. +.It Cleartext IRC: TLS is now ubiquitous and certificates are easy to obtain. @@ -93,17 +122,27 @@ It targets .Ox , macOS and Linux. -On -.Ox , -configure with -.Fl \-mandir=/usr/local/man . +.Nm +and +.Sy libtls +may be packaged for your system. +Check the Repology pages for +.Lk https://repology.org/project/catgirl/versions catgirl +and +.Lk https://repology.org/project/libretls/versions libretls . +. .Bd -literal -offset indent -\&./configure -make all -sudo make install +$ ./configure +$ make all +# make install .Ed . .Pp +Packagers are encouraged +to patch in their own text macros in +.Pa input.c . +. +.Pp If installing .Sy libtls manually to @@ -124,7 +163,7 @@ for .Nm ./configure to find it. .Bd -literal -offset indent -PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure +$ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure .Ed . .Pp @@ -134,14 +173,14 @@ wrapper is provided for macOS to stop and start .Nm on system sleep and wake. -Install it as follows: +To enable it, +configure with: .Bd -literal -offset indent -make scripts/sandman -sudo make install-sandman +$ ./configure --enable-sandman .Ed . .Sh FILES -.Bl -tag -width "complete.c" -compact +.Bl -tag -width "command.c" -compact .It Pa chat.h global state and declarations .It Pa chat.c @@ -150,17 +189,23 @@ startup and event loop IRC connection and parsing .It Pa ui.c curses interface +.It Pa window.c +window management +.It Pa input.c +input handling .It Pa handle.c IRC message handling .It Pa command.c -input command handling +command handling +.It Pa buffer.c +line wrapping .It Pa edit.c line editing .It Pa complete.c tab complete .It Pa url.c URL detection -.It Pa ignore.c +.It Pa filter.c message filtering .It Pa log.c chat logging @@ -168,15 +213,25 @@ chat logging configuration parsing .It Pa xdg.c XDG base directories +.It Pa sandman.m +sleep/wake wrapper for macOS .El . .Pp .Bl -tag -width "scripts/notify-send.scpt" -compact -.It Pa scripts/sandman.m -sleep/wake wrapper for macOS +.It Pa scripts/chat.tmux.conf +example +.Xr tmux 1 +configuration for multiple networks +and automatic reconnects .It Pa scripts/notify-send.scpt .Xr notify-send 1 in AppleScript +.It Pa scripts/build-chroot.sh +chroot builder for +.Ox +and +.Fx .It Pa scripts/chroot-prompt.sh name prompt wrapper for chroot .It Pa scripts/chroot-man.sh @@ -194,19 +249,26 @@ Contributions in any form can be sent to .Aq Mt list+catgirl@causal.agency . For sending patches by email, see .Aq Lk https://git-send-email.io . +Mailing list archives are available at +.Aq Lk https://causal.agency/list/catgirl.html . +. +.Pp +Monetary contributions can be +.Lk https://liberapay.com/june/donate "donated via Liberapay" . . .Sh SEE ALSO -.Xr catgirl 1 +.Xr catgirl 1 , +.Xr sandman 1 . .Pp IRC bouncer: .Lk https://git.causal.agency/pounce "pounce" . .Rs -.%A June Bug +.%A June McEnroe .%T IRC Suite .%U https://text.causal.agency/010-irc-suite.txt .%D June 19, 2020 .Re . -.\" To view this file, run: man ./README.7 +.\" To view this file: $ man ./README.7 diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..f82e553 --- /dev/null +++ b/buffer.c @@ -0,0 +1,233 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <err.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> +#include <wchar.h> +#include <wctype.h> + +#include "chat.h" + +struct Lines { + size_t len; + struct Line lines[BufferCap]; +}; +_Static_assert(!(BufferCap & (BufferCap - 1)), "BufferCap is power of two"); + +struct Buffer { + struct Lines soft; + struct Lines hard; +}; + +struct Buffer *bufferAlloc(void) { + struct Buffer *buffer = calloc(1, sizeof(*buffer)); + if (!buffer) err(EX_OSERR, "calloc"); + return buffer; +} + +void bufferFree(struct Buffer *buffer) { + for (size_t i = 0; i < BufferCap; ++i) { + free(buffer->soft.lines[i].str); + free(buffer->hard.lines[i].str); + } + free(buffer); +} + +static const struct Line *linesLine(const struct Lines *lines, size_t i) { + const struct Line *line = &lines->lines[(lines->len + i) % BufferCap]; + return (line->str ? line : NULL); +} + +static struct Line *linesNext(struct Lines *lines) { + struct Line *line = &lines->lines[lines->len++ % BufferCap]; + free(line->str); + return line; +} + +const struct Line *bufferSoft(const struct Buffer *buffer, size_t i) { + return linesLine(&buffer->soft, i); +} + +const struct Line *bufferHard(const struct Buffer *buffer, size_t i) { + return linesLine(&buffer->hard, i); +} + +enum { StyleCap = 10 }; +static char *styleCopy(char *ptr, char *end, struct Style style) { + ptr = seprintf( + ptr, end, "%s%s%s%s", + (style.attr & Bold ? (const char []) { B, '\0' } : ""), + (style.attr & Reverse ? (const char []) { R, '\0' } : ""), + (style.attr & Italic ? (const char []) { I, '\0' } : ""), + (style.attr & Underline ? (const char []) { U, '\0' } : "") + ); + if (style.fg != Default || style.bg != Default) { + ptr = seprintf(ptr, end, "\3%02d,%02d", style.fg, style.bg); + } + return ptr; +} + +static const wchar_t ZWS = L'\u200B'; +static const wchar_t ZWNJ = L'\u200C'; + +static int flow(struct Lines *hard, int cols, const struct Line *soft) { + int flowed = 1; + + struct Line *line = linesNext(hard); + line->num = soft->num; + line->heat = soft->heat; + line->time = soft->time; + line->str = strdup(soft->str); + if (!line->str) err(EX_OSERR, "strdup"); + + int width = 0; + int align = 0; + char *wrap = NULL; + struct Style style = StyleDefault; + struct Style wrapStyle = StyleDefault; + for (char *str = line->str; *str;) { + size_t len = styleParse(&style, (const char **)&str); + if (!len) continue; + + bool tab = (*str == '\t' && !align); + if (tab) *str = ' '; + + wchar_t wc = L'\0'; + int n = mbtowc(&wc, str, len); + if (n < 0) { + n = 1; + // ncurses will render these as "~A". + width += (*str & '\200' ? 2 : 1); + } else if (wc == ZWS || wc == ZWNJ) { + // ncurses likes to render these as spaces when they should be + // zero-width, so just remove them entirely. + memmove(str, &str[n], strlen(&str[n]) + 1); + continue; + } else if (wc == L'\t') { + // Assuming TABSIZE = 8. + width += 8 - (width % 8); + } else if (wc < L' ' || wc == L'\177') { + // ncurses will render these as "^A". + width += 2; + } else if (wcwidth(wc) > 0) { + width += wcwidth(wc); + } + + if (tab && width < cols) { + align = width; + wrap = NULL; + } + if (iswspace(wc) && !tab) { + wrap = str; + wrapStyle = style; + } + if (wc == L'-' && width <= cols) { + wrap = &str[n]; + wrapStyle = style; + } + + if (width <= cols) { + str += n; + continue; + } else if (!wrap) { + wrap = str; + wrapStyle = style; + } + + n = 0; + len = strlen(wrap); + for (int m; wrap[n] && (m = mbtowc(&wc, &wrap[n], len - n)); n += m) { + if (m < 0) { + m = 1; + } else if (!iswspace(wc)) { + break; + } + } + if (!wrap[n]) { + *wrap = '\0'; + break; + } + + flowed++; + line = linesNext(hard); + line->num = soft->num; + line->heat = soft->heat; + line->time = 0; + + size_t cap = StyleCap + align + strlen(&wrap[n]) + 1; + line->str = malloc(cap); + if (!line->str) err(EX_OSERR, "malloc"); + + char *end = &line->str[cap]; + str = seprintf(line->str, end, "%*s", (width = align), ""); + str = styleCopy(str, end, wrapStyle); + style = wrapStyle; + seprintf(str, end, "%s", &wrap[n]); + + *wrap = '\0'; + wrap = NULL; + } + + return flowed; +} + +int bufferPush( + struct Buffer *buffer, int cols, enum Heat thresh, + enum Heat heat, time_t time, const char *str +) { + struct Line *soft = linesNext(&buffer->soft); + soft->num = buffer->soft.len; + soft->heat = heat; + soft->time = time; + soft->str = strdup(str); + if (!soft->str) err(EX_OSERR, "strdup"); + if (heat < thresh) return 0; + return flow(&buffer->hard, cols, soft); +} + +int +bufferReflow(struct Buffer *buffer, int cols, enum Heat thresh, size_t tail) { + buffer->hard.len = 0; + for (size_t i = 0; i < BufferCap; ++i) { + free(buffer->hard.lines[i].str); + buffer->hard.lines[i].str = NULL; + } + int flowed = 0; + for (size_t i = 0; i < BufferCap; ++i) { + const struct Line *soft = bufferSoft(buffer, i); + if (!soft) continue; + if (soft->heat < thresh) continue; + int n = flow(&buffer->hard, cols, soft); + if (i >= BufferCap - tail) flowed += n; + } + return flowed; +} diff --git a/catgirl.1 b/catgirl.1 index 363c393..815eade 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd July 13, 2020 +.Dd October 11, 2023 .Dt CATGIRL 1 .Os . @@ -8,44 +8,82 @@ . .Sh SYNOPSIS .Nm -.Op Fl Relv +.Op Fl Relqv .Op Fl C Ar copy .Op Fl H Ar hash -.Op Fl N Ar send +.Op Fl I Ar highlight +.Op Fl N Ar notify .Op Fl O Ar open .Op Fl S Ar bind -.Op Fl a Ar auth +.Op Fl T Ns Op Ar timestamp +.Op Fl a Ar plain .Op Fl c Ar cert .Op Fl h Ar host -.Op Fl i Ar patt +.Op Fl i Ar ignore .Op Fl j Ar join .Op Fl k Ar priv +.Op Fl m Ar mode .Op Fl n Ar nick .Op Fl p Ar port .Op Fl r Ar real .Op Fl s Ar save +.Op Fl t Ar trust .Op Fl u Ar user .Op Fl w Ar pass .Op Ar config ... . .Nm +.Fl o +.Op Fl S Ar bind +.Op Fl h Ar host +.Op Fl p Ar port +.Op Ar config ... +. +.Nm .Fl g Ar cert . .Sh DESCRIPTION The .Nm -program is a TLS-only -curses IRC client. +IRC client +provides a curses interface +for TLS-only +Internet Relay Chat. +The only required option is +.Fl h , +the host name to connect to. +See +.Sx EXAMPLES +for managing further configuration. +Type +.Ic /help +in +.Nm +to view the list of +.Sx COMMANDS +and +.Sx KEY BINDINGS . . .Pp Options can be loaded from files listed on the command line. Files are searched for in .Pa $XDG_CONFIG_DIRS/catgirl +.Po +usually +.Pa ~/.config/catgirl +.Pc unless the path starts with -.Ql / +.Ql / , +.Ql \&./ or -.Ql \&. . +.Ql \&../ . +Files and flags listed later +on the command line +take precedence over +those listed earlier. +. +.Pp Each option is placed on a line, and lines beginning with .Ql # @@ -53,89 +91,153 @@ are ignored. The options are listed below following their corresponding flags. . -.Pp -The arguments are as follows: .Bl -tag -width Ds -.It Fl C Ar util , Cm copy = Ar util +.It Fl C Ar util | Cm copy No = Ar util Set the utility used by .Ic /copy . -Use more than once to add arguments to +Subsequent +.Cm copy +options append arguments to .Ar util . +The URL to copy is provided to +.Ar util +on standard input. The default is the first available of .Xr pbcopy 1 , .Xr wl-copy 1 , .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 H Ar seed,bound | Cm hash No = Ar seed,bound +Set the initial seed +of the nick and channel +color hash function +and the maximum IRC color value +produced by the function. +The default is 0,75. +To use only colors from +the 16-color terminal set, +use 0,15. +To disable nick and channel colors, +use 0,0. . -.It Fl N Ar util , Cm notify = Ar util +.It Fl I Ar pattern | Cm highlight No = Ar pattern +Add a case-insensitive message highlight pattern, +which may contain +.Ql * , +.Ql \&? +and +.Ql [] +wildcards as in +.Xr glob 7 . +The format of the pattern is as follows: +.Bd -ragged -offset indent +.Ar nick Ns Op Ar !user@host Op Ar command Op Ar channel Op Ar message +.Ed +.Pp +The commands which can be matched are: +.Sy INVITE , +.Sy JOIN , +.Sy NICK , +.Sy NOTICE , +.Sy PART , +.Sy PRIVMSG , +.Sy QUIT , +.Sy SETNAME . +. +.It Fl N Ar util | Cm notify No = Ar util Send notifications using a utility. -Use more than once to add arguments to +Subsequent +.Cm notify +options append arguments to .Ar util . -Two additional arguments are passed to -.Ar util : -a title and a description, +The window name and message +are provided to +.Ar util +as two additional arguments, appropriate for .Xr notify-send 1 . . -.It Fl O Ar util , Cm open = Ar util +.It Fl O Ar util | Cm open No = Ar util Set the utility used by .Ic /open . -Use more than once to add arguments to +Subsequent +.Cm open +options append arguments to .Ar util . +The URL to open is provided to +.Ar util +as an argument. The default is the first available of .Xr open 1 , .Xr xdg-open 1 . . -.It Fl R , Cm restrict +.It Fl R | Cm restrict Disable the .Ic /copy , -.Ic /debug , -.Ic /exec , -.Ic /join , -.Ic /msg , -.Ic /open , -.Ic /query , -.Ic /quote -commands. -. -.It Fl S Ar host , Cm bind = Ar host +.Ic /exec +and +.Ic /open +commands, +the +.Cm notify +option, +and viewing this manual with +.Ic /help . +. +.It Fl S Ar host | Cm bind No = Ar host Bind to source address .Ar host when connecting to the server. +To connect from any address +over IPv4 only, +use 0.0.0.0. +To connect from any address +over IPv6 only, +use ::. . -.It Fl a Ar user : Ns Ar pass , Cm sasl-plain = Ar user : Ns Ar pass +.It Fl T Ns Oo Ar format Oc | Cm timestamp Op = Ar format +Show timestamps by default, +in the specified +.Xr strftime 3 +.Ar format . +The format string may contain +raw IRC formatting codes. +The default format is +.Qq \&%X . +. +.It Fl a Ar user : Ns Ar pass | Cm sasl-plain No = Ar user : 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 . +Leave +.Ar pass +blank to prompt for the password. . -.It Fl c Ar path , Cm cert = Ar path +.It Fl c Ar path | Cm cert No = Ar path Load the TLS client certificate from .Ar path . +The +.Ar path +is searched for in the same manner +as configuration files. If the private key is in a separate file, it is loaded with -.Fl k . +.Cm priv . With -.Fl e , +.Cm sasl-external , authenticate using SASL EXTERNAL. Certificates can be generated with .Fl g . . -.It Fl e , Cm sasl-external +.It Fl e | Cm sasl-external Authenticate using SASL EXTERNAL, also known as CertFP. The TLS client certificate is loaded with -.Fl c . -For more information, see +.Cm cert . +See .Sx Configuring CertFP . . .It Fl g Ar path @@ -144,81 +246,132 @@ Generate a TLS client certificate using and write it to .Ar path . . -.It Fl h Ar host , Cm host = Ar host +.It Fl h Ar host | Cm host No = Ar host Connect to .Ar host . . -.It Fl i Ar pattern, Cm ignore = Ar pattern -Add a case-insensitive message filtering pattern, +.It Fl i Ar pattern | Cm ignore No = Ar pattern +Add a case-insensitive message ignore pattern, which may contain .Ql * , .Ql \&? and .Ql [] wildcards as in -.Xr sh 1 . +.Xr glob 7 . The format of the pattern is as follows: .Bd -ragged -offset indent -.Ar nick Ns Op Ar !user@host -.Op Ar command -.Op Ar channel -.Op Ar message +.Ar nick Ns Op Ar !user@host Op Ar command Op Ar channel Op Ar message .Ed +.Pp +The commands which can be matched are: +.Sy INVITE , +.Sy JOIN , +.Sy NICK , +.Sy NOTICE , +.Sy PART , +.Sy PRIVMSG , +.Sy QUIT , +.Sy SETNAME . . -.It Fl j Ar join , Cm join = Ar join -Join the comma-separated list of channels -.Ar join . +.It Fl j Ar channels Oo Ar keys Oc | Cm join No = Ar channels Oo Ar keys Oc +Join the comma-separated list of +.Ar channels +with the optional comma-separated list of channel +.Ar keys . . -.It Fl k Ar path , Cm priv = Ar priv +.It Fl k Ar path | Cm priv No = Ar priv Load the TLS client private key from .Ar path . +The +.Ar path +is searched for in the same manner +as configuration files. . -.It Fl l , Cm log +.It Fl l | Cm log Log chat events to files in paths .Pa $XDG_DATA_HOME/catgirl/log/network/channel/YYYY-MM-DD.log . . -.It Fl n Ar nick , Cm nick = Ar nick +.It Fl m Ar mode | Cm mode No = Ar mode +Set the user +.Ar mode . +. +.It Fl n Ar nick Oo Ar ... Oc | Cm nick No = Ar nick Oo Ar ... Oc Set nickname to .Ar nick . -The default nickname is the user's name. +The default nickname is +the value of the environment variable +.Ev USER . +Additional space-separated nicks +will be tried in order +if the first is not available, +and all nicks +are treated as highlight words. +. +.It Fl o +Print the server certificate chain +to standard output in PEM format +and exit. . -.It Fl p Ar port , Cm port = Ar port +.It Fl p Ar port | Cm port No = Ar port Connect to .Ar port . The default port is 6697. . -.It Fl r Ar real , Cm real = Ar real +.It Fl q | Cm quiet +Raise the default message visibility threshold +for new windows, +hiding general events +(joins, quits, etc.). +. +.It Fl r Ar real | Cm real No = Ar real 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 +.It Fl s Ar name | Cm save No = Ar name +Save and load 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 / +.Ql / , +.Ql \&./ , or -.Ql \&. . +.Ql \&../ . . -.It Fl u Ar user , Cm user = Ar user +.It Fl t Ar path | Cm trust No = Ar path +Trust the self-signed certificate +loaded from +.Ar path +and disable server name verification. +The +.Ar path +is searched for in the same manner +as configuration files. +See +.Sx Connecting to Servers with Self-signed Certificates . +. +.It Fl u Ar user | Cm user No = Ar user Set username to .Ar user . The default username is the same as the nickname. . -.It Fl v , Cm debug +.It Fl v | Cm debug Log raw IRC messages to the .Sy <debug> window as well as standard error if it is not a terminal. . -.It Fl w Ar pass , Cm pass = Ar pass +.It Fl w Ar pass | Cm pass No = Ar pass Log in with the server password .Ar pass . +Leave +.Ar pass +blank to prompt for the password. .El . .Ss Configuring CertFP @@ -226,13 +379,13 @@ Log in with the server password .It Generate a new TLS client certificate: .Bd -literal -offset indent -catgirl -g ~/.config/catgirl/example.pem +$ catgirl -g ~/.config/catgirl/example.pem .Ed .It Connect to the server using the certificate: .Bd -literal -offset indent cert = example.pem -# or: catgirl -c example.pem +# or: $ catgirl -c example.pem .Ed .It Identify with services or use @@ -240,19 +393,136 @@ Identify with services or use then add the certificate fingerprint to your account: .Bd -literal -offset indent -/msg NickServ CERT ADD +/ns CERT ADD .Ed .It Enable SASL EXTERNAL to require successful authentication -when connecting: +when connecting +(not possible on all networks): .Bd -literal -offset indent cert = example.pem sasl-external -# or: catgirl -e -c example.pem +# or: $ catgirl -e -c example.pem .Ed .El . +.Ss Connecting to Servers with Self-signed Certificates +.Bl -enum +.It +Connect to the server +and write its certificate to a file: +.Bd -literal -offset indent +$ catgirl -o -h irc.example.org > ~/.config/catgirl/example.pem +.Ed +.It +Configure +.Nm +to trust the certificate: +.Bd -literal -offset indent +trust = example.pem +# or: $ catgirl -t example.pem +.Ed +.El +. +.Sh INTERFACE +The +.Nm +interface is split +into three areas. +. +.Ss Status Line +The top line of the terminal +shows window statuses. +Only the currently active window +and windows with activity are listed. +The status line for a window +might look like this: +.Bd -literal -offset indent +1+ #ascii.town +3 ~7 @ +.Ed +.Pp +The number on the left +is the window number. +Following it may be one of +.Ql - , +.Ql + , +.Ql ++ , +as well as +.Ql = . +These indicate +the message visibility threshold +and mute status +of the window. +.Pp +On the right side, +the number following +.Ql + +indicates the number of unread messages. +The number following +.Ql ~ +indicates how many lines +are below the scroll position. +An +.Ql @ +indicates that there is unsent input +in the window's +.Sx Input Line . +.Pp +.Nm +will also set the terminal title, +if possible, +to the name of the network +and active window, +followed by the unread count +for that window, +and the unread count +for all other windows +in parentheses. +. +.Ss Chat Area +The chat area shows +messages and events. +Regular messages are shown +with the nick between +.Ql <> +angle brackets. +Actions are shown +with the nick preceded by +.Ql * . +Notices are shown +with the nick between +.Ql - +hyphens. +.Pp +Blank lines are inserted into the chat +as unread markers. +.Pp +While scrolling, +the most recent 5 lines of chat +are kept visible below a marker line. +. +.Ss Input Line +The bottom line of the terminal +is where messages and commands are entered. +When entering a message, action or notice, +your nick appears on the left, +as it would in the +.Sx Chat Area . +When entering a command, +no nick is shown. +.Pp +Formatting codes are shown +in the input line +as reverse-video uppercase letters. +These will not appear in the sent message. +.Pp +Input that is too long +to send as a single message +will have a red background +starting at the point where it will be split +into a second message. +. .Sh COMMANDS Any unique prefix can be used to abbreviate a command. For example, @@ -268,8 +538,10 @@ Set or clear your away status. Send a command to ChanServ. .It Ic /invite Ar nick Invite a user to the channel. -.It Ic /join Ar channel -Join a channel. +.It Ic /join Op Ar channel Op Ar key +Join the named channel, +the current channel, +or the channel you've been invited to. .It Ic /list Op Ar channel List channels. .It Ic /me Op Ar action @@ -284,6 +556,8 @@ Change nicknames. Send a notice. .It Ic /ns Ar command Send a command to NickServ. +.It Ic /ops +List channel operators. .It Ic /part Op Ar message Leave the channel. .It Ic /query Ar nick @@ -292,19 +566,23 @@ Start a private conversation. Quit IRC. .It Ic /quote Ar command Send a raw IRC command. -The -.Ic /debug -command is likely needed -for command output. +Use +.Ic M-- +to show unknown replies. .It Ic /say Ar message Send a regular message. +.It Ic /setname Ar name +Update realname +if supported by the server. .It Ic /topic Op Ar topic Show or set the topic of the channel. Press .Ic Tab twice to copy the current topic. -.It Ic /whois Ar nick -Query information about a user. +.It Ic /whois Op Ar nick +Query information about a user or yourself. +.It Ic /whowas Ar nick +Query past information about a user. .El . .Ss UI Commands @@ -328,19 +606,36 @@ with and interpret its output as input to the current window, including as commands. -.It Ic /help Op Ar search +.It Ic /help View this manual. Type .Ic q to return to .Nm . +.It Ic /help Ar topic +List the server help for a topic. +Try +.Ic /help index +for a list of topics. +.It Ic /highlight Op Ar pattern +List message highlight patterns +or temporarily add a pattern. +To permanently add a pattern, +use the +.Cm highlight +option. .It Ic /ignore Op Ar pattern -List message filtering patterns +List message ignore patterns or temporarily add a pattern. -To permanently add a pattern, use -.Fl i . +To permanently add a pattern, +use the +.Cm ignore +option. .It Ic /move Oo Ar name Oc Ar num -Move named window to number. +Move the named or current window to number. +.It Ic /o ... +Alias of +.Ic /open . .It Ic /open Op Ar count Open each of .Ar count @@ -350,11 +645,16 @@ Open the most recent URL from .Ar nick or matching .Ar substring . +.It Ic /unhighlight Ar pattern +Temporarily remove a message highlight pattern. .It Ic /unignore Ar pattern -Temporarily remove a message filtering pattern. -.It Ic /window Ar name -Switch to window by name. -.It Ic /window Ar num , Ic / Ns Ar num +Temporarily remove a message ignore pattern. +.It Ic /window +List all windows. +.It Ic /window Ar name | substring +Switch to window by name +or matching substring. +.It Ic /window Ar num | Ic / Ns Ar num Switch to window by number. .El . @@ -442,6 +742,9 @@ Collapse all whitespace. .It Ic Tab Complete nick, channel, command or macro. .El +.Pp +Arrow and navigation keys +also work as expected. . .Ss Window Keys .Bl -tag -width Ds -compact @@ -451,10 +754,22 @@ Redraw the UI. Switch to next window. .It Ic C-p Switch to previous window. +.It Ic C-r +Scroll to previous line matching input. +.It Ic C-s +Scroll to next line matching input. .It Ic C-v Scroll down a page. +.It Ic M-+ +Raise message visibility threshold, +hiding ignored messages, +general events +(joins, quits, etc.), +or non-highlighted messages. .It Ic M-- -Toggle visibility of filtered messages. +Lower message visibility threshold, +showing ignored messages +and unknown replies. .It Ic M-= Toggle mute. Muted windows do not appear in the status line @@ -465,19 +780,28 @@ Switch to previously selected window. Scroll to top. .It Ic M-> Scroll to bottom. +.It Ic M- Ns Ar n +Switch to window by number 0\(en9. .It Ic M-a Cycle through unread windows. .It Ic M-l List the contents of the window -without word-wrapping. +without word-wrapping +and with timestamps. Press .Ic Enter to return to .Nm . .It Ic M-m Insert a blank line in the window. -.It Ic M- Ns Ar n -Switch to window by number 0\(en9. +.It Ic M-n +Scroll to next highlight. +.It Ic M-p +Scroll to previous highlight. +.It Ic M-s +Reveal spoiler text. +.It Ic M-t +Toggle timestamps. .It Ic M-u Scroll to first unread line. .It Ic M-v @@ -485,7 +809,9 @@ Scroll up a page. .El . .Ss IRC Formatting -.Bl -tag -width Ds -compact +.Bl -tag -width "C-z C-v" -compact +.It Ic C-z C-v +Insert the next input character literally. .It Ic C-z b Toggle bold. .It Ic C-z c @@ -494,14 +820,30 @@ Set or reset color. Toggle italics. .It Ic C-z o Reset formatting. +.It Ic C-z p +Manually toggle paste mode. .It Ic C-z r Toggle reverse color. +.It Ic C-z s +Set spoiler text (black on black). .It Ic C-z u Toggle underline. .El . .Pp -To set colors, follow +Some color codes can be inserted +with the following: +.Bl -column "C-z A" "magenta" "C-z N" "orange (dark yellow)" +.It Ic C-z A Ta gray Ta Ic C-z N Ta brown (dark red) +.It Ic C-z B Ta blue Ta Ic C-z O Ta orange (dark yellow) +.It Ic C-z C Ta cyan Ta Ic C-z P Ta pink (light magenta) +.It Ic C-z G Ta green Ta Ic C-z R Ta red +.It Ic C-z K Ta black Ta Ic C-z W Ta white +.It Ic C-z M Ta magenta Ta Ic C-z Y Ta yellow +.El +. +.Pp +To set other colors, follow .Ic C-z c by one or two digits for the foreground color, optionally followed by a comma @@ -527,13 +869,10 @@ The color numbers are as follows: .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev SHELL -The path executed by -.Ic /exec -with -.Fl c Ar command . -If unset, -.Pa /bin/sh -is used. +The shell used by +.Ic /exec . +The default is +.Pa /bin/sh . .It Ev USER The default nickname. .El @@ -573,23 +912,26 @@ if requested by the user, .Dv EX_UNAVAILABLE (69) if the connection is lost, -and >0 if an error occurs. +and >0 if any other error occurs. . .Sh EXAMPLES -Command line: +Join +.Li #ascii.town +from the command line: .Bd -literal -offset indent -catgirl -h chat.freenode.net -j '#ascii.town' +$ catgirl -h irc.tilde.chat -j '#ascii.town' .Ed .Pp -Configuration file: +Create a configuration file in +.Pa ~/.config/catgirl/tilde : .Bd -literal -offset indent -host = chat.freenode.net +host = irc.tilde.chat join = #ascii.town .Ed .Pp -Filtering joins, parts and quits from a channel: +Load the configuration file: .Bd -literal -offset indent -ignore = * [JPQ][OAU][IR][NT] #example +$ catgirl tilde .Ed . .Sh STANDARDS @@ -598,121 +940,133 @@ ignore = * [JPQ][OAU][IR][NT] #example .Rs .%A Adam .%A Attila Molnar -.%T IRCv3.2 invite-notify Extension +.%T invite-notify Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/invite-notify-3.2 +.%U https://ircv3.net/specs/extensions/invite-notify +.Re +.It +.Rs +.%A Jack Allnutt +.%A Val Lorentz +.%A Daniel Oaks +.%T Modern IRC Client Protocol +.%I ircdocs +.%U https://modern.ircdocs.horse/index.html .Re .It .Rs -.%A Alexey Sokolov -.%A St\('ephan Kochen -.%A Kyle Fuller .%A Kiyoshi Aman +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov .%A James Wheare -.%T IRCv3 Message Tags +.%T Message Tags .%I IRCv3 Working Group .%U https://ircv3.net/specs/extensions/message-tags .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 +.%A Kiyoshi Aman +.%T extended-join Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/extended-join +.Re +.It +.Rs +.%A Waldo Bastian +.%A Ryan Lortie +.%A Lennart Poettering +.%T XDG Base Directory Specification +.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +.%D November 24, 2010 .Re .It .Rs .%A Christine Dodrill -.%T IRCv3.2 chghost Extension +.%A Ryan +.%A James Wheare +.%T chghost Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/chghost-3.2 +.%U https://ircv3.net/specs/extensions/chghost .Re .It .Rs -.%A Daniel Oaks -.%T IRC Formatting -.%I ircdocs -.%U https://modern.ircdocs.horse/formatting.html +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov +.%A James Wheare +.%T server-time Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/server-time .Re .It .Rs -.%A J. Oikarinen -.%A D. Reed -.%T Internet Relay Chat Protocol -.%I IETF -.%N RFC 1459 -.%D May 1993 -.%U https://tools.ietf.org/html/rfc1459 +.%A Lee Hardy +.%A Perry Lorier +.%A Kevin L. Mitchell +.%A Attila Molnar +.%A Daniel Oakley +.%A William Pitcock +.%A James Wheare +.%T Client Capability Negotiation +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/core/capability-negotiation .Re .It .Rs -.%A Jack Allnutt -.%T Modern IRC Client Protocol -.%I ircdocs -.%U https://modern.ircdocs.horse/index.html +.%A S. Josefsson +.%T The Base16, Base32, and Base64 Data Encodings +.%I IETF +.%R RFC 4648 +.%U https://tools.ietf.org/html/rfc4648 +.%D October 2006 .Re .It .Rs -.%A K. Zeilenga, Ed. -.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism +.%A C. Kalt +.%T Internet Relay Chat: Client Protocol .%I IETF -.%N RFC 4616 -.%D August 2006 -.%U https://tools.ietf.org/html/rfc4616 +.%R RFC 2812 +.%U https://tools.ietf.org/html/rfc2812 +.%D April 2000 .Re .It .Rs -.%A Kiyoshi Aman -.%T IRCv3.1 extended-join Extension +.%A Janne Mareike Koschinski +.%T setname Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/extended-join-3.1 +.%U https://ircv3.net/specs/extensions/setname .Re .It .Rs -.%A Kyle Fuller -.%A St\('ephan Kochen -.%A Alexey Sokolov -.%A James Wheare -.%T IRCv3.2 server-time Extension +.%A Mantas Mikul\[u0117]nas +.%T userhost-in-names Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/server-time-3.2 +.%U https://ircv3.net/specs/extensions/userhost-in-names .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 +.%A Daniel Oaks +.%T IRC Formatting +.%I ircdocs +.%U https://modern.ircdocs.horse/formatting.html .Re .It .Rs -.%A Mantas Mikul\[u0117]nas -.%T IRCv3.2 userhost-in-names Extension +.%A Daniel Oaks +.%T Standard Replies Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/userhost-in-names-3.2 +.%U https://ircv3.net/specs/extensions/standard-replies .Re .It .Rs -.%A S. Josefsson -.%T The Base16, Base32, and Base64 Data Encodings +.%A J. Oikarinen +.%A D. Reed +.%T Internet Relay Chat Protocol .%I IETF -.%N RFC 4648 -.%D October 2006 -.%U https://tools.ietf.org/html/rfc4648 -.Re -.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 +.%R RFC 1459 +.%U https://tools.ietf.org/html/rfc1459 +.%D May 1993 .Re .It .Rs @@ -720,31 +1074,54 @@ ignore = * [JPQ][OAU][IR][NT] #example .%A Jilles Tjoelker .%T IRCv3.1 SASL Authentication .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/sasl-3.1.html +.%U https://ircv3.net/specs/extensions/sasl-3.1 .Re .It .Rs .%A William Pitcock -.%T IRCv3.1 multi-prefix Extension +.%T multi-prefix Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/multi-prefix +.Re +.It +.Rs +.%A James Wheare +.%T Message IDs .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/multi-prefix-3.1 +.%U https://ircv3.net/specs/extensions/message-ids +.Re +.It +.Rs +.%A James Wheare +.%T reply Client Tag +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/client-tags/reply +.Re +.It +.Rs +.%A K. Zeilenga, Ed. +.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism +.%I IETF +.%R RFC 4616 +.%U https://tools.ietf.org/html/rfc4616 +.%D August 2006 .Re .El . .Ss Extensions The .Nm -client can take advantage of the +client implements the .Sy causal.agency/consumer vendor-specific IRCv3 capability -implemented by +offered by .Xr pounce 1 . The consumer position is stored in the .Cm save file. . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency . .Sh BUGS Send mail to @@ -752,4 +1129,4 @@ Send mail to or join .Li #ascii.town on -.Li chat.freenode.net . +.Li irc.tilde.chat . diff --git a/chat.c b/chat.c index 2a16b06..6728240 100644 --- a/chat.c +++ b/chat.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ #include <err.h> #include <errno.h> #include <fcntl.h> +#include <inttypes.h> #include <limits.h> #include <locale.h> #include <poll.h> @@ -38,10 +39,19 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sys/time.h> #include <sys/wait.h> #include <sysexits.h> +#include <time.h> +#include <tls.h> #include <unistd.h> +#ifdef __FreeBSD__ +#include <capsicum_helpers.h> +#endif + +char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags); + #include "chat.h" #ifndef OPENSSL_BIN @@ -80,15 +90,13 @@ struct Self self = { .color = Default }; static const char *save; static void exitSave(void) { - int error = uiSave(save); + int error = uiSave(); if (error) { warn("%s", save); _exit(EX_IOERR); } } -uint32_t hashInit; - uint execID; int execPipe[2] = { -1, -1 }; int utilPipe[2] = { -1, -1 }; @@ -100,7 +108,7 @@ static void execRead(void) { if (!len) return; buf[len] = '\0'; for (char *ptr = buf; ptr;) { - char *line = strsep(&ptr, "\n"); + char *line = strsep(&ptr, "\r\n"); if (line[0]) command(execID, line); } } @@ -112,29 +120,123 @@ static void utilRead(void) { if (!len) return; buf[len] = '\0'; for (char *ptr = buf; ptr;) { - char *line = strsep(&ptr, "\n"); + char *line = strsep(&ptr, "\r\n"); if (line[0]) uiFormat(Network, Warm, NULL, "%s", line); } } +uint32_t hashInit; +uint32_t hashBound = 75; + +static void parseHash(char *str) { + hashInit = strtoul(str, &str, 0); + if (*str) hashBound = strtoul(&str[1], NULL, 0); +} + +static void parsePlain(char *str) { + self.plainUser = strsep(&str, ":"); + if (!str) errx(EX_USAGE, "SASL PLAIN missing colon"); + self.plainPass = str; +} + static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { signals[signal] = 1; } +static void sandboxEarly(bool log); +static void sandboxLate(int irc); + +#if defined __OpenBSD__ + +static char *promisesInitial; +static char promises[64] = "stdio tty"; + +static void sandboxEarly(bool log) { + char *ptr = &promises[strlen(promises)]; + char *end = &promises[sizeof(promises)]; + + if (log) { + char buf[PATH_MAX]; + int error = unveil(dataPath(buf, sizeof(buf), "log", 0), "wc"); + if (error) err(EX_OSERR, "unveil"); + ptr = seprintf(ptr, end, " wpath cpath"); + } + + if (!self.restricted) { + int error = unveil("/", "x"); + if (error) err(EX_OSERR, "unveil"); + ptr = seprintf(ptr, end, " proc exec"); + } + + promisesInitial = ptr; + ptr = seprintf(ptr, end, " inet dns"); + int error = pledge(promises, NULL); + if (error) err(EX_OSERR, "pledge"); +} + +static void sandboxLate(int irc) { + (void)irc; + *promisesInitial = '\0'; + int error = pledge(promises, NULL); + if (error) err(EX_OSERR, "pledge"); +} + +#elif defined __FreeBSD__ + +static void sandboxEarly(bool log) { + (void)log; +} + +static void sandboxLate(int irc) { + if (!self.restricted) return; + + // Rights are also limited in uiLoad() and logOpen(). + cap_rights_t rights; + int error = 0 + || caph_limit_stdin() + || caph_rights_limit( + STDOUT_FILENO, cap_rights_init(&rights, CAP_WRITE, CAP_IOCTL) + ) + || caph_limit_stderr() + || caph_rights_limit( + irc, cap_rights_init(&rights, CAP_SEND, CAP_RECV, CAP_EVENT) + ); + if (error) err(EX_OSERR, "cap_rights_limit"); + + // caph_cache_tzdata(3) doesn't load UTC info, which we need for + // certificate verification. gmtime(3) does. + caph_cache_tzdata(); + gmtime(&(time_t) { time(NULL) }); + + error = cap_enter(); + if (error) err(EX_OSERR, "cap_enter"); +} + +#else +static void sandboxEarly(bool log) { + (void)log; +} +static void sandboxLate(int irc) { + (void)irc; +} +#endif + int main(int argc, char *argv[]) { setlocale(LC_CTYPE, ""); bool insecure = false; + bool printCert = false; const char *bind = NULL; const char *host = NULL; const char *port = "6697"; + const char *trust = NULL; const char *cert = NULL; const char *priv = NULL; + bool log = false; bool sasl = false; - const char *pass = NULL; - const char *nick = NULL; + char *pass = NULL; const char *user = NULL; const char *real = NULL; @@ -142,10 +244,12 @@ int main(int argc, char *argv[]) { { .val = '!', .name = "insecure", no_argument }, { .val = 'C', .name = "copy", required_argument }, { .val = 'H', .name = "hash", required_argument }, + { .val = 'I', .name = "highlight", required_argument }, { .val = 'N', .name = "notify", required_argument }, { .val = 'O', .name = "open", required_argument }, { .val = 'R', .name = "restrict", no_argument }, { .val = 'S', .name = "bind", required_argument }, + { .val = 'T', .name = "timestamp", optional_argument }, { .val = 'a', .name = "sasl-plain", required_argument }, { .val = 'c', .name = "cert", required_argument }, { .val = 'e', .name = "sasl-external", no_argument }, @@ -155,43 +259,61 @@ int main(int argc, char *argv[]) { { .val = 'j', .name = "join", required_argument }, { .val = 'k', .name = "priv", required_argument }, { .val = 'l', .name = "log", no_argument }, + { .val = 'm', .name = "mode", required_argument }, { .val = 'n', .name = "nick", required_argument }, + { .val = 'o', .name = "print-chain", no_argument }, { .val = 'p', .name = "port", required_argument }, + { .val = 'q', .name = "quiet", no_argument }, { .val = 'r', .name = "real", required_argument }, { .val = 's', .name = "save", required_argument }, + { .val = 't', .name = "trust", required_argument }, { .val = 'u', .name = "user", required_argument }, { .val = 'v', .name = "debug", no_argument }, { .val = 'w', .name = "pass", required_argument }, {0}, }; - char opts[2 * ARRAY_LEN(options)]; + char opts[3 * ARRAY_LEN(options)]; for (size_t i = 0, j = 0; i < ARRAY_LEN(options); ++i) { opts[j++] = options[i].val; - if (options[i].has_arg) opts[j++] = ':'; + if (options[i].has_arg != no_argument) opts[j++] = ':'; + if (options[i].has_arg == optional_argument) opts[j++] = ':'; } for (int opt; 0 < (opt = getopt_config(argc, argv, opts, options, NULL));) { switch (opt) { break; case '!': insecure = true; break; case 'C': utilPush(&urlCopyUtil, optarg); - break; case 'H': hashInit = strtoul(optarg, NULL, 0); + break; case 'H': parseHash(optarg); + break; case 'I': filterAdd(Hot, optarg); break; case 'N': utilPush(&uiNotifyUtil, optarg); break; case 'O': utilPush(&urlOpenUtil, optarg); break; case 'R': self.restricted = true; break; case 'S': bind = optarg; - break; case 'a': sasl = true; self.plain = optarg; + break; case 'T': { + windowTime.enable = true; + if (optarg) windowTime.format = optarg; + } + break; case 'a': sasl = true; parsePlain(optarg); break; case 'c': cert = optarg; break; case 'e': sasl = true; break; case 'g': genCert(optarg); break; case 'h': host = optarg; - break; case 'i': ignoreAdd(optarg); + break; case 'i': filterAdd(Ice, optarg); break; case 'j': self.join = optarg; break; case 'k': priv = optarg; - break; case 'l': logEnable = true; - break; case 'n': nick = optarg; + break; case 'l': log = true; logOpen(); + break; case 'm': self.mode = optarg; + break; case 'n': { + for (uint i = 0; i < ARRAY_LEN(self.nicks); ++i) { + self.nicks[i] = strsep(&optarg, " "); + } + } + break; case 'o': printCert = true; break; case 'p': port = optarg; + break; case 'q': windowThreshold = Warm; break; case 'r': real = optarg; break; case 's': save = optarg; + break; case 't': trust = optarg; break; case 'u': user = optarg; break; case 'v': self.debug = true; break; case 'w': pass = optarg; @@ -200,10 +322,36 @@ int main(int argc, char *argv[]) { } 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; + if (printCert) { +#ifdef __OpenBSD__ + int error = pledge("stdio inet dns", NULL); + if (error) err(EX_OSERR, "pledge"); +#endif + ircConfig(true, NULL, NULL, NULL); + ircConnect(bind, host, port); + ircPrintCert(); + ircClose(); + return EX_OK; + } + + if (!self.nicks[0]) self.nicks[0] = getenv("USER"); + if (!self.nicks[0]) errx(EX_CONFIG, "USER unset"); + if (!user) user = self.nicks[0]; + if (!real) real = self.nicks[0]; + + if (pass && !pass[0]) { + char *buf = malloc(512); + if (!buf) err(EX_OSERR, "malloc"); + pass = readpassphrase("Server password: ", buf, 512, 0); + if (!pass) errx(EX_IOERR, "unable to read passphrase"); + } + + if (self.plainPass && !self.plainPass[0]) { + char *buf = malloc(512); + if (!buf) err(EX_OSERR, "malloc"); + self.plainPass = readpassphrase("Account password: ", buf, 512, 0); + if (!self.plainPass) errx(EX_IOERR, "unable to read passphrase"); + } // Modes defined in RFC 1459: set(&network.chanTypes, "#&"); @@ -217,29 +365,17 @@ int main(int argc, char *argv[]) { set(&network.name, host); set(&self.nick, "*"); - editCompleteAdd(); - commandCompleteAdd(); + inputCompletion(); - FILE *certFile = NULL; - FILE *privFile = NULL; - if (cert) { - certFile = configOpen(cert, "r"); - if (!certFile) return EX_NOINPUT; - } - if (priv) { - privFile = configOpen(priv, "r"); - if (!privFile) return EX_NOINPUT; - } - ircConfig(insecure, certFile, privFile); - if (certFile) fclose(certFile); - if (privFile) fclose(privFile); + ircConfig(insecure, trust, cert, priv); uiInit(); + sig_t cursesWinch = signal(SIGWINCH, signalHandler); if (save) { uiLoad(save); atexit(exitSave); } - uiShowID(Network); + windowShow(windowFor(Network)); uiFormat( Network, Cold, NULL, "\3%dcatgirl\3\tis GPLv3 fwee softwawe ^w^ " @@ -248,26 +384,33 @@ int main(int argc, char *argv[]) { ); uiFormat(Network, Cold, NULL, "Traveling..."); uiDraw(); - + + sandboxEarly(log); int irc = ircConnect(bind, host, port); - if (pass) ircFormat("PASS :%s\r\n", pass); + sandboxLate(irc); + + ircHandshake(); + if (pass) { + ircFormat("PASS :"); + ircSend(pass, strlen(pass)); + ircFormat("\r\n"); + explicit_bzero(pass, strlen(pass)); + } if (sasl) ircFormat("CAP REQ :sasl\r\n"); ircFormat("CAP LS\r\n"); - ircFormat("NICK :%s\r\n", nick); + ircFormat("NICK %s\r\n", self.nicks[0]); ircFormat("USER %s 0 * :%s\r\n", user, real); + // Avoid disabling VINTR until main loop. + inputInit(); signal(SIGHUP, signalHandler); signal(SIGINT, signalHandler); + signal(SIGALRM, signalHandler); signal(SIGTERM, signalHandler); signal(SIGCHLD, signalHandler); - sig_t cursesWinch = signal(SIGWINCH, signalHandler); - fcntl(irc, F_SETFD, FD_CLOEXEC); if (!self.restricted) { - int error = pipe(utilPipe); - if (error) err(EX_OSERR, "pipe"); - - error = pipe(execPipe); + int error = pipe(utilPipe) || pipe(execPipe); if (error) err(EX_OSERR, "pipe"); fcntl(utilPipe[0], F_SETFD, FD_CLOEXEC); @@ -276,6 +419,7 @@ int main(int argc, char *argv[]) { fcntl(execPipe[1], F_SETFD, FD_CLOEXEC); } + bool ping = false; struct pollfd fds[] = { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, @@ -286,7 +430,7 @@ int main(int argc, char *argv[]) { int nfds = poll(fds, (self.restricted ? 2 : ARRAY_LEN(fds)), -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); if (nfds > 0) { - if (fds[0].revents) uiRead(); + if (fds[0].revents) inputRead(); if (fds[1].revents) ircRecv(); if (fds[2].revents) utilRead(); if (fds[3].revents) execRead(); @@ -295,6 +439,25 @@ int main(int argc, char *argv[]) { if (signals[SIGHUP]) self.quit = "zzz"; if (signals[SIGINT] || signals[SIGTERM]) break; + if (nfds > 0 && fds[1].revents) { + ping = false; + struct itimerval timer = { + .it_value.tv_sec = 2 * 60, + .it_interval.tv_sec = 30, + }; + int error = setitimer(ITIMER_REAL, &timer, NULL); + if (error) err(EX_OSERR, "setitimer"); + } + if (signals[SIGALRM]) { + signals[SIGALRM] = 0; + if (ping) { + errx(EX_UNAVAILABLE, "ping timeout"); + } else { + ircFormat("PING nyaa\r\n"); + ping = true; + } + } + if (signals[SIGCHLD]) { signals[SIGCHLD] = 0; for (int status; 0 < waitpid(-1, &status, WNOHANG);) { @@ -317,10 +480,9 @@ int main(int argc, char *argv[]) { 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. + // doupdate(3) needs to be called for KEY_RESIZE to be picked up. uiDraw(); - uiRead(); + inputRead(); } uiDraw(); diff --git a/chat.h b/chat.h index 6e3d20f..2a41cf6 100644 --- a/chat.h +++ b/chat.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ */ #include <assert.h> +#include <ctype.h> #include <err.h> #include <getopt.h> #include <stdarg.h> @@ -33,6 +34,7 @@ #include <stdint.h> #include <stdio.h> #include <string.h> +#include <strings.h> #include <sysexits.h> #include <time.h> #include <wchar.h> @@ -43,28 +45,73 @@ typedef unsigned uint; typedef unsigned char byte; -struct Cat { - char *buf; - size_t cap; - size_t len; -}; -static inline void __attribute__((format(printf, 2, 3))) -catf(struct Cat *cat, const char *format, ...) { +static inline char *seprintf(char *ptr, char *end, const char *fmt, ...) + __attribute__((format(printf, 3, 4))); +static inline char *seprintf(char *ptr, char *end, const char *fmt, ...) { va_list ap; - va_start(ap, format); - int len = vsnprintf(&cat->buf[cat->len], cat->cap - cat->len, format, ap); - assert(len >= 0); + va_start(ap, fmt); + int n = vsnprintf(ptr, end - ptr, fmt, ap); va_end(ap); - cat->len += len; - if (cat->len >= cat->cap) cat->len = cat->cap - 1; + if (n < 0) return NULL; + if (n > end - ptr) return end; + return ptr + n; } +enum Attr { + BIT(Bold), + BIT(Reverse), + BIT(Italic), + BIT(Underline), +}; enum Color { White, Black, Blue, Green, Red, Brown, Magenta, Orange, Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, Default = 99, ColorCap, }; +struct Style { + enum Attr attr; + enum Color fg, bg; +}; + +static const struct Style StyleDefault = { 0, Default, Default }; +enum { B = '\2', C = '\3', O = '\17', R = '\26', I = '\35', U = '\37' }; + +static inline size_t styleParse(struct Style *style, const char **str) { + switch (**str) { + break; case B: (*str)++; style->attr ^= Bold; + break; case O: (*str)++; *style = StyleDefault; + break; case R: (*str)++; style->attr ^= Reverse; + break; case I: (*str)++; style->attr ^= Italic; + break; case U: (*str)++; style->attr ^= Underline; + break; case C: { + (*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'; + } + } + return strcspn(*str, (const char[]) { B, C, O, R, I, U, '\0' }); +} + +static inline void styleStrip(char *buf, size_t cap, const char *str) { + *buf = '\0'; + char *ptr = buf, *end = &buf[cap]; + struct Style style = StyleDefault; + while (*str) { + size_t len = styleParse(&style, &str); + ptr = seprintf(ptr, end, "%.*s", (int)len, str); + str += len; + } +} enum { None, Debug, Network, IDCap = 256 }; extern char *idNames[IDCap]; @@ -73,7 +120,7 @@ extern uint idNext; static inline uint idFind(const char *name) { for (uint id = 0; id < idNext; ++id) { - if (!strcmp(idNames[id], name)) return id; + if (!strcasecmp(idNames[id], name)) return id; } return None; } @@ -89,7 +136,8 @@ static inline uint idFor(const char *name) { } extern uint32_t hashInit; -static inline enum Color hash(const char *str) { +extern uint32_t hashBound; +static inline uint32_t _hash(const char *str) { if (*str == '~') str++; uint32_t hash = hashInit; for (; *str; ++str) { @@ -97,7 +145,11 @@ static inline enum Color hash(const char *str) { hash ^= *str; hash *= 0x27220A95; } - return Blue + hash % 74; + return hash; +} +static inline enum Color hash(const char *str) { + if (hashBound < Blue) return Default; + return Blue + _hash(str) % (hashBound + 1 - Blue); } extern struct Network { @@ -105,6 +157,7 @@ extern struct Network { uint userLen; uint hostLen; char *chanTypes; + char *statusmsg; char *prefixes; char *prefixModes; char *listModes; @@ -115,15 +168,31 @@ extern struct Network { char invex; } network; +static inline uint prefixBit(char p) { + char *s = strchr(network.prefixes, p); + if (!s) return 0; + return 1 << (s - network.prefixes); +} + +static inline char bitPrefix(uint p) { + for (uint i = 0; network.prefixes[i]; ++i) { + if (p & (1 << i)) return network.prefixes[i]; + } + return '\0'; +} + #define ENUM_CAP \ X("causal.agency/consumer", CapConsumer) \ X("chghost", CapChghost) \ X("extended-join", CapExtendedJoin) \ X("invite-notify", CapInviteNotify) \ + X("message-tags", CapMessageTags) \ X("multi-prefix", CapMultiPrefix) \ X("sasl", CapSASL) \ X("server-time", CapServerTime) \ - X("userhost-in-names", CapUserhostInNames) + X("setname", CapSetname) \ + X("userhost-in-names", CapUserhostInNames) \ + X("znc.in/self-message", CapSelfMessage) enum Cap { #define X(name, id) BIT(id), @@ -136,12 +205,16 @@ extern struct Self { bool restricted; size_t pos; enum Cap caps; - char *plain; - char *join; + const char *plainUser; + char *plainPass; + const char *nicks[8]; + const char *mode; + const char *join; char *nick; char *user; char *host; enum Color color; + char *invited; char *quit; } self; @@ -152,7 +225,9 @@ static inline void set(char **field, const char *value) { } #define ENUM_TAG \ + X("+draft/reply", TagReply) \ X("causal.agency/pos", TagPos) \ + X("msgid", TagMsgID) \ X("time", TagTime) enum Tag { @@ -172,8 +247,12 @@ struct Message { char *params[ParamCap]; }; -void ircConfig(bool insecure, FILE *cert, FILE *priv); +void ircConfig( + bool insecure, const char *trust, const char *cert, const char *priv +); int ircConnect(const char *bind, const char *host, const char *port); +void ircHandshake(void); +void ircPrintCert(void); void ircRecv(void); void ircSend(const char *ptr, size_t len); void ircFormat(const char *format, ...) @@ -198,80 +277,152 @@ static inline void utilPush(struct Util *util, const char *arg) { } } -extern struct Replies { - uint away; - uint ban; - uint excepts; - uint invex; - uint join; - uint list; - uint mode; - uint names; - uint topic; - uint whois; -} replies; +enum Reply { + ReplyAway = 1, + ReplyBan, + ReplyExcepts, + ReplyHelp, + ReplyInvex, + ReplyJoin, + ReplyList, + ReplyMode, + ReplyNames, + ReplyNamesAuto, + ReplyTopic, + ReplyTopicAuto, + ReplyWhois, + ReplyWhowas, + ReplyCap, +}; + +extern uint replies[ReplyCap]; void handle(struct Message *msg); void command(uint id, char *input); const char *commandIsPrivmsg(uint id, const char *input); const char *commandIsNotice(uint id, const char *input); const char *commandIsAction(uint id, const char *input); -void commandCompleteAdd(void); +size_t commandWillSplit(uint id, const char *input); +void commandCompletion(void); -enum Heat { Ice, Cold, Warm, Hot }; +enum Heat { + Ice, + Cold, + Warm, + Hot, +}; + +enum { + TitleCap = 256, + StatusLines = 1, + MarkerLines = 1, + SplitLines = 5, + InputLines = 1, + InputCols = 1024, +}; +extern char uiTitle[TitleCap]; +extern struct _win_st *uiStatus; +extern struct _win_st *uiMain; +extern struct _win_st *uiInput; +extern bool uiSpoilerReveal; extern struct Util uiNotifyUtil; void uiInit(void); +uint uiAttr(struct Style style); +short uiPair(struct Style style); void uiShow(void); void uiHide(void); void uiDraw(void); -void uiShowID(uint id); -void uiShowNum(uint num); -void uiMoveID(uint id, uint num); -void uiCloseID(uint id); -void uiCloseNum(uint id); -void uiRead(void); +void uiResize(void); void uiWrite(uint id, enum Heat heat, const time_t *time, const char *str); void uiFormat( uint 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, - EditTail, - EditPrev, - EditNext, - EditPrevWord, - EditNextWord, - EditDeleteHead, - EditDeleteTail, - EditDeletePrev, - EditDeleteNext, - EditDeletePrevWord, - EditDeleteNextWord, - EditPaste, - EditTranspose, - EditCollapse, - EditInsert, - EditComplete, - EditExpand, - EditEnter, +int uiSave(void); + +void inputInit(void); +void inputWait(void); +void inputUpdate(void); +bool inputPending(uint id); +void inputRead(void); +void inputCompletion(void); +int inputSave(FILE *file); +void inputLoad(FILE *file, size_t version); + +enum Scroll { + ScrollOne, + ScrollPage, + ScrollAll, + ScrollUnread, + ScrollHot, +}; +extern struct Time { + bool enable; + const char *format; + int width; +} windowTime; +extern enum Heat windowThreshold; +void windowInit(void); +void windowUpdate(void); +void windowResize(void); +bool windowWrite(uint id, enum Heat heat, const time_t *time, const char *str); +void windowBare(void); +uint windowID(void); +uint windowNum(void); +uint windowFor(uint id); +void windowShow(uint num); +void windowAuto(void); +void windowSwap(void); +void windowMove(uint from, uint to); +void windowClose(uint num); +void windowList(void); +void windowMark(void); +void windowUnmark(void); +void windowToggleMute(void); +void windowToggleTime(void); +void windowToggleThresh(int n); +bool windowTimeEnable(void); +void windowScroll(enum Scroll by, int n); +void windowSearch(const char *str, int dir); +int windowSave(FILE *file); +void windowLoad(FILE *file, size_t version); + +enum { BufferCap = 1024 }; +struct Buffer; +struct Line { + uint num; + enum Heat heat; + time_t time; + char *str; +}; +struct Buffer *bufferAlloc(void); +void bufferFree(struct Buffer *buffer); +const struct Line *bufferSoft(const struct Buffer *buffer, size_t i); +const struct Line *bufferHard(const struct Buffer *buffer, size_t i); +int bufferPush( + struct Buffer *buffer, int cols, enum Heat thresh, + enum Heat heat, time_t time, const char *str +); +int bufferReflow( + struct Buffer *buffer, int cols, enum Heat thresh, size_t tail +); + +struct Cursor { + uint gen; + struct Node *node; }; -void edit(uint id, enum Edit op, wchar_t ch); -char *editBuffer(size_t *pos); -void editCompleteAdd(void); - -const char *complete(uint id, const char *prefix); -void completeAccept(void); -void completeReject(void); -void completeAdd(uint id, const char *str, enum Color color); -void completeTouch(uint id, const char *str, enum Color color); -void completeReplace(uint id, const char *old, const char *new); +void completePush(uint id, const char *str, enum Color color); +void completePull(uint id, const char *str, enum Color color); +void completeReplace(const char *old, const char *new); void completeRemove(uint id, const char *str); -void completeClear(uint id); -uint completeID(const char *str); enum Color completeColor(uint id, const char *str); +uint *completeBits(uint id, const char *str); +const char *completePrefix(struct Cursor *curs, uint id, const char *prefix); +const char *completeSubstr(struct Cursor *curs, uint id, const char *substr); +const char *completeEach(struct Cursor *curs, uint id); +uint completeEachID(struct Cursor *curs, const char *str); +void completeAccept(struct Cursor *curs); +void completeReject(struct Cursor *curs); extern struct Util urlOpenUtil; extern struct Util urlCopyUtil; @@ -279,24 +430,31 @@ void urlScan(uint id, const char *nick, const char *mesg); void urlOpenCount(uint id, uint count); void urlOpenMatch(uint id, const char *str); void urlCopyMatch(uint id, const char *str); +int urlSave(FILE *file); +void urlLoad(FILE *file, size_t version); -enum { IgnoreCap = 256 }; -extern struct Ignore { - size_t len; - char *patterns[IgnoreCap]; -} ignore; -const char *ignoreAdd(const char *pattern); -bool ignoreRemove(const char *pattern); -enum Heat ignoreCheck(enum Heat heat, uint id, const struct Message *msg); +enum { FilterCap = 64 }; +extern struct Filter { + enum Heat heat; + char *mask; + char *cmd; + char *chan; + char *mesg; +} filters[FilterCap]; +struct Filter filterParse(enum Heat heat, char *pattern); +struct Filter filterAdd(enum Heat heat, const char *pattern); +bool filterRemove(struct Filter filter); +enum Heat filterCheck(enum Heat heat, uint id, const struct Message *msg); -extern bool logEnable; +void logOpen(void); void logFormat(uint id, const time_t *time, const char *format, ...) __attribute__((format(printf, 3, 4))); void logClose(void); +char *configPath(char *buf, size_t cap, const char *path, int i); +char *dataPath(char *buf, size_t cap, const char *path, int i); FILE *configOpen(const char *path, const char *mode); FILE *dataOpen(const char *path, const char *mode); -void dataMkdir(const char *path); int getopt_config( int argc, char *const *argv, diff --git a/command.c b/command.c index 4c51433..502ff17 100644 --- a/command.c +++ b/command.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -64,8 +64,7 @@ static void echoMessage(char *cmd, uint id, char *params) { handle(&msg); } -static void splitMessage(char *cmd, uint id, char *params) { - if (!params) return; +static int splitChunk(const char *cmd, uint id) { int overhead = snprintf( NULL, 0, ":%s!%*s@%*s %s %s :\r\n", self.nick, @@ -74,22 +73,32 @@ static void splitMessage(char *cmd, uint id, char *params) { cmd, idNames[id] ); assert(overhead > 0 && overhead < 512); - int chunk = 512 - overhead; + return 512 - overhead; +} + +static int splitLen(int chunk, const char *params) { + int len = 0; + size_t cap = 1 + strlen(params); + for (int n = 0; params[len] != '\n' && len + n <= chunk; len += n) { + n = mblen(¶ms[len], cap - len); + if (n < 0) { + n = 1; + mblen(NULL, 0); + } + if (!n) break; + } + return len; +} + +static void splitMessage(char *cmd, uint id, char *params) { + if (!params) return; + int chunk = splitChunk(cmd, id); if (strlen(params) <= (size_t)chunk && !strchr(params, '\n')) { echoMessage(cmd, id, params); return; } - while (*params) { - int len = 0; - for (int n = 0; params[len] != '\n' && len + n <= chunk; len += n) { - n = mblen(¶ms[len], 1 + strlen(¶ms[len])); - if (n < 0) { - n = 1; - mblen(NULL, 0); - } - if (!n) break; - } + int len = splitLen(chunk, params); char ch = params[len]; params[len] = '\0'; echoMessage(cmd, id, params); @@ -109,25 +118,47 @@ static void commandNotice(uint id, char *params) { static void commandMe(uint id, char *params) { char buf[512]; - snprintf(buf, sizeof(buf), "\1ACTION %s\1", (params ?: "")); - echoMessage("PRIVMSG", id, buf); + if (!params) params = ""; + int chunk = splitChunk("PRIVMSG \1ACTION\1", id); + if (strlen(params) <= (size_t)chunk && !strchr(params, '\n')) { + snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + echoMessage("PRIVMSG", id, buf); + return; + } + while (*params) { + int len = splitLen(chunk, params); + snprintf(buf, sizeof(buf), "\1ACTION %.*s\1", len, params); + echoMessage("PRIVMSG", id, buf); + params += len; + if (*params == '\n') params++; + } } static void commandMsg(uint id, char *params) { - id = idFor(strsep(¶ms, " ")); - splitMessage("PRIVMSG", id, params); + if (!params) return; + char *nick = strsep(¶ms, " "); + uint msg = idFor(nick); + if (idColors[msg] == Default) { + idColors[msg] = completeColor(id, nick); + } + if (params) { + splitMessage("PRIVMSG", msg, params); + } else { + windowShow(windowFor(msg)); + } } static void commandJoin(uint id, char *params) { + if (!params && id == Network) params = self.invited; if (!params) params = idNames[id]; uint count = 1; for (char *ch = params; *ch && *ch != ' '; ++ch) { if (*ch == ',') count++; } ircFormat("JOIN %s\r\n", params); - replies.join += count; - replies.topic += count; - replies.names += count; + replies[ReplyJoin] += count; + replies[ReplyTopic] += count; + replies[ReplyNames] += count; } static void commandPart(uint id, char *params) { @@ -145,8 +176,14 @@ static void commandQuit(uint id, char *params) { static void commandNick(uint id, char *params) { (void)id; - if (!params) return; - ircFormat("NICK :%s\r\n", params); + if (params) { + ircFormat("NICK :%s\r\n", params); + } else { + uiFormat( + Network, Warm, NULL, "You are \3%02d%s", + self.color, self.nick + ); + } } static void commandAway(uint id, char *params) { @@ -156,7 +193,13 @@ static void commandAway(uint id, char *params) { } else { ircFormat("AWAY\r\n"); } - replies.away++; + replies[ReplyAway]++; +} + +static void commandSetname(uint id, char *params) { + (void)id; + if (!params) return; + ircFormat("SETNAME :%s\r\n", params); } static void commandTopic(uint id, char *params) { @@ -164,14 +207,43 @@ static void commandTopic(uint id, char *params) { ircFormat("TOPIC %s :%s\r\n", idNames[id], params); } else { ircFormat("TOPIC %s\r\n", idNames[id]); - replies.topic++; + replies[ReplyTopic]++; } } static void commandNames(uint id, char *params) { (void)params; ircFormat("NAMES %s\r\n", idNames[id]); - replies.names++; + replies[ReplyNames]++; +} + +static void commandOps(uint id, char *params) { + (void)params; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf( + ptr, end, "The council of \3%02d%s\3 are ", + idColors[id], idNames[id] + ); + bool first = true; + struct Cursor curs = {0}; + for (const char *nick; (nick = completeEach(&curs, id));) { + char prefix = bitPrefix(*completeBits(id, nick)); + if (!prefix || prefix == '+') continue; + ptr = seprintf( + ptr, end, "%s\3%02d%c%s\3", + (first ? "" : ", "), completeColor(id, nick), prefix, nick + ); + first = false; + } + if (first) { + uiFormat( + id, Warm, NULL, "\3%02d%s\3 is a lawless wasteland", + idColors[id], idNames[id] + ); + } else { + uiWrite(id, Warm, NULL, buf); + } } static void commandInvite(uint id, char *params) { @@ -196,14 +268,20 @@ static void commandMode(uint id, char *params) { ircFormat("MODE %s %s\r\n", self.nick, params); } else { ircFormat("MODE %s\r\n", self.nick); - replies.mode++; + replies[ReplyMode]++; } } else { if (params) { + if (!params[1] || (params[0] == '+' && !params[2])) { + char m = (params[0] == '+' ? params[1] : params[0]); + if (m == 'b') replies[ReplyBan]++; + if (m == network.excepts) replies[ReplyExcepts]++; + if (m == network.invex) replies[ReplyInvex]++; + } ircFormat("MODE %s %s\r\n", idNames[id], params); } else { ircFormat("MODE %s\r\n", idNames[id]); - replies.mode++; + replies[ReplyMode]++; } } } @@ -221,7 +299,7 @@ static void commandOp(uint id, char *params) { if (params) { channelListMode(id, '+', 'o', params); } else { - ircFormat("PRIVMSG ChanServ :OP %s\r\n", idNames[id]); + ircFormat("CS OP %s\r\n", idNames[id]); } } @@ -233,7 +311,7 @@ static void commandVoice(uint id, char *params) { if (params) { channelListMode(id, '+', 'v', params); } else { - ircFormat("PRIVMSG ChanServ :VOICE %s\r\n", idNames[id]); + ircFormat("CS VOICE %s\r\n", idNames[id]); } } @@ -246,7 +324,7 @@ static void commandBan(uint id, char *params) { channelListMode(id, '+', 'b', params); } else { ircFormat("MODE %s b\r\n", idNames[id]); - replies.ban++; + replies[ReplyBan]++; } } @@ -260,7 +338,7 @@ static void commandExcept(uint id, char *params) { channelListMode(id, '+', network.excepts, params); } else { ircFormat("MODE %s %c\r\n", idNames[id], network.excepts); - replies.excepts++; + replies[ReplyExcepts]++; } } @@ -274,7 +352,7 @@ static void commandInvex(uint id, char *params) { channelListMode(id, '+', network.invex, params); } else { ircFormat("MODE %s %c\r\n", idNames[id], network.invex); - replies.invex++; + replies[ReplyInvex]++; } } @@ -290,40 +368,65 @@ static void commandList(uint id, char *params) { } else { ircFormat("LIST\r\n"); } - replies.list++; + replies[ReplyList]++; } static void commandWhois(uint id, char *params) { (void)id; + if (!params) params = self.nick; + uint count = 1; + for (char *ch = params; *ch; ++ch) { + if (*ch == ',') count++; + } + ircFormat("WHOIS %s\r\n", params); + replies[ReplyWhois] += count; +} + +static void commandWhowas(uint id, char *params) { + (void)id; if (!params) return; - ircFormat("WHOIS :%s\r\n", params); - replies.whois++; + ircFormat("WHOWAS %s\r\n", params); + replies[ReplyWhowas]++; } static void commandNS(uint id, char *params) { (void)id; - if (params) ircFormat("PRIVMSG NickServ :%s\r\n", params); + ircFormat("NS %s\r\n", (params ?: "HELP")); } static void commandCS(uint id, char *params) { (void)id; - if (params) ircFormat("PRIVMSG ChanServ :%s\r\n", params); + ircFormat("CS %s\r\n", (params ?: "HELP")); } static void commandQuery(uint id, char *params) { if (!params) return; uint query = idFor(params); - idColors[query] = completeColor(id, params); - uiShowID(query); + if (idColors[query] == Default) { + idColors[query] = completeColor(id, params); + } + windowShow(windowFor(query)); } static void commandWindow(uint id, char *params) { - if (!params) return; - if (isdigit(params[0])) { - uiShowNum(strtoul(params, NULL, 10)); + if (!params) { + windowList(); + } else if (isdigit(params[0])) { + windowShow(strtoul(params, NULL, 10)); } else { id = idFind(params); - if (id) uiShowID(id); + if (id) { + windowShow(windowFor(id)); + return; + } + struct Cursor curs = {0}; + for (const char *str; (str = completeSubstr(&curs, None, params));) { + id = idFind(str); + if (!id) continue; + completeAccept(&curs); + windowShow(windowFor(id)); + break; + } } } @@ -332,20 +435,20 @@ static void commandMove(uint id, char *params) { char *name = strsep(¶ms, " "); if (params) { id = idFind(name); - if (id) uiMoveID(id, strtoul(params, NULL, 10)); + if (id) windowMove(windowFor(id), strtoul(params, NULL, 10)); } else { - uiMoveID(id, strtoul(name, NULL, 10)); + windowMove(windowFor(id), strtoul(name, NULL, 10)); } } static void commandClose(uint id, char *params) { if (!params) { - uiCloseID(id); + windowClose(windowFor(id)); } else if (isdigit(params[0])) { - uiCloseNum(strtoul(params, NULL, 10)); + windowClose(strtoul(params, NULL, 10)); } else { id = idFind(params); - if (id) uiCloseID(id); + if (id) windowClose(windowFor(id)); } } @@ -363,33 +466,50 @@ static void commandCopy(uint id, char *params) { urlCopyMatch(id, params); } -static void commandIgnore(uint id, char *params) { +static void commandFilter(enum Heat heat, uint id, char *params) { if (params) { - const char *pattern = ignoreAdd(params); + struct Filter filter = filterAdd(heat, params); uiFormat( - id, Cold, NULL, "Ignoring \3%02d%s\3", - Brown, pattern + id, Cold, NULL, "%sing \3%02d%s %s %s %s", + (heat == Hot ? "Highlight" : "Ignor"), Brown, filter.mask, + (filter.cmd ?: ""), (filter.chan ?: ""), (filter.mesg ?: "") ); } else { - for (size_t i = 0; i < ignore.len; ++i) { + for (size_t i = 0; i < FilterCap && filters[i].mask; ++i) { + if (filters[i].heat != heat) continue; uiFormat( - Network, Warm, NULL, "Ignoring \3%02d%s\3", - Brown, ignore.patterns[i] + Network, Warm, NULL, "%sing \3%02d%s %s %s %s", + (heat == Hot ? "Highlight" : "Ignor"), Brown, filters[i].mask, + (filters[i].cmd ?: ""), (filters[i].chan ?: ""), + (filters[i].mesg ?: "") ); } } } -static void commandUnignore(uint id, char *params) { +static void commandUnfilter(enum Heat heat, uint id, char *params) { if (!params) return; - if (ignoreRemove(params)) { - uiFormat( - id, Cold, NULL, "No longer ignoring \3%02d%s\3", - Brown, params - ); - } else { - uiFormat(id, Cold, NULL, "Not ignoring \3%02d%s\3", Brown, params); - } + struct Filter filter = filterParse(heat, params); + bool found = filterRemove(filter); + uiFormat( + id, Cold, NULL, "%s %sing \3%02d%s %s %s %s", + (found ? "No longer" : "Not"), (heat == Hot ? "highlight" : "ignor"), + Brown, filter.mask, (filter.cmd ?: ""), (filter.chan ?: ""), + (filter.mesg ?: "") + ); +} + +static void commandHighlight(uint id, char *params) { + commandFilter(Hot, id, params); +} +static void commandIgnore(uint id, char *params) { + commandFilter(Ice, id, params); +} +static void commandUnhighlight(uint id, char *params) { + commandUnfilter(Hot, id, params); +} +static void commandUnignore(uint id, char *params) { + commandUnfilter(Ice, id, params); } static void commandExec(uint id, char *params) { @@ -399,26 +519,37 @@ static void commandExec(uint id, char *params) { if (pid < 0) err(EX_OSERR, "fork"); if (pid) return; + setsid(); close(STDIN_FILENO); dup2(execPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); const char *shell = getenv("SHELL") ?: "/bin/sh"; - execlp(shell, shell, "-c", params, NULL); + execl(shell, shell, "-c", params, NULL); warn("%s", shell); _exit(EX_UNAVAILABLE); } static void commandHelp(uint id, char *params) { (void)id; - uiHide(); + if (params) { + ircFormat("HELP :%s\r\n", params); + replies[ReplyHelp]++; + return; + } + if (self.restricted) { + uiFormat(id, Warm, NULL, "See catgirl(1) or /help index"); + return; + } + + 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 ?: "COMMANDS")); + snprintf(buf, sizeof(buf), "%sp^COMMANDS$", (getenv("LESS") ?: "")); setenv("LESS", buf, 1); execlp("man", "man", "1", "catgirl", NULL); dup2(utilPipe[1], STDERR_FILENO); @@ -428,55 +559,61 @@ static void commandHelp(uint id, char *params) { enum Flag { BIT(Multiline), - BIT(Restricted), + BIT(Restrict), }; static const struct Handler { const char *cmd; Command *fn; enum Flag flags; + enum Cap caps; } Commands[] = { - { "/away", commandAway, 0 }, - { "/ban", commandBan, 0 }, - { "/close", commandClose, 0 }, - { "/copy", commandCopy, Restricted }, - { "/cs", commandCS, 0 }, - { "/debug", commandDebug, Restricted }, - { "/deop", commandDeop, 0 }, - { "/devoice", commandDevoice, 0 }, - { "/except", commandExcept, 0 }, - { "/exec", commandExec, Multiline | Restricted }, - { "/help", commandHelp, 0 }, - { "/ignore", commandIgnore, 0 }, - { "/invex", commandInvex, 0 }, - { "/invite", commandInvite, 0 }, - { "/join", commandJoin, Restricted }, - { "/kick", commandKick, 0 }, - { "/list", commandList, 0 }, - { "/me", commandMe, 0 }, - { "/mode", commandMode, 0 }, - { "/move", commandMove, 0 }, - { "/msg", commandMsg, Multiline | Restricted }, - { "/names", commandNames, 0 }, - { "/nick", commandNick, 0 }, - { "/notice", commandNotice, Multiline }, - { "/ns", commandNS, 0 }, - { "/o", commandOpen, Restricted }, - { "/op", commandOp, 0 }, - { "/open", commandOpen, Restricted }, - { "/part", commandPart, 0 }, - { "/query", commandQuery, Restricted }, - { "/quit", commandQuit, 0 }, - { "/quote", commandQuote, Multiline | Restricted }, - { "/say", commandPrivmsg, Multiline }, - { "/topic", commandTopic, 0 }, - { "/unban", commandUnban, 0 }, - { "/unexcept", commandUnexcept, 0 }, - { "/unignore", commandUnignore, 0 }, - { "/uninvex", commandUninvex, 0 }, - { "/voice", commandVoice, 0 }, - { "/whois", commandWhois, 0 }, - { "/window", commandWindow, 0 }, + { "/away", commandAway, 0, 0 }, + { "/ban", commandBan, 0, 0 }, + { "/close", commandClose, 0, 0 }, + { "/copy", commandCopy, Restrict, 0 }, + { "/cs", commandCS, 0, 0 }, + { "/debug", commandDebug, 0, 0 }, + { "/deop", commandDeop, 0, 0 }, + { "/devoice", commandDevoice, 0, 0 }, + { "/except", commandExcept, 0, 0 }, + { "/exec", commandExec, Multiline | Restrict, 0 }, + { "/help", commandHelp, 0, 0 }, // Restrict special case. + { "/highlight", commandHighlight, 0, 0 }, + { "/ignore", commandIgnore, 0, 0 }, + { "/invex", commandInvex, 0, 0 }, + { "/invite", commandInvite, 0, 0 }, + { "/join", commandJoin, 0, 0 }, + { "/kick", commandKick, 0, 0 }, + { "/list", commandList, 0, 0 }, + { "/me", commandMe, Multiline, 0 }, + { "/mode", commandMode, 0, 0 }, + { "/move", commandMove, 0, 0 }, + { "/msg", commandMsg, Multiline, 0 }, + { "/names", commandNames, 0, 0 }, + { "/nick", commandNick, 0, 0 }, + { "/notice", commandNotice, Multiline, 0 }, + { "/ns", commandNS, 0, 0 }, + { "/o", commandOpen, Restrict, 0 }, + { "/op", commandOp, 0, 0 }, + { "/open", commandOpen, Restrict, 0 }, + { "/ops", commandOps, 0, 0 }, + { "/part", commandPart, 0, 0 }, + { "/query", commandQuery, 0, 0 }, + { "/quit", commandQuit, 0, 0 }, + { "/quote", commandQuote, Multiline, 0 }, + { "/say", commandPrivmsg, Multiline, 0 }, + { "/setname", commandSetname, 0, CapSetname }, + { "/topic", commandTopic, 0, 0 }, + { "/unban", commandUnban, 0, 0 }, + { "/unexcept", commandUnexcept, 0, 0 }, + { "/unhighlight", commandUnhighlight, 0, 0 }, + { "/unignore", commandUnignore, 0, 0 }, + { "/uninvex", commandUninvex, 0, 0 }, + { "/voice", commandVoice, 0, 0 }, + { "/whois", commandWhois, 0, 0 }, + { "/whowas", commandWhowas, 0, 0 }, + { "/window", commandWindow, 0, 0 }, }; static int compar(const void *cmd, const void *_handler) { @@ -505,6 +642,41 @@ const char *commandIsAction(uint id, const char *input) { return &input[4]; } +size_t commandWillSplit(uint id, const char *input) { + int chunk; + const char *params; + if (NULL != (params = commandIsPrivmsg(id, input))) { + chunk = splitChunk("PRIVMSG", id); + } else if (NULL != (params = commandIsNotice(id, input))) { + chunk = splitChunk("NOTICE", id); + } else if (NULL != (params = commandIsAction(id, input))) { + chunk = splitChunk("PRIVMSG \1ACTION\1", id); + } else if (id != Network && id != Debug && !strncmp(input, "/say ", 5)) { + params = &input[5]; + chunk = splitChunk("PRIVMSG", id); + } else { + return 0; + } + if (strlen(params) <= (size_t)chunk) return 0; + for ( + int split; + params[(split = splitLen(chunk, params))]; + params = ¶ms[split + 1] + ) { + if (params[split] == '\n') continue; + return (params - input) + split; + } + return 0; +} + +static bool commandAvailable(const struct Handler *handler) { + if (handler->flags & Restrict && self.restricted) return false; + if (handler->caps && (handler->caps & self.caps) != handler->caps) { + return false; + } + return true; +} + void command(uint id, char *input) { if (id == Debug && input[0] != '/' && !self.restricted) { commandQuote(id, input); @@ -519,11 +691,11 @@ void command(uint id, char *input) { return; } + struct Cursor curs = {0}; const char *cmd = strsep(&input, " "); - const char *unique = complete(None, cmd); - if (unique && !complete(None, cmd)) { + const char *unique = completePrefix(&curs, None, cmd); + if (unique && !completePrefix(&curs, None, cmd)) { cmd = unique; - completeReject(); } const struct Handler *handler = bsearch( @@ -533,8 +705,8 @@ void command(uint id, char *input) { uiFormat(id, Warm, NULL, "No such command %s", cmd); return; } - if (self.restricted && handler->flags & Restricted) { - uiFormat(id, Warm, NULL, "Command %s is restricted", cmd); + if (!commandAvailable(handler)) { + uiFormat(id, Warm, NULL, "Command %s is unavailable", cmd); return; } @@ -550,8 +722,9 @@ void command(uint id, char *input) { handler->fn(id, input); } -void commandCompleteAdd(void) { +void commandCompletion(void) { for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { - completeAdd(None, Commands[i].cmd, Default); + if (!commandAvailable(&Commands[i])) continue; + completePush(None, Commands[i].cmd, Default); } } diff --git a/compat_readpassphrase.c b/compat_readpassphrase.c new file mode 100644 index 0000000..3bb2045 --- /dev/null +++ b/compat_readpassphrase.c @@ -0,0 +1,206 @@ +/* + * Original: readpassphrase.c in OpenSSH portable + */ +/* + * Copyright (c) 2000-2002, 2007, 2010 + * Todd C. Miller <millert@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +/* + * Macros and function required for readpassphrase(3). + */ +#define RPP_ECHO_OFF 0x00 +#define RPP_ECHO_ON 0x01 +#define RPP_REQUIRE_TTY 0x02 +#define RPP_FORCELOWER 0x04 +#define RPP_FORCEUPPER 0x08 +#define RPP_SEVENBIT 0x10 +#define RPP_STDIN 0x20 +char *readpassphrase(const char *, char *, size_t, int); + +#if !defined(_NSIG) && defined(NSIG) +# define _NSIG NSIG +#endif + +static volatile sig_atomic_t readpassphrase_signo[_NSIG]; + +static void +readpassphrase_handler(int s) +{ + + readpassphrase_signo[s] = 1; +} + +char * +readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags) +{ + ssize_t nr; + int input, output, save_errno, i, need_restart; + char ch, *p, *end; + struct termios term, oterm; + struct sigaction sa, savealrm, saveint, savehup, savequit, saveterm; + struct sigaction savetstp, savettin, savettou, savepipe; +/* If we don't have TCSASOFT define it so that ORing it it below is a no-op. */ +#ifndef TCSASOFT + const int tcasoft = 0; +#else + const int tcasoft = TCSASOFT; +#endif + + /* I suppose we could alloc on demand in this case (XXX). */ + if (bufsiz == 0) { + errno = EINVAL; + return(NULL); + } + +restart: + for (i = 0; i < _NSIG; i++) + readpassphrase_signo[i] = 0; + nr = -1; + save_errno = 0; + need_restart = 0; + /* + * Read and write to /dev/tty if available. If not, read from + * stdin and write to stderr unless a tty is required. + */ + if ((flags & RPP_STDIN) || + (input = output = open(_PATH_TTY, O_RDWR)) == -1) { + if (flags & RPP_REQUIRE_TTY) { + errno = ENOTTY; + return(NULL); + } + input = STDIN_FILENO; + output = STDERR_FILENO; + } + + /* + * Turn off echo if possible. + * If we are using a tty but are not the foreground pgrp this will + * generate SIGTTOU, so do it *before* installing the signal handlers. + */ + if (input != STDIN_FILENO && tcgetattr(input, &oterm) == 0) { + memcpy(&term, &oterm, sizeof(term)); + if (!(flags & RPP_ECHO_ON)) + term.c_lflag &= ~(ECHO | ECHONL); +#ifdef VSTATUS + if (term.c_cc[VSTATUS] != _POSIX_VDISABLE) + term.c_cc[VSTATUS] = _POSIX_VDISABLE; +#endif + (void)tcsetattr(input, TCSAFLUSH|tcasoft, &term); + } else { + memset(&term, 0, sizeof(term)); + term.c_lflag |= ECHO; + memset(&oterm, 0, sizeof(oterm)); + oterm.c_lflag |= ECHO; + } + + /* + * Catch signals that would otherwise cause the user to end + * up with echo turned off in the shell. Don't worry about + * things like SIGXCPU and SIGVTALRM for now. + */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; /* don't restart system calls */ + sa.sa_handler = readpassphrase_handler; + (void)sigaction(SIGALRM, &sa, &savealrm); + (void)sigaction(SIGHUP, &sa, &savehup); + (void)sigaction(SIGINT, &sa, &saveint); + (void)sigaction(SIGPIPE, &sa, &savepipe); + (void)sigaction(SIGQUIT, &sa, &savequit); + (void)sigaction(SIGTERM, &sa, &saveterm); + (void)sigaction(SIGTSTP, &sa, &savetstp); + (void)sigaction(SIGTTIN, &sa, &savettin); + (void)sigaction(SIGTTOU, &sa, &savettou); + + if (!(flags & RPP_STDIN)) + (void)write(output, prompt, strlen(prompt)); + end = buf + bufsiz - 1; + p = buf; + while ((nr = read(input, &ch, 1)) == 1 && ch != '\n' && ch != '\r') { + if (p < end) { + if ((flags & RPP_SEVENBIT)) + ch &= 0x7f; + if (isalpha((unsigned char)ch)) { + if ((flags & RPP_FORCELOWER)) + ch = (char)tolower((unsigned char)ch); + if ((flags & RPP_FORCEUPPER)) + ch = (char)toupper((unsigned char)ch); + } + *p++ = ch; + } + } + *p = '\0'; + save_errno = errno; + if (!(term.c_lflag & ECHO)) + (void)write(output, "\n", 1); + + /* Restore old terminal settings and signals. */ + if (memcmp(&term, &oterm, sizeof(term)) != 0) { + const int sigttou = readpassphrase_signo[SIGTTOU]; + + /* Ignore SIGTTOU generated when we are not the fg pgrp. */ + while (tcsetattr(input, TCSAFLUSH|tcasoft, &oterm) == -1 && + errno == EINTR && !readpassphrase_signo[SIGTTOU]) + continue; + readpassphrase_signo[SIGTTOU] = sigttou; + } + (void)sigaction(SIGALRM, &savealrm, NULL); + (void)sigaction(SIGHUP, &savehup, NULL); + (void)sigaction(SIGINT, &saveint, NULL); + (void)sigaction(SIGQUIT, &savequit, NULL); + (void)sigaction(SIGPIPE, &savepipe, NULL); + (void)sigaction(SIGTERM, &saveterm, NULL); + (void)sigaction(SIGTSTP, &savetstp, NULL); + (void)sigaction(SIGTTIN, &savettin, NULL); + (void)sigaction(SIGTTOU, &savettou, NULL); + if (input != STDIN_FILENO) + (void)close(input); + + /* + * If we were interrupted by a signal, resend it to ourselves + * now that we have restored the signal handlers. + */ + for (i = 0; i < _NSIG; i++) { + if (readpassphrase_signo[i]) { + kill(getpid(), i); + switch (i) { + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + need_restart = 1; + } + } + } + if (need_restart) + goto restart; + + if (save_errno) + errno = save_errno; + return(nr == -1 ? NULL : buf); +} diff --git a/complete.c b/complete.c index 5835926..3552c7c 100644 --- a/complete.c +++ b/complete.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020, 2022 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,7 +26,6 @@ */ #include <err.h> -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sysexits.h> @@ -37,25 +36,26 @@ struct Node { uint id; char *str; enum Color color; + uint bits; struct Node *prev; struct Node *next; }; +static uint gen; +static struct Node *head; +static struct Node *tail; + static struct Node *alloc(uint id, const char *str, enum Color color) { - struct Node *node = malloc(sizeof(*node)); - if (!node) err(EX_OSERR, "malloc"); + struct Node *node = calloc(1, sizeof(*node)); + if (!node) err(EX_OSERR, "calloc"); node->id = id; node->str = strdup(str); - node->color = color; - node->prev = NULL; - node->next = NULL; if (!node->str) err(EX_OSERR, "strdup"); + node->color = color; + node->bits = 0; 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; @@ -86,67 +86,39 @@ static struct Node *append(struct Node *node) { static struct Node *find(uint id, const char *str) { for (struct Node *node = head; node; node = node->next) { - if (node->id != id) continue; - if (strcmp(node->str, str)) continue; - return node; + if (node->id == id && !strcmp(node->str, str)) return node; } return NULL; } -void completeAdd(uint id, const char *str, enum Color color) { - if (!find(id, str)) append(alloc(id, str, color)); -} - -void completeTouch(uint id, const char *str, enum Color color) { - struct Node *node = find(id, str); - if (node) node->color = color; - prepend(node ? detach(node) : alloc(id, str, color)); -} - -enum Color completeColor(uint id, const char *str) { +void completePush(uint id, const char *str, enum Color color) { struct Node *node = find(id, str); - return (node ? node->color : Default); -} - -static struct Node *match; - -const char *complete(uint 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; + if (node) { + if (color != Default) node->color = color; + } else { + append(alloc(id, str, color)); } - return NULL; -} - -void completeAccept(void) { - if (match) prepend(detach(match)); - match = NULL; -} - -void completeReject(void) { - match = NULL; } -static struct Node *iter; - -uint completeID(const char *str) { - for (iter = (iter ? iter->next : head); iter; iter = iter->next) { - if (iter->id && !strcmp(iter->str, str)) return iter->id; +void completePull(uint id, const char *str, enum Color color) { + struct Node *node = find(id, str); + if (node) { + if (color != Default) node->color = color; + prepend(detach(node)); + } else { + prepend(alloc(id, str, color)); } - return None; } -void completeReplace(uint id, const char *old, const char *new) { +void completeReplace(const char *old, const char *new) { 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, old)) continue; free(node->str); node->str = strdup(new); - prepend(detach(node)); if (!node->str) err(EX_OSERR, "strdup"); + prepend(detach(node)); } } @@ -155,24 +127,83 @@ void completeRemove(uint id, const char *str) { 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; - if (iter == node) iter = NULL; + if (str && strcmp(node->str, str)) continue; detach(node); free(node->str); free(node); } + gen++; } -void completeClear(uint 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; - if (iter == node) iter = NULL; - detach(node); - free(node->str); - free(node); +enum Color completeColor(uint id, const char *str) { + struct Node *node = find(id, str); + return (node ? node->color : Default); +} + +uint *completeBits(uint id, const char *str) { + struct Node *node = find(id, str); + return (node ? &node->bits : NULL); +} + +const char *completePrefix(struct Cursor *curs, uint id, const char *prefix) { + size_t len = strlen(prefix); + if (curs->gen != gen) curs->node = NULL; + for ( + curs->gen = gen, curs->node = (curs->node ? curs->node->next : head); + curs->node; + curs->node = curs->node->next + ) { + if (curs->node->id && curs->node->id != id) continue; + if (!strncasecmp(curs->node->str, prefix, len)) return curs->node->str; + } + return NULL; +} + +const char *completeSubstr(struct Cursor *curs, uint id, const char *substr) { + if (curs->gen != gen) curs->node = NULL; + for ( + curs->gen = gen, curs->node = (curs->node ? curs->node->next : head); + curs->node; + curs->node = curs->node->next + ) { + if (curs->node->id && curs->node->id != id) continue; + if (strstr(curs->node->str, substr)) return curs->node->str; + } + return NULL; +} + +const char *completeEach(struct Cursor *curs, uint id) { + if (curs->gen != gen) curs->node = NULL; + for ( + curs->gen = gen, curs->node = (curs->node ? curs->node->next : head); + curs->node; + curs->node = curs->node->next + ) { + if (curs->node->id == id) return curs->node->str; + } + return NULL; +} + +uint completeEachID(struct Cursor *curs, const char *str) { + if (curs->gen != gen) curs->node = NULL; + for ( + curs->gen = gen, curs->node = (curs->node ? curs->node->next : head); + curs->node; + curs->node = curs->node->next + ) { + if (!curs->node->id) continue; + if (!strcmp(curs->node->str, str)) return curs->node->id; + } + return None; +} + +void completeAccept(struct Cursor *curs) { + if (curs->gen == gen && curs->node) { + prepend(detach(curs->node)); } + curs->node = NULL; +} + +void completeReject(struct Cursor *curs) { + curs->node = NULL; } diff --git a/config.c b/config.c index 121a849..be88f2f 100644 --- a/config.c +++ b/config.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 C. McEnroe <june@causal.agency> +/* Copyright (C) 2019 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,21 +56,17 @@ int getopt_config( 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) return clean('?'); - } else { - return clean(-1); + if (optind == argc) return clean(-1); + if (opt >= 0 && argv[optind][0] == '-') { + opt = getopt_long(argc, argv, optstring, longopts, longindex); + if (opt >= 0 || optind == argc) return clean(opt); } + num = 0; + path = argv[optind++]; + file = configOpen(path, "r"); + if (!file) return clean('?'); } for (;;) { diff --git a/configure b/configure index d59e7ba..07e3245 100755 --- a/configure +++ b/configure @@ -1,23 +1,25 @@ #!/bin/sh set -eu +: ${PKG_CONFIG:=pkg-config} + cflags() { echo "CFLAGS += $*" } -ldlibs() { - echo "LDLIBS ${o:-}= $*" - o=+ -} -config() { - pkg-config --print-errors "$@" - cflags $(pkg-config --cflags "$@") - ldlibs $(pkg-config --libs "$@") -} defstr() { cflags "-D'$1=\"$2\"'" } defvar() { - defstr "$1" "$(pkg-config --variable=$3 $2)${4:-}" + defstr "$1" "$(${PKG_CONFIG} --variable=$3 $2)${4:-}" +} +ldadd() { + lib=$1; shift + echo "LDADD.${lib} = $*" +} +config() { + ${PKG_CONFIG} --print-errors "$@" + cflags $(${PKG_CONFIG} --cflags "$@") + for lib; do ldadd $lib $(${PKG_CONFIG} --libs $lib); done } exec >config.mk @@ -25,25 +27,26 @@ exec >config.mk for opt; do case "${opt}" in (--prefix=*) echo "PREFIX = ${opt#*=}" ;; + (--bindir=*) echo "BINDIR = ${opt#*=}" ;; (--mandir=*) echo "MANDIR = ${opt#*=}" ;; + (--enable-sandman) echo 'BINS += sandman' ;; (*) echo "warning: unsupported option ${opt}" >&2 ;; esac done case "$(uname)" in (FreeBSD) - ldlibs -lncursesw config libtls - defvar OPENSSL_BIN openssl exec_prefix /bin/openssl + defstr OPENSSL_BIN /usr/bin/openssl ;; (OpenBSD) - ldlibs -lncursesw -ltls defstr OPENSSL_BIN /usr/bin/openssl ;; (Linux) cflags -Wno-pedantic -D_GNU_SOURCE config libtls ncursesw defvar OPENSSL_BIN openssl exec_prefix /bin/openssl + echo 'OBJS += compat_readpassphrase.o' ;; (Darwin) cflags -D__STDC_WANT_LIB_EXT1__=1 diff --git a/edit.c b/edit.c index d63678d..effb623 100644 --- a/edit.c +++ b/edit.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020, 2022 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,264 +25,290 @@ * covered work. */ -#include <assert.h> +#include <errno.h> #include <limits.h> #include <stdbool.h> -#include <stdio.h> #include <stdlib.h> #include <wchar.h> #include <wctype.h> -#include "chat.h" +#include "edit.h" -enum { Cap = 1024 }; -static wchar_t buf[Cap]; -static size_t len; -static size_t pos; +static bool isword(wchar_t ch) { + return !iswspace(ch) && !iswpunct(ch); +} -char *editBuffer(size_t *mbsPos) { - static char mbs[MB_LEN_MAX * Cap]; +char *editString(const struct Edit *e, char **buf, size_t *cap, size_t *pos) { + size_t req = e->len * MB_CUR_MAX + 1; + if (req > *cap) { + char *new = realloc(*buf, req); + if (!new) return NULL; + *buf = new; + *cap = req; + } - const wchar_t *ptr = buf; - size_t mbsLen = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL); - assert(mbsLen != (size_t)-1); - if (mbsPos) *mbsPos = mbsLen; + const wchar_t *ptr = e->buf; + size_t len = wcsnrtombs(*buf, &ptr, e->pos, *cap-1, NULL); + if (len == (size_t)-1) return NULL; + if (pos) *pos = len; - ptr = &buf[pos]; + ptr = &e->buf[e->pos]; size_t n = wcsnrtombs( - &mbs[mbsLen], &ptr, len - pos, sizeof(mbs) - mbsLen - 1, NULL + *buf + len, &ptr, e->len - e->pos, *cap-1 - len, NULL ); - assert(n != (size_t)-1); - mbsLen += n; + if (n == (size_t)-1) return NULL; + len += n; - mbs[mbsLen] = '\0'; - return mbs; + (*buf)[len] = '\0'; + return *buf; } -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(bool copy, size_t index, size_t count) { - if (index + count > len) return; - if (copy) { - memcpy(cut.buf, &buf[index], sizeof(*buf) * count); - cut.len = count; +int editReserve(struct Edit *e, size_t index, size_t count) { + if (index > e->len) { + errno = EINVAL; + return -1; } - memmove( - &buf[index], &buf[index + count], sizeof(*buf) * (len - index - count) - ); - len -= count; -} - -static const struct { - const wchar_t *name; - const wchar_t *string; -} Macros[] = { - { L"\\banhammer", L"▬▬▬▬▬▬▬▋ Ò╭╮Ó" }, - { L"\\bear", L"ʕっ•ᴥ•ʔっ" }, - { L"\\blush", L"(˶′◡‵˶)" }, - { L"\\cool", L"(⌐■_■)" }, - { L"\\flip", L"(╯°□°)╯︵ ┻━┻" }, - { L"\\gary", L"ᕕ( ᐛ )ᕗ" }, - { L"\\hug", L"(っ・∀・)っ" }, - { L"\\lenny", L"( ͡° ͜ʖ ͡°)" }, - { L"\\look", L"ಠ_ಠ" }, - { L"\\shrug", L"¯\\_(ツ)_/¯" }, - { L"\\unflip", L"┬─┬ノ(º_ºノ)" }, - { L"\\wave", L"ヾ(^∇^)" }, -}; - -void editCompleteAdd(void) { - char mbs[256]; - for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) { - size_t n = wcstombs(mbs, Macros[i].name, sizeof(mbs)); - assert(n != (size_t)-1); - completeAdd(None, mbs, Default); + if (e->len + count > e->cap) { + size_t cap = (e->cap ?: 256); + while (cap < e->len + count) cap *= 2; + wchar_t *buf = realloc(e->buf, sizeof(*buf) * cap); + if (!buf) return -1; + e->buf = buf; + e->cap = cap; } + wmemmove(&e->buf[index + count], &e->buf[index], e->len - index); + e->len += count; + return 0; } -static void macroExpand(void) { - size_t macro = pos; - while (macro && !iswspace(buf[macro - 1])) macro--; - if (macro == pos) return; - for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) { - if (wcsncmp(Macros[i].name, &buf[macro], pos - macro)) continue; - delete(false, macro, pos - macro); - pos = macro; - size_t expand = wcslen(Macros[i].string); - if (reserve(macro, expand)) { - wcsncpy(&buf[macro], Macros[i].string, expand); - pos += expand; - } +int editCopy(struct Edit *e, size_t index, size_t count) { + if (index + count > e->len) { + errno = EINVAL; + return -1; } + if (!e->cut) return 0; + e->cut->len = 0; + if (editReserve(e->cut, 0, count) < 0) return -1; + wmemcpy(e->cut->buf, &e->buf[index], count); + return 0; } -static struct { - size_t pos; - size_t pre; - size_t len; -} tab; - -static void tabComplete(uint id) { - if (!tab.len) { - tab.pos = pos; - while (tab.pos && !iswspace(buf[tab.pos - 1])) tab.pos--; - if (tab.pos == pos) return; - tab.pre = pos - tab.pos; - tab.len = tab.pre; - } - - 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); - mbs[n] = '\0'; - - const char *comp = complete(id, mbs); - if (!comp) comp = complete(id, mbs); - if (!comp) { - tab.len = 0; - return; +int editDelete(struct Edit *e, bool cut, size_t index, size_t count) { + if (index + count > e->len) { + errno = EINVAL; + return -1; } - - wchar_t wcs[Cap]; - n = mbstowcs(wcs, comp, Cap); - assert(n != (size_t)-1); - if (tab.pos + n + 2 > Cap) { - completeReject(); - tab.len = 0; - return; - } - - delete(false, tab.pos, tab.len); - tab.len = n; - if (wcs[0] == L'\\' || wcschr(wcs, L' ')) { - reserve(tab.pos, tab.len); - } else if (wcs[0] != L'/' && !tab.pos) { - tab.len += 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':') { - tab.len += 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++; - reserve(tab.pos, tab.len); - buf[tab.pos + n] = L' '; - } - memcpy(&buf[tab.pos], wcs, sizeof(*wcs) * n); - pos = tab.pos + tab.len; + if (cut && editCopy(e, index, count) < 0) return -1; + wmemmove(&e->buf[index], &e->buf[index + count], e->len - index - count); + e->len -= count; + if (e->pos > e->len) e->pos = e->len; + return 0; } -static void tabAccept(void) { - completeAccept(); - tab.len = 0; +static size_t prevSpacing(const struct Edit *e, size_t pos) { + if (!pos) return 0; + do { + pos--; + } while (pos && !wcwidth(e->buf[pos])); + return pos; } -static void tabReject(void) { - completeReject(); - tab.len = 0; +static size_t nextSpacing(const struct Edit *e, size_t pos) { + if (pos == e->len) return e->len; + do { + pos++; + } while (pos < e->len && !wcwidth(e->buf[pos])); + return pos; } -void edit(uint id, enum Edit op, wchar_t ch) { - size_t init = pos; - switch (op) { - break; case EditHead: pos = 0; - break; case EditTail: pos = len; - break; case EditPrev: if (pos) pos--; - break; case EditNext: if (pos < len) pos++; +int editFn(struct Edit *e, enum EditFn fn) { + int ret = 0; + switch (fn) { + break; case EditHead: e->pos = 0; + break; case EditTail: e->pos = e->len; + break; case EditPrev: e->pos = prevSpacing(e, e->pos); + break; case EditNext: e->pos = nextSpacing(e, e->pos); break; case EditPrevWord: { - if (pos) pos--; - while (pos && !iswspace(buf[pos - 1])) pos--; + while (e->pos && !isword(e->buf[e->pos-1])) e->pos--; + while (e->pos && isword(e->buf[e->pos-1])) e->pos--; } break; case EditNextWord: { - if (pos < len) pos++; - while (pos < len && !iswspace(buf[pos])) pos++; + while (e->pos < e->len && isword(e->buf[e->pos])) e->pos++; + while (e->pos < e->len && !isword(e->buf[e->pos])) e->pos++; } - break; case EditDeleteHead: delete(true, 0, pos); pos = 0; - break; case EditDeleteTail: delete(true, pos, len - pos); - break; case EditDeletePrev: if (pos) delete(false, --pos, 1); - break; case EditDeleteNext: delete(false, pos, 1); + break; case EditDeleteHead: { + ret = editDelete(e, true, 0, e->pos); + e->pos = 0; + } + break; case EditDeleteTail: { + ret = editDelete(e, true, e->pos, e->len - e->pos); + } + break; case EditDeletePrev: { + size_t prev = prevSpacing(e, e->pos); + editDelete(e, false, prev, e->pos - prev); + e->pos = prev; + } + break; case EditDeleteNext: { + editDelete(e, false, e->pos, nextSpacing(e, e->pos) - e->pos); + } break; case EditDeletePrevWord: { - if (!pos) break; - size_t word = pos - 1; - while (word && !iswspace(buf[word - 1])) word--; - delete(true, word, pos - word); - pos = word; + if (!e->pos) break; + size_t word = e->pos; + while (word && !isword(e->buf[word-1])) word--; + while (word && isword(e->buf[word-1])) word--; + ret = editDelete(e, true, word, e->pos - word); + e->pos = word; } break; case EditDeleteNextWord: { - if (pos == len) break; - size_t word = pos + 1; - while (word < len && !iswspace(buf[word])) word++; - delete(true, pos, word - pos); + if (e->pos == e->len) break; + size_t word = e->pos; + while (word < e->len && !isword(e->buf[word])) word++; + while (word < e->len && isword(e->buf[word])) word++; + ret = editDelete(e, true, e->pos, word - e->pos); } + break; case EditPaste: { - if (reserve(pos, cut.len)) { - memcpy(&buf[pos], cut.buf, sizeof(*buf) * cut.len); - pos += cut.len; + if (!e->cut) break; + ret = editReserve(e, e->pos, e->cut->len); + if (ret == 0) { + wmemcpy(&e->buf[e->pos], e->cut->buf, e->cut->len); + e->pos += e->cut->len; } } - break; case EditTranspose: { - if (!pos || len < 2) break; - if (pos == len) pos--; - wchar_t t = buf[pos - 1]; - buf[pos - 1] = buf[pos]; - buf[pos++] = t; + if (e->len < 2) break; + if (!e->pos) e->pos++; + if (e->pos == e->len) e->pos--; + wchar_t x = e->buf[e->pos-1]; + e->buf[e->pos-1] = e->buf[e->pos]; + e->buf[e->pos++] = x; } break; case EditCollapse: { size_t ws; - for (pos = 0; pos < len;) { - for (; pos < len && !iswspace(buf[pos]); ++pos); - for (ws = pos; ws < len && iswspace(buf[ws]); ++ws); - if (pos && ws < len) { - delete(false, pos, ws - pos - 1); - buf[pos++] = L' '; + for (e->pos = 0; e->pos < e->len;) { + for (; e->pos < e->len && !iswspace(e->buf[e->pos]); ++e->pos); + for (ws = e->pos; ws < e->len && iswspace(e->buf[ws]); ++ws); + if (e->pos && ws < e->len) { + editDelete(e, false, e->pos, ws - e->pos - 1); + e->buf[e->pos++] = L' '; } else { - delete(false, pos, ws - pos); + editDelete(e, false, e->pos, ws - e->pos); } } } - break; case EditInsert: { - if (reserve(pos, 1)) { - buf[pos++] = ch; - } - } - break; case EditComplete: { - tabComplete(id); - return; - } - break; case EditExpand: { - macroExpand(); - tabAccept(); - return; - } - break; case EditEnter: { - tabAccept(); - command(id, editBuffer(NULL)); - len = pos = 0; - return; - } + break; case EditClear: e->len = e->pos = 0; } + return ret; +} - if (pos < init) { - tabReject(); - } else { - tabAccept(); +int editInsert(struct Edit *e, wchar_t ch) { + char mb[MB_LEN_MAX]; + if (wctomb(mb, ch) < 0) return -1; + if (editReserve(e, e->pos, 1) < 0) return -1; + e->buf[e->pos++] = ch; + return 0; +} + +#ifdef TEST +#undef NDEBUG +#include <assert.h> +#include <string.h> + +static void fix(struct Edit *e, const char *str) { + assert(0 == editFn(e, EditClear)); + for (const char *ch = str; *ch; ++ch) { + assert(0 == editInsert(e, (wchar_t)*ch)); } } + +static bool eq(struct Edit *e, const char *str1) { + size_t pos; + static size_t cap; + static char *buf; + assert(NULL != editString(e, &buf, &cap, &pos)); + const char *str2 = &str1[strlen(str1) + 1]; + return pos == strlen(str1) + && !strncmp(buf, str1, pos) + && !strcmp(&buf[pos], str2); +} + +#define editFn(...) assert(0 == editFn(__VA_ARGS__)) + +int main(void) { + struct Edit cut = {0}; + struct Edit e = { .cut = &cut }; + + fix(&e, "foo bar"); + editFn(&e, EditHead); + assert(eq(&e, "\0foo bar")); + editFn(&e, EditTail); + assert(eq(&e, "foo bar\0")); + editFn(&e, EditPrev); + assert(eq(&e, "foo ba\0r")); + editFn(&e, EditNext); + assert(eq(&e, "foo bar\0")); + + fix(&e, "foo, bar"); + editFn(&e, EditPrevWord); + assert(eq(&e, "foo, \0bar")); + editFn(&e, EditPrevWord); + assert(eq(&e, "\0foo, bar")); + editFn(&e, EditNextWord); + assert(eq(&e, "foo, \0bar")); + editFn(&e, EditNextWord); + assert(eq(&e, "foo, bar\0")); + + fix(&e, "foo bar"); + editFn(&e, EditPrevWord); + editFn(&e, EditDeleteHead); + assert(eq(&e, "\0bar")); + + fix(&e, "foo bar"); + editFn(&e, EditPrevWord); + editFn(&e, EditDeleteTail); + assert(eq(&e, "foo \0")); + + fix(&e, "foo bar"); + editFn(&e, EditDeletePrev); + assert(eq(&e, "foo ba\0")); + editFn(&e, EditHead); + editFn(&e, EditDeleteNext); + assert(eq(&e, "\0oo ba")); + + fix(&e, "foo, bar"); + editFn(&e, EditDeletePrevWord); + assert(eq(&e, "foo, \0")); + editFn(&e, EditDeletePrevWord); + assert(eq(&e, "\0")); + + fix(&e, "foo, bar"); + editFn(&e, EditHead); + editFn(&e, EditDeleteNextWord); + assert(eq(&e, "\0, bar")); + editFn(&e, EditDeleteNextWord); + assert(eq(&e, "\0")); + + fix(&e, "foo bar"); + editFn(&e, EditDeletePrevWord); + editFn(&e, EditPaste); + assert(eq(&e, "foo bar\0")); + editFn(&e, EditPaste); + assert(eq(&e, "foo barbar\0")); + + fix(&e, "bar"); + editFn(&e, EditTranspose); + assert(eq(&e, "bra\0")); + editFn(&e, EditHead); + editFn(&e, EditTranspose); + assert(eq(&e, "rb\0a")); + editFn(&e, EditTranspose); + assert(eq(&e, "rab\0")); + + fix(&e, " foo bar "); + editFn(&e, EditCollapse); + assert(eq(&e, "foo bar\0")); +} + +#endif /* TEST */ diff --git a/edit.h b/edit.h new file mode 100644 index 0000000..41966b8 --- /dev/null +++ b/edit.h @@ -0,0 +1,74 @@ +/* Copyright (C) 2022 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <stdbool.h> +#include <stddef.h> + +struct Edit { + wchar_t *buf; + size_t pos; + size_t len; + size_t cap; + struct Edit *cut; +}; + +enum EditFn { + EditHead, + EditTail, + EditPrev, + EditNext, + EditPrevWord, + EditNextWord, + EditDeleteHead, + EditDeleteTail, + EditDeletePrev, + EditDeleteNext, + EditDeletePrevWord, + EditDeleteNextWord, + EditPaste, + EditTranspose, + EditCollapse, + EditClear, +}; + +// Perform an editing function. +int editFn(struct Edit *e, enum EditFn fn); + +// Insert a character at the cursor. +int editInsert(struct Edit *e, wchar_t ch); + +// Convert the buffer to a multi-byte string. +char *editString(const struct Edit *e, char **buf, size_t *cap, size_t *pos); + +// Reserve a range in the buffer. +int editReserve(struct Edit *e, size_t index, size_t count); + +// Copy a range of the buffer into e->cut. +int editCopy(struct Edit *e, size_t index, size_t count); + +// Delete a range from the buffer. If cut is true, copy the deleted portion. +int editDelete(struct Edit *e, bool cut, size_t index, size_t count); diff --git a/filter.c b/filter.c new file mode 100644 index 0000000..a7f9a29 --- /dev/null +++ b/filter.c @@ -0,0 +1,132 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <err.h> +#include <fnmatch.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> + +#include "chat.h" + +struct Filter filters[FilterCap]; +static size_t len; + +struct Filter filterParse(enum Heat heat, char *pattern) { + struct Filter filter = { .heat = heat }; + filter.mask = strsep(&pattern, " "); + filter.cmd = strsep(&pattern, " "); + filter.chan = strsep(&pattern, " "); + filter.mesg = pattern; + return filter; +} + +struct Filter filterAdd(enum Heat heat, const char *pattern) { + if (len == FilterCap) errx(EX_CONFIG, "filter limit exceeded"); + char *own; + if (!strchr(pattern, '!') && !strchr(pattern, ' ')) { + int n = asprintf(&own, "%s!*@*", pattern); + if (n < 0) err(EX_OSERR, "asprintf"); + } else { + own = strdup(pattern); + if (!own) err(EX_OSERR, "strdup"); + } + struct Filter filter = filterParse(heat, own); + filters[len++] = filter; + return filter; +} + +bool filterRemove(struct Filter filter) { + bool found = false; + for (size_t i = len - 1; i < len; --i) { + if (filters[i].heat != filter.heat) continue; + if (!filters[i].cmd != !filter.cmd) continue; + if (!filters[i].chan != !filter.chan) continue; + if (!filters[i].mesg != !filter.mesg) continue; + if (strcasecmp(filters[i].mask, filter.mask)) continue; + if (filter.cmd && strcasecmp(filters[i].cmd, filter.cmd)) continue; + if (filter.chan && strcasecmp(filters[i].chan, filter.chan)) continue; + if (filter.mesg && strcasecmp(filters[i].mesg, filter.mesg)) continue; + free(filters[i].mask); + memmove(&filters[i], &filters[i + 1], sizeof(*filters) * --len); + filters[len] = (struct Filter) {0}; + found = true; + } + return found; +} + +static bool filterTest( + struct Filter filter, const char *mask, uint id, const struct Message *msg +) { + if (fnmatch(filter.mask, mask, FNM_CASEFOLD)) return false; + if (!filter.cmd) return true; + if (fnmatch(filter.cmd, msg->cmd, FNM_CASEFOLD)) return false; + if (!filter.chan) return true; + if (fnmatch(filter.chan, idNames[id], FNM_CASEFOLD)) return false; + if (!filter.mesg) return true; + if (!msg->params[1]) return false; + return !fnmatch(filter.mesg, msg->params[1], FNM_CASEFOLD); +} + +enum { IcedCap = 8 }; +static struct { + size_t len; + char *msgIDs[IcedCap]; +} iced; + +static void icedPush(const char *msgID) { + if (!msgID) return; + size_t i = iced.len % IcedCap; + free(iced.msgIDs[i]); + iced.msgIDs[i] = strdup(msgID); + if (!iced.msgIDs[i]) err(EX_OSERR, "strdup"); + iced.len++; +} + +enum Heat filterCheck(enum Heat heat, uint id, const struct Message *msg) { + if (!len) return heat; + + if (msg->tags[TagReply]) { + for (size_t i = 0; i < IcedCap; ++i) { + if (!iced.msgIDs[i]) continue; + if (strcmp(msg->tags[TagReply], iced.msgIDs[i])) continue; + icedPush(msg->tags[TagMsgID]); + return Ice; + } + } + + char mask[512]; + snprintf(mask, sizeof(mask), "%s!%s@%s", msg->nick, msg->user, msg->host); + for (size_t i = 0; i < len; ++i) { + if (!filterTest(filters[i], mask, id, msg)) continue; + if (filters[i].heat == Ice) icedPush(msg->tags[TagMsgID]); + return filters[i].heat; + } + return heat; +} diff --git a/handle.c b/handle.c index fcc0c5d..5a2cf7c 100644 --- a/handle.c +++ b/handle.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,10 +33,11 @@ #include <stdlib.h> #include <string.h> #include <sysexits.h> +#include <wchar.h> #include "chat.h" -struct Replies replies; +uint replies[ReplyCap]; static const char *CapNames[] = { #define X(name, id) [id##Bit] = name, @@ -62,21 +63,21 @@ static enum Cap capParse(const char *list) { return caps; } -static const char *capList(enum Cap caps) { - static char buf[1024]; - buf[0] = '\0'; - struct Cat cat = { buf, sizeof(buf), 0 }; +static void capList(char *buf, size_t cap, enum Cap caps) { + *buf = '\0'; + char *ptr = buf, *end = &buf[cap]; for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { if (caps & (1 << i)) { - catf(&cat, "%s%s", (buf[0] ? " " : ""), CapNames[i]); + ptr = seprintf( + ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i] + ); } } - return buf; } static void require(struct Message *msg, bool origin, uint len) { if (origin) { - if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + if (!msg->nick) msg->nick = "*.*"; if (!msg->user) msg->user = msg->nick; if (!msg->host) msg->host = msg->user; } @@ -90,13 +91,25 @@ static const time_t *tagTime(const struct Message *msg) { static time_t time; struct tm tm; if (!msg->tags[TagTime]) return NULL; - if (!strptime(msg->tags[TagTime], "%FT%T", &tm)) return NULL; + if (!strptime(msg->tags[TagTime], "%Y-%m-%dT%T", &tm)) return NULL; time = timegm(&tm); return &time; } typedef void Handler(struct Message *msg); +static void handleStandardReply(struct Message *msg) { + require(msg, false, 3); + for (uint i = 2; i < ParamCap - 1; ++i) { + if (msg->params[i + 1]) continue; + uiFormat( + Network, Warm, tagTime(msg), + "%s", msg->params[i] + ); + break; + } +} + static void handleErrorGeneric(struct Message *msg) { require(msg, false, 2); if (msg->params[2]) { @@ -114,10 +127,33 @@ static void handleErrorGeneric(struct Message *msg) { } } +static void handleReplyGeneric(struct Message *msg) { + uint first = 1; + uint id = Network; + if (msg->params[1] && strchr(network.chanTypes, msg->params[1][0])) { + id = idFor(msg->params[1]); + first++; + } + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf(ptr, end, "\3%d(%s)\3\t", Gray, msg->cmd); + for (uint i = first; i < ParamCap && msg->params[i]; ++i) { + ptr = seprintf( + ptr, end, "%s%s", (i > first ? " " : ""), msg->params[i] + ); + } + uiWrite(id, Ice, tagTime(msg), buf); +} + static void handleErrorNicknameInUse(struct Message *msg) { require(msg, false, 2); if (!strcmp(self.nick, "*")) { - ircFormat("NICK :%s_\r\n", msg->params[1]); + static uint i = 1; + if (i < ARRAY_LEN(self.nicks) && self.nicks[i]) { + ircFormat("NICK %s\r\n", self.nicks[i++]); + } else { + ircFormat("NICK %s_\r\n", msg->params[1]); + } } else { handleErrorGeneric(msg); } @@ -142,14 +178,18 @@ static void handleCap(struct Message *msg) { caps &= ~CapConsumer; } if (caps) { - ircFormat("CAP REQ :%s\r\n", capList(caps)); + char buf[512]; + capList(buf, sizeof(buf), caps); + ircFormat("CAP REQ :%s\r\n", buf); } else { if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); } } else if (!strcmp(msg->params[1], "ACK")) { self.caps |= caps; if (caps & CapSASL) { - ircFormat("AUTHENTICATE %s\r\n", (self.plain ? "PLAIN" : "EXTERNAL")); + ircFormat( + "AUTHENTICATE %s\r\n", (self.plainUser ? "PLAIN" : "EXTERNAL") + ); } if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); } else if (!strcmp(msg->params[1], "NAK")) { @@ -188,33 +228,34 @@ static void base64(char *dst, const byte *src, size_t len) { static void handleAuthenticate(struct Message *msg) { (void)msg; - if (!self.plain) { + if (!self.plainUser) { 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]); - } + byte buf[299] = {0}; + size_t userLen = strlen(self.plainUser); + size_t passLen = strlen(self.plainPass); + size_t len = 1 + userLen + 1 + passLen; + if (sizeof(buf) < len) errx(EX_USAGE, "SASL PLAIN is too long"); + memcpy(&buf[1], self.plainUser, userLen); + memcpy(&buf[1 + userLen + 1], self.plainPass, passLen); char b64[BASE64_SIZE(sizeof(buf))]; base64(b64, buf, len); ircFormat("AUTHENTICATE "); - ircSend(b64, BASE64_SIZE(len)); + ircSend(b64, BASE64_SIZE(len) - 1); ircFormat("\r\n"); explicit_bzero(b64, sizeof(b64)); explicit_bzero(buf, sizeof(buf)); - explicit_bzero(self.plain, strlen(self.plain)); + explicit_bzero(self.plainPass, strlen(self.plainPass)); } static void handleReplyLoggedIn(struct Message *msg) { (void)msg; ircFormat("CAP END\r\n"); + handleReplyGeneric(msg); } static void handleErrorSASLFail(struct Message *msg) { @@ -225,30 +266,38 @@ static void handleErrorSASLFail(struct Message *msg) { static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); - completeTouch(Network, self.nick, Default); + completePull(Network, self.nick, Default); + if (self.mode) ircFormat("MODE %s %s\r\n", self.nick, self.mode); if (self.join) { uint count = 1; for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { if (*ch == ',') count++; } ircFormat("JOIN %s\r\n", self.join); - replies.join += count; - replies.topic += count; - replies.names += count; + if (count == 1) replies[ReplyJoin]++; + replies[ReplyTopicAuto] += count; + replies[ReplyNamesAuto] += count; } + commandCompletion(); + handleReplyGeneric(msg); } static void handleReplyISupport(struct Message *msg) { + handleReplyGeneric(msg); for (uint i = 1; i < ParamCap; ++i) { if (!msg->params[i]) break; char *key = strsep(&msg->params[i], "="); if (!strcmp(key, "NETWORK")) { if (!msg->params[i]) continue; set(&network.name, msg->params[i]); - uiFormat( - Network, Cold, tagTime(msg), - "You arrive in %s", msg->params[i] - ); + static bool arrived; + if (!arrived) { + uiFormat( + Network, Cold, tagTime(msg), + "You arrive in %s", msg->params[i] + ); + arrived = true; + } } else if (!strcmp(key, "USERLEN")) { if (!msg->params[i]) continue; network.userLen = strtoul(msg->params[i], NULL, 10); @@ -258,6 +307,9 @@ static void handleReplyISupport(struct Message *msg) { } else if (!strcmp(key, "CHANTYPES")) { if (!msg->params[i]) continue; set(&network.chanTypes, msg->params[i]); + } else if (!strcmp(key, "STATUSMSG")) { + if (!msg->params[i]) continue; + set(&network.statusmsg, msg->params[i]); } else if (!strcmp(key, "PREFIX")) { strsep(&msg->params[i], "("); char *modes = strsep(&msg->params[i], ")"); @@ -290,7 +342,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); + urlScan(Network, NULL, line); if (!strncmp(line, "- ", 2)) { uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); } else { @@ -302,6 +354,12 @@ static void handleErrorNoMOTD(struct Message *msg) { (void)msg; } +static void handleReplyHelp(struct Message *msg) { + require(msg, false, 3); + urlScan(Network, NULL, msg->params[2]); + uiWrite(Network, Warm, tagTime(msg), msg->params[2]); +} + static void handleJoin(struct Message *msg) { require(msg, true, 1); uint id = idFor(msg->params[0]); @@ -314,23 +372,23 @@ static void handleJoin(struct Message *msg) { set(&self.host, msg->host); } idColors[id] = hash(msg->params[0]); - completeTouch(None, msg->params[0], idColors[id]); - if (replies.join) { - uiShowID(id); - replies.join--; + completePull(None, msg->params[0], idColors[id]); + if (replies[ReplyJoin]) { + windowShow(windowFor(id)); + replies[ReplyJoin]--; } } - completeTouch(id, msg->nick, hash(msg->user)); + completePull(id, msg->nick, hash(msg->user)); if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { msg->params[2] = NULL; } uiFormat( - id, ignoreCheck(Cold, id, msg), tagTime(msg), + id, filterCheck(Cold, id, msg), tagTime(msg), "\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] ? "\17) " : ""), hash(msg->params[0]), msg->params[0] ); logFormat(id, tagTime(msg), "%s arrives in %s", msg->nick, msg->params[0]); @@ -352,10 +410,10 @@ static void handlePart(struct Message *msg) { require(msg, true, 1); uint id = idFor(msg->params[0]); if (!strcmp(msg->nick, self.nick)) { - completeClear(id); + completeRemove(id, NULL); } completeRemove(id, msg->nick); - enum Heat heat = ignoreCheck(Cold, id, msg); + enum Heat heat = filterCheck(Cold, id, msg); if (heat > Ice) urlScan(id, msg->nick, msg->params[1]); uiFormat( id, heat, tagTime(msg), @@ -374,7 +432,7 @@ static void handleKick(struct Message *msg) { require(msg, true, 2); uint id = idFor(msg->params[0]); bool kicked = !strcmp(msg->params[1], self.nick); - completeTouch(id, msg->nick, hash(msg->user)); + completePull(id, msg->nick, hash(msg->user)); urlScan(id, msg->nick, msg->params[2]); uiFormat( id, (kicked ? Hot : Cold), tagTime(msg), @@ -391,21 +449,22 @@ static void handleKick(struct Message *msg) { (msg->params[2] ? ": " : ""), (msg->params[2] ?: "") ); completeRemove(id, msg->params[1]); - if (kicked) completeClear(id); + if (kicked) completeRemove(id, NULL); } static void handleNick(struct Message *msg) { require(msg, true, 1); if (!strcmp(msg->nick, self.nick)) { set(&self.nick, msg->params[0]); - uiRead(); // Update prompt. + inputUpdate(); } - for (uint id; (id = completeID(msg->nick));) { + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { if (!strcmp(idNames[id], msg->nick)) { set(&idNames[id], msg->params[0]); } uiFormat( - id, ignoreCheck(Cold, id, msg), tagTime(msg), + id, filterCheck(Cold, id, msg), 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] ); @@ -415,13 +474,27 @@ static void handleNick(struct Message *msg) { msg->nick, msg->params[0] ); } - completeReplace(None, msg->nick, msg->params[0]); + completeReplace(msg->nick, msg->params[0]); +} + +static void handleSetname(struct Message *msg) { + require(msg, true, 1); + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { + uiFormat( + id, filterCheck(Cold, id, msg), tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3 (%s\17)", + hash(msg->user), msg->nick, hash(msg->user), msg->nick, + msg->params[0] + ); + } } static void handleQuit(struct Message *msg) { require(msg, true, 0); - for (uint id; (id = completeID(msg->nick));) { - enum Heat heat = ignoreCheck(Cold, id, msg); + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { + enum Heat heat = filterCheck(Cold, id, msg); if (heat > Ice) urlScan(id, msg->nick, msg->params[0]); uiFormat( id, heat, tagTime(msg), @@ -442,8 +515,9 @@ static void handleQuit(struct Message *msg) { static void handleInvite(struct Message *msg) { require(msg, true, 2); if (!strcmp(msg->params[0], self.nick)) { + set(&self.invited, msg->params[1]); uiFormat( - Network, ignoreCheck(Hot, Network, msg), tagTime(msg), + Network, filterCheck(Hot, Network, msg), tagTime(msg), "\3%02d%s\3\tinvites you to \3%02d%s\3", hash(msg->user), msg->nick, hash(msg->params[1]), msg->params[1] ); @@ -465,7 +539,6 @@ static void handleInvite(struct Message *msg) { static void handleReplyInviting(struct Message *msg) { require(msg, false, 3); - if (self.caps & CapInviteNotify) return; struct Message invite = { .nick = self.nick, .user = self.user, @@ -477,10 +550,10 @@ static void handleReplyInviting(struct Message *msg) { } static void handleErrorUserOnChannel(struct Message *msg) { - require(msg, false, 4); + require(msg, false, 3); uint id = idFor(msg->params[2]); uiFormat( - id, Cold, tagTime(msg), + id, Warm, tagTime(msg), "\3%02d%s\3 is already in \3%02d%s\3", completeColor(id, msg->params[1]), msg->params[1], hash(msg->params[2]), msg->params[2] @@ -490,21 +563,28 @@ static void handleErrorUserOnChannel(struct Message *msg) { static void handleReplyNames(struct Message *msg) { require(msg, false, 4); uint id = idFor(msg->params[2]); - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; while (msg->params[3]) { char *name = strsep(&msg->params[3], " "); char *prefixes = strsep(&name, "!"); char *nick = &prefixes[strspn(prefixes, network.prefixes)]; char *user = strsep(&name, "@"); enum Color color = (user ? hash(user) : Default); - completeAdd(id, nick, color); - if (!replies.names) continue; - catf(&cat, "%s\3%02d%s\3", (buf[0] ? ", " : ""), color, prefixes); + uint bits = 0; + for (char *p = prefixes; p < nick; ++p) { + bits |= prefixBit(*p); + } + completePush(id, nick, color); + *completeBits(id, nick) = bits; + if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue; + ptr = seprintf( + ptr, end, "%s\3%02d%s\3", (ptr > buf ? ", " : ""), color, prefixes + ); } - if (!replies.names) return; + if (ptr == buf) return; uiFormat( - id, Cold, tagTime(msg), + id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg), "In \3%02d%s\3 are %s", hash(msg->params[2]), msg->params[2], buf ); @@ -512,15 +592,17 @@ static void handleReplyNames(struct Message *msg) { static void handleReplyEndOfNames(struct Message *msg) { (void)msg; - if (replies.names) replies.names--; + if (replies[ReplyNamesAuto]) { + replies[ReplyNamesAuto]--; + } else if (replies[ReplyNames]) { + replies[ReplyNames]--; + } } static void handleReplyNoTopic(struct Message *msg) { require(msg, false, 2); - if (!replies.topic) return; - replies.topic--; uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + idFor(msg->params[1]), Warm, tagTime(msg), "There is no sign in \3%02d%s\3", hash(msg->params[1]), msg->params[1] ); @@ -528,14 +610,15 @@ static void handleReplyNoTopic(struct Message *msg) { static void topicComplete(uint id, const char *topic) { char buf[512]; - const char *prev = complete(id, "/topic "); + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); if (prev) { snprintf(buf, sizeof(buf), "%s", prev); completeRemove(id, buf); } if (topic) { snprintf(buf, sizeof(buf), "/topic %s", topic); - completeAdd(id, buf, Default); + completePush(id, buf, Default); } } @@ -543,11 +626,10 @@ static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); uint id = idFor(msg->params[1]); topicComplete(id, msg->params[2]); - if (!replies.topic) return; - replies.topic--; + if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return; urlScan(id, NULL, msg->params[2]); uiFormat( - id, Cold, tagTime(msg), + id, (replies[ReplyTopicAuto] ? Cold : Warm), tagTime(msg), "The sign in \3%02d%s\3 reads: %s", hash(msg->params[1]), msg->params[1], msg->params[2] ); @@ -555,25 +637,45 @@ static void handleReplyTopic(struct Message *msg) { id, tagTime(msg), "The sign in %s reads: %s", msg->params[1], msg->params[2] ); + if (replies[ReplyTopicAuto]) { + replies[ReplyTopicAuto]--; + } else { + replies[ReplyTopic]--; + } +} + +static void swap(wchar_t *a, wchar_t *b) { + wchar_t x = *a; + *a = *b; + *b = x; +} + +static char *highlightMiddle( + char *ptr, char *end, enum Color color, + wchar_t *str, size_t pre, size_t suf +) { + wchar_t nul = L'\0'; + swap(&str[pre], &nul); + ptr = seprintf(ptr, end, "%ls", str); + swap(&str[pre], &nul); + swap(&str[suf], &nul); + if (hashBound) { + ptr = seprintf( + ptr, end, "\3%02d,%02d%ls\3%02d,%02d", + Default, color, &str[pre], Default, Default + ); + } else { + ptr = seprintf(ptr, end, "\26%ls\26", &str[pre]); + } + swap(&str[suf], &nul); + ptr = seprintf(ptr, end, "%ls", &str[suf]); + return ptr; } static void handleTopic(struct Message *msg) { require(msg, true, 2); uint id = idFor(msg->params[0]); - if (msg->params[1][0]) { - topicComplete(id, msg->params[1]); - urlScan(id, msg->nick, msg->params[1]); - uiFormat( - 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] - ); - logFormat( - id, tagTime(msg), "%s places a new sign in %s: %s", - msg->nick, msg->params[0], msg->params[1] - ); - } else { + if (!msg->params[1][0]) { topicComplete(id, NULL); uiFormat( id, Warm, tagTime(msg), @@ -584,7 +686,62 @@ static void handleTopic(struct Message *msg) { id, tagTime(msg), "%s removes the sign in %s", msg->nick, msg->params[0] ); + return; } + + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); + if (prev) { + prev += 7; + } else { + goto plain; + } + + wchar_t old[512]; + wchar_t new[512]; + if (swprintf(old, ARRAY_LEN(old), L"%s", prev) < 0) goto plain; + if (swprintf(new, ARRAY_LEN(new), L"%s", msg->params[1]) < 0) goto plain; + + size_t pre; + for (pre = 0; old[pre] && new[pre] && old[pre] == new[pre]; ++pre); + size_t osuf = wcslen(old); + size_t nsuf = wcslen(new); + while (osuf > pre && nsuf > pre && old[osuf-1] == new[nsuf-1]) { + osuf--; + nsuf--; + } + + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf( + ptr, end, "\3%02d%s\3\ttakes down the sign in \3%02d%s\3: ", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + ptr = highlightMiddle(ptr, end, Brown, old, pre, osuf); + if (osuf != pre) uiWrite(id, Cold, tagTime(msg), buf); + ptr = buf; + ptr = seprintf( + ptr, end, "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: ", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + ptr = highlightMiddle(ptr, end, Green, new, pre, nsuf); + uiWrite(id, Warm, tagTime(msg), buf); + goto log; + +plain: + uiFormat( + 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] + ); +log: + logFormat( + id, tagTime(msg), "%s places a new sign in %s: %s", + msg->nick, msg->params[0], msg->params[1] + ); + topicComplete(id, msg->params[1]); + urlScan(id, msg->nick, msg->params[1]); } static const char *UserModes[256] = { @@ -597,20 +754,19 @@ static const char *UserModes[256] = { static void handleReplyUserModeIs(struct Message *msg) { require(msg, false, 2); - if (!replies.mode) return; - replies.mode--; - - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; for (char *ch = msg->params[1]; *ch; ++ch) { if (*ch == '+') continue; const char *name = UserModes[(byte)*ch]; - catf(&cat, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: "")); + ptr = seprintf( + ptr, end, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: "") + ); } uiFormat( Network, Warm, tagTime(msg), "\3%02d%s\3\tis %s", - self.color, self.nick, (buf[0] ? &buf[2] : "modeless") + self.color, self.nick, (ptr > buf ? &buf[2] : "modeless") ); } @@ -631,12 +787,9 @@ static const char *ChanModes[256] = { static void handleReplyChannelModeIs(struct Message *msg) { require(msg, false, 3); - if (!replies.mode) return; - replies.mode--; - uint param = 3; - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; for (char *ch = msg->params[2]; *ch; ++ch) { if (*ch == '+') continue; const char *name = ChanModes[(byte)*ch]; @@ -645,23 +798,23 @@ static void handleReplyChannelModeIs(struct Message *msg) { strchr(network.setParamModes, *ch) ) { assert(param < ParamCap); - catf( - &cat, ", +%c%s%s %s", + ptr = seprintf( + ptr, end, ", +%c%s%s %s", *ch, (name ? " " : ""), (name ?: ""), msg->params[param++] ); } else { - catf( - &cat, ", +%c%s%s", + ptr = seprintf( + ptr, end, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: "") ); } } uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + idFor(msg->params[1]), Warm, tagTime(msg), "\3%02d%s\3\tis %s", hash(msg->params[1]), msg->params[1], - (buf[0] ? &buf[2] : "modeless") + (ptr > buf ? &buf[2] : "modeless") ); } @@ -710,6 +863,12 @@ static void handleMode(struct Message *msg) { char prefix = network.prefixes[ strchr(network.prefixModes, *ch) - network.prefixModes ]; + completePush(id, nick, Default); + if (set) { + *completeBits(id, nick) |= prefixBit(prefix); + } else { + *completeBits(id, nick) &= ~prefixBit(prefix); + } uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\t%s \3%02d%c%s\3 %s%s in \3%02d%s\3", @@ -819,7 +978,7 @@ static void handleMode(struct Message *msg) { static void handleErrorChanopPrivsNeeded(struct Message *msg) { require(msg, false, 3); uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + idFor(msg->params[1]), Warm, tagTime(msg), "%s", msg->params[2] ); } @@ -827,7 +986,7 @@ static void handleErrorChanopPrivsNeeded(struct Message *msg) { static void handleErrorUserNotInChannel(struct Message *msg) { require(msg, false, 4); uiFormat( - idFor(msg->params[2]), Cold, tagTime(msg), + idFor(msg->params[2]), Warm, tagTime(msg), "%s\tis not in \3%02d%s\3", msg->params[1], hash(msg->params[2]), msg->params[2] ); @@ -836,21 +995,20 @@ static void handleErrorUserNotInChannel(struct Message *msg) { static void handleErrorBanListFull(struct Message *msg) { require(msg, false, 4); uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + idFor(msg->params[1]), Warm, tagTime(msg), "%s", (msg->params[4] ?: msg->params[3]) ); } static void handleReplyBanList(struct Message *msg) { require(msg, false, 3); - if (!replies.ban) return; uint id = idFor(msg->params[1]); if (msg->params[3] && msg->params[4]) { char since[sizeof("0000-00-00 00:00:00")]; time_t time = strtol(msg->params[4], NULL, 10); strftime(since, sizeof(since), "%F %T", localtime(&time)); uiFormat( - id, Cold, tagTime(msg), + id, Warm, tagTime(msg), "Banned from \3%02d%s\3 since %s by \3%02d%s\3: %s", hash(msg->params[1]), msg->params[1], since, completeColor(id, msg->params[3]), msg->params[3], @@ -858,26 +1016,22 @@ static void handleReplyBanList(struct Message *msg) { ); } else { uiFormat( - id, Cold, tagTime(msg), + id, Warm, tagTime(msg), "Banned from \3%02d%s\3: %s", hash(msg->params[1]), msg->params[1], msg->params[2] ); } } -static void handleReplyEndOfBanList(struct Message *msg) { - (void)msg; - if (replies.ban) replies.ban--; -} - static void onList(const char *list, struct Message *msg) { + require(msg, false, 3); uint id = idFor(msg->params[1]); if (msg->params[3] && msg->params[4]) { char since[sizeof("0000-00-00 00:00:00")]; time_t time = strtol(msg->params[4], NULL, 10); strftime(since, sizeof(since), "%F %T", localtime(&time)); uiFormat( - id, Cold, tagTime(msg), + id, Warm, tagTime(msg), "On the \3%02d%s\3 %s list since %s by \3%02d%s\3: %s", hash(msg->params[1]), msg->params[1], list, since, completeColor(id, msg->params[3]), msg->params[3], @@ -885,7 +1039,7 @@ static void onList(const char *list, struct Message *msg) { ); } else { uiFormat( - id, Cold, tagTime(msg), + id, Warm, tagTime(msg), "On the \3%02d%s\3 %s list: %s", hash(msg->params[1]), msg->params[1], list, msg->params[2] ); @@ -893,71 +1047,48 @@ static void onList(const char *list, struct Message *msg) { } static void handleReplyExceptList(struct Message *msg) { - require(msg, false, 3); - if (!replies.excepts) return; onList("except", msg); } -static void handleReplyEndOfExceptList(struct Message *msg) { - (void)msg; - if (replies.excepts) replies.excepts--; -} - static void handleReplyInviteList(struct Message *msg) { - require(msg, false, 3); - if (!replies.invex) return; onList("invite", msg); } -static void handleReplyEndOfInviteList(struct Message *msg) { - (void)msg; - if (replies.invex) replies.invex--; -} - static void handleReplyList(struct Message *msg) { - require(msg, false, 4); - if (!replies.list) return; + require(msg, false, 3); uiFormat( Network, Warm, tagTime(msg), "In \3%02d%s\3 are %ld under the banner: %s", hash(msg->params[1]), msg->params[1], strtol(msg->params[2], NULL, 10), - msg->params[3] + (msg->params[3] ?: "") ); } -static void handleReplyListEnd(struct Message *msg) { - (void)msg; - if (!replies.list) return; - replies.list--; -} - static void handleReplyWhoisUser(struct Message *msg) { require(msg, false, 6); - if (!replies.whois) return; - completeTouch(Network, msg->params[1], hash(msg->params[2])); + completePull(Network, msg->params[1], hash(msg->params[2])); uiFormat( Network, Warm, tagTime(msg), - "\3%02d%s\3\tis %s!%s@%s (%s)", + "\3%02d%s\3\tis %s!%s@%s (%s\17)", 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) { + if (!replies[ReplyWhois] && !replies[ReplyWhowas]) return; require(msg, false, 4); - if (!replies.whois) return; uiFormat( Network, Warm, tagTime(msg), - "\3%02d%s\3\tis connected to %s (%s)", + "\3%02d%s\3\t%s connected to %s (%s)", completeColor(Network, msg->params[1]), msg->params[1], - msg->params[2], msg->params[3] + (replies[ReplyWhowas] ? "was" : "is"), 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) { @@ -983,13 +1114,16 @@ static void handleReplyWhoisIdle(struct Message *msg) { static void handleReplyWhoisChannels(struct Message *msg) { require(msg, false, 3); - if (!replies.whois) return; - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; while (msg->params[2]) { char *channel = strsep(&msg->params[2], " "); + if (!channel[0]) break; char *name = &channel[strspn(channel, network.prefixes)]; - catf(&cat, "%s\3%02d%s\3", (buf[0] ? ", " : ""), hash(name), channel); + ptr = seprintf( + ptr, end, "%s\3%02d%s\3", + (ptr > buf ? ", " : ""), hash(name), channel + ); } uiFormat( Network, Warm, tagTime(msg), @@ -1000,7 +1134,6 @@ static void handleReplyWhoisChannels(struct Message *msg) { 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]; @@ -1016,24 +1149,35 @@ static void handleReplyWhoisGeneric(struct Message *msg) { static void handleReplyEndOfWhois(struct Message *msg) { require(msg, false, 2); - if (!replies.whois) return; if (strcmp(msg->params[1], self.nick)) { completeRemove(Network, msg->params[1]); } - replies.whois--; +} + +static void handleReplyWhowasUser(struct Message *msg) { + require(msg, false, 6); + completePull(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\twas %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 handleReplyEndOfWhowas(struct Message *msg) { + require(msg, false, 2); + if (strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } } static void handleReplyAway(struct Message *msg) { require(msg, false, 3); // Might be part of a WHOIS response. - uint id; - if (completeColor(Network, msg->params[1]) != Default) { - id = Network; - } else { - id = idFor(msg->params[1]); - } + uint id = (replies[ReplyWhois] ? Network : idFor(msg->params[1])); uiFormat( - id, Warm, tagTime(msg), + id, (id == Network ? Warm : Cold), tagTime(msg), "\3%02d%s\3\tis away: %s", completeColor(id, msg->params[1]), msg->params[1], msg->params[2] ); @@ -1045,24 +1189,30 @@ static void handleReplyAway(struct Message *msg) { static void handleReplyNowAway(struct Message *msg) { require(msg, false, 2); - if (!replies.away) return; uiFormat(Network, Warm, tagTime(msg), "%s", msg->params[1]); - replies.away--; } static bool isAction(struct Message *msg) { - if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; - msg->params[1] += 8; + if (strncmp(msg->params[1], "\1ACTION", 7)) return false; + if (msg->params[1][7] == ' ') { + msg->params[1] += 8; + } else if (msg->params[1][7] == '\1') { + msg->params[1] += 7; + } else { + return false; + } size_t len = strlen(msg->params[1]); - if (msg->params[1][len - 1] == '\1') msg->params[1][len - 1] = '\0'; + if (msg->params[1][len - 1] == '\1') { + msg->params[1][len - 1] = '\0'; + } return true; } -static bool isMention(const struct Message *msg) { - 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] : ' '); +static bool matchWord(const char *str, const char *word) { + size_t len = strlen(word); + const char *match = str; + while (NULL != (match = strstr(match, word))) { + char a = (match > str ? match[-1] : ' '); char b = (match[len] ?: ' '); if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { return true; @@ -1072,47 +1222,54 @@ static bool isMention(const struct Message *msg) { return false; } -static const char *colorMentions(uint id, struct Message *msg) { - char *split = strstr(msg->params[1], ": "); +static bool isMention(const struct Message *msg) { + if (matchWord(msg->params[1], self.nick)) return true; + for (uint i = 0; i < ARRAY_LEN(self.nicks) && self.nicks[i]; ++i) { + if (matchWord(msg->params[1], self.nicks[i])) return true; + } + return false; +} + +static char *colorMentions(char *ptr, char *end, uint id, const char *msg) { + // Consider words before a colon, or only the first two. + const char *split = strstr(msg, ": "); if (!split) { - split = strchr(msg->params[1], ' '); + split = strchr(msg, ' '); if (split) split = strchr(&split[1], ' '); } - if (!split) split = &msg->params[1][strlen(msg->params[1])]; - for (char *ch = msg->params[1]; ch < split; ++ch) { - if (iscntrl(*ch)) return ""; + if (!split) split = &msg[strlen(msg)]; + // Bail if there is existing formatting. + for (const char *ch = msg; ch < split; ++ch) { + if (iscntrl(*ch)) goto rest; } - char delimit = *split; - char *mention = msg->params[1]; - msg->params[1] = (delimit ? &split[1] : split); - *split = '\0'; - - static char buf[1024]; - buf[0] = '\0'; - struct Cat cat = { buf, sizeof(buf), 0 }; - while (*mention) { - size_t skip = strspn(mention, ",<> "); - catf(&cat, "%.*s", (int)skip, mention); - mention += skip; - - size_t len = strcspn(mention, ",<> "); - char punct = mention[len]; - mention[len] = '\0'; - enum Color color = completeColor(id, mention); + + while (msg < split) { + size_t skip = strspn(msg, ",:<> "); + ptr = seprintf(ptr, end, "%.*s", (int)skip, msg); + msg += skip; + + size_t len = strcspn(msg, ",:<> "); + char *p = seprintf(ptr, end, "%.*s", (int)len, msg); + enum Color color = completeColor(id, ptr); if (color != Default) { - catf(&cat, "\3%02d%s\3", color, mention); + ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg); } else { - catf(&cat, "%s", mention); + ptr = p; } - mention[len] = punct; - mention += len; + msg += len; } - catf(&cat, "%c", delimit); - return buf; + +rest: + return seprintf(ptr, end, "%s", msg); } static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); + char statusmsg = '\0'; + if (network.statusmsg && strchr(network.statusmsg, msg->params[0][0])) { + statusmsg = msg->params[0][0]; + msg->params[0]++; + } bool query = !strchr(network.chanTypes, msg->params[0][0]); bool server = strchr(msg->nick, '.'); bool mine = !strcmp(msg->nick, self.nick); @@ -1127,39 +1284,50 @@ 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)); - enum Heat heat = ignoreCheck((mention || query ? Hot : Warm), id, msg); + bool action = !notice && isAction(msg); + bool highlight = !mine && isMention(msg); + enum Heat heat = (!notice && (highlight || query) ? Hot : Warm); + heat = filterCheck(heat, id, msg); + if (heat > Warm && !mine && !query) highlight = true; + if (!notice && !mine && heat > Ice) { + completePull(id, msg->nick, hash(msg->user)); + } if (heat > Ice) urlScan(id, msg->nick, msg->params[1]); + + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + if (statusmsg) { + ptr = seprintf( + ptr, end, "\3%d[%c]\3 ", hash(msg->params[0]), statusmsg + ); + } if (notice) { if (id != Network) { logFormat(id, tagTime(msg), "-%s- %s", msg->nick, msg->params[1]); } - uiFormat( - id, ignoreCheck(Warm, id, msg), tagTime(msg), - "\3%d-%s-\3%d\t%s", - hash(msg->user), msg->nick, LightGray, msg->params[1] + ptr = seprintf( + ptr, end, "\3%d-%s-\3%d\t", + hash(msg->user), msg->nick, LightGray ); } else if (action) { logFormat(id, tagTime(msg), "* %s %s", msg->nick, msg->params[1]); - const char *mentions = colorMentions(id, msg); - uiFormat( - id, heat, tagTime(msg), - "%s\35\3%d* %s\17\35\t%s%s", - (mention ? "\26" : ""), hash(msg->user), msg->nick, - mentions, msg->params[1] + ptr = seprintf( + ptr, end, "%s\35\3%d* %s\17\35\t", + (highlight ? "\26" : ""), hash(msg->user), msg->nick ); } else { logFormat(id, tagTime(msg), "<%s> %s", msg->nick, msg->params[1]); - const char *mentions = colorMentions(id, msg); - uiFormat( - id, heat, tagTime(msg), - "%s\3%d<%s>\17\t%s%s", - (mention ? "\26" : ""), hash(msg->user), msg->nick, - mentions, msg->params[1] + ptr = seprintf( + ptr, end, "%s\3%d<%s>\17\t", + (highlight ? "\26" : ""), hash(msg->user), msg->nick ); } + if (notice) { + ptr = seprintf(ptr, end, "%s", msg->params[1]); + } else { + ptr = colorMentions(ptr, end, id, msg->params[1]); + } + uiWrite(id, heat, tagTime(msg), buf); } static void handlePing(struct Message *msg) { @@ -1174,67 +1342,81 @@ static void handleError(struct Message *msg) { static const struct Handler { const char *cmd; + int reply; Handler *fn; } Handlers[] = { - { "001", handleReplyWelcome }, - { "005", handleReplyISupport }, - { "221", handleReplyUserModeIs }, - { "276", handleReplyWhoisGeneric }, - { "301", handleReplyAway }, - { "305", handleReplyNowAway }, - { "306", handleReplyNowAway }, - { "307", handleReplyWhoisGeneric }, - { "311", handleReplyWhoisUser }, - { "312", handleReplyWhoisServer }, - { "313", handleReplyWhoisGeneric }, - { "317", handleReplyWhoisIdle }, - { "318", handleReplyEndOfWhois }, - { "319", handleReplyWhoisChannels }, - { "322", handleReplyList }, - { "323", handleReplyListEnd }, - { "324", handleReplyChannelModeIs }, - { "330", handleReplyWhoisGeneric }, - { "331", handleReplyNoTopic }, - { "332", handleReplyTopic }, - { "341", handleReplyInviting }, - { "346", handleReplyInviteList }, - { "347", handleReplyEndOfInviteList }, - { "348", handleReplyExceptList }, - { "349", handleReplyEndOfExceptList }, - { "353", handleReplyNames }, - { "366", handleReplyEndOfNames }, - { "367", handleReplyBanList }, - { "368", handleReplyEndOfBanList }, - { "372", handleReplyMOTD }, - { "378", handleReplyWhoisGeneric }, - { "379", handleReplyWhoisGeneric }, - { "422", handleErrorNoMOTD }, - { "432", handleErrorErroneousNickname }, - { "433", handleErrorNicknameInUse }, - { "441", handleErrorUserNotInChannel }, - { "443", handleErrorUserOnChannel }, - { "478", handleErrorBanListFull }, - { "482", handleErrorChanopPrivsNeeded }, - { "671", handleReplyWhoisGeneric }, - { "900", handleReplyLoggedIn }, - { "904", handleErrorSASLFail }, - { "905", handleErrorSASLFail }, - { "906", handleErrorSASLFail }, - { "AUTHENTICATE", handleAuthenticate }, - { "CAP", handleCap }, - { "CHGHOST", handleChghost }, - { "ERROR", handleError }, - { "INVITE", handleInvite }, - { "JOIN", handleJoin }, - { "KICK", handleKick }, - { "MODE", handleMode }, - { "NICK", handleNick }, - { "NOTICE", handlePrivmsg }, - { "PART", handlePart }, - { "PING", handlePing }, - { "PRIVMSG", handlePrivmsg }, - { "QUIT", handleQuit }, - { "TOPIC", handleTopic }, + { "001", 0, handleReplyWelcome }, + { "005", 0, handleReplyISupport }, + { "221", -ReplyMode, handleReplyUserModeIs }, + { "276", +ReplyWhois, handleReplyWhoisGeneric }, + { "301", 0, handleReplyAway }, + { "305", -ReplyAway, handleReplyNowAway }, + { "306", -ReplyAway, handleReplyNowAway }, + { "307", +ReplyWhois, handleReplyWhoisGeneric }, + { "311", +ReplyWhois, handleReplyWhoisUser }, + { "312", 0, handleReplyWhoisServer }, + { "313", +ReplyWhois, handleReplyWhoisGeneric }, + { "314", +ReplyWhowas, handleReplyWhowasUser }, + { "317", +ReplyWhois, handleReplyWhoisIdle }, + { "318", -ReplyWhois, handleReplyEndOfWhois }, + { "319", +ReplyWhois, handleReplyWhoisChannels }, + { "320", +ReplyWhois, handleReplyWhoisGeneric }, + { "322", +ReplyList, handleReplyList }, + { "323", -ReplyList, NULL }, + { "324", -ReplyMode, handleReplyChannelModeIs }, + { "330", +ReplyWhois, handleReplyWhoisGeneric }, + { "331", -ReplyTopic, handleReplyNoTopic }, + { "332", 0, handleReplyTopic }, + { "335", +ReplyWhois, handleReplyWhoisGeneric }, + { "338", +ReplyWhois, handleReplyWhoisGeneric }, + { "341", 0, handleReplyInviting }, + { "346", +ReplyInvex, handleReplyInviteList }, + { "347", -ReplyInvex, NULL }, + { "348", +ReplyExcepts, handleReplyExceptList }, + { "349", -ReplyExcepts, NULL }, + { "353", 0, handleReplyNames }, + { "366", 0, handleReplyEndOfNames }, + { "367", +ReplyBan, handleReplyBanList }, + { "368", -ReplyBan, NULL }, + { "369", -ReplyWhowas, handleReplyEndOfWhowas }, + { "372", 0, handleReplyMOTD }, + { "378", +ReplyWhois, handleReplyWhoisGeneric }, + { "379", +ReplyWhois, handleReplyWhoisGeneric }, + { "422", 0, handleErrorNoMOTD }, + { "432", 0, handleErrorErroneousNickname }, + { "433", 0, handleErrorNicknameInUse }, + { "437", 0, handleErrorNicknameInUse }, + { "441", 0, handleErrorUserNotInChannel }, + { "443", 0, handleErrorUserOnChannel }, + { "478", 0, handleErrorBanListFull }, + { "482", 0, handleErrorChanopPrivsNeeded }, + { "671", +ReplyWhois, handleReplyWhoisGeneric }, + { "704", +ReplyHelp, handleReplyHelp }, + { "705", +ReplyHelp, handleReplyHelp }, + { "706", -ReplyHelp, NULL }, + { "900", 0, handleReplyLoggedIn }, + { "904", 0, handleErrorSASLFail }, + { "905", 0, handleErrorSASLFail }, + { "906", 0, handleErrorSASLFail }, + { "AUTHENTICATE", 0, handleAuthenticate }, + { "CAP", 0, handleCap }, + { "CHGHOST", 0, handleChghost }, + { "ERROR", 0, handleError }, + { "FAIL", 0, handleStandardReply }, + { "INVITE", 0, handleInvite }, + { "JOIN", 0, handleJoin }, + { "KICK", 0, handleKick }, + { "MODE", 0, handleMode }, + { "NICK", 0, handleNick }, + { "NOTE", 0, handleStandardReply }, + { "NOTICE", 0, handlePrivmsg }, + { "PART", 0, handlePart }, + { "PING", 0, handlePing }, + { "PRIVMSG", 0, handlePrivmsg }, + { "QUIT", 0, handleQuit }, + { "SETNAME", 0, handleSetname }, + { "TOPIC", 0, handleTopic }, + { "WARN", 0, handleStandardReply }, }; static int compar(const void *cmd, const void *_handler) { @@ -1251,8 +1433,12 @@ void handle(struct Message *msg) { msg->cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar ); if (handler) { - handler->fn(msg); + if (handler->reply && !replies[abs(handler->reply)]) return; + if (handler->fn) handler->fn(msg); + if (handler->reply < 0) replies[abs(handler->reply)]--; } else if (strcmp(msg->cmd, "400") >= 0 && strcmp(msg->cmd, "599") <= 0) { handleErrorGeneric(msg); + } else if (isdigit(msg->cmd[0])) { + handleReplyGeneric(msg); } } diff --git a/ignore.c b/ignore.c deleted file mode 100644 index f8e4d59..0000000 --- a/ignore.c +++ /dev/null @@ -1,89 +0,0 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * Additional permission under GNU GPL version 3 section 7: - * - * If you modify this Program, or any covered work, by linking or - * combining it with OpenSSL (or a modified version of that library), - * containing parts covered by the terms of the OpenSSL License and the - * original SSLeay license, the licensors of this Program grant you - * additional permission to convey the resulting work. Corresponding - * Source for a non-source form of such a combination shall include the - * source code for the parts of OpenSSL used as well as that of the - * covered work. - */ - -#include <err.h> -#include <fnmatch.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sysexits.h> - -#include "chat.h" - -struct Ignore ignore; - -const char *ignoreAdd(const char *pattern) { - if (ignore.len == IgnoreCap) errx(EX_CONFIG, "ignore limit exceeded"); - uint ex = 0, sp = 0; - for (const char *ch = pattern; *ch; ++ch) { - if (*ch == '!') ex++; - if (*ch == ' ') sp++; - } - char **dest = &ignore.patterns[ignore.len++]; - int n = 0; - if (!ex && !sp) { - n = asprintf(dest, "%s!*@* * * *", pattern); - } else if (sp < 1) { - n = asprintf(dest, "%s * * *", pattern); - } else if (sp < 2) { - n = asprintf(dest, "%s * *", pattern); - } else if (sp < 3) { - n = asprintf(dest, "%s *", pattern); - } else { - *dest = strdup(pattern); - } - if (n < 0) err(EX_OSERR, "asprintf"); - if (!*dest) err(EX_OSERR, "strdup"); - return *dest; -} - -bool ignoreRemove(const char *pattern) { - bool found = false; - for (size_t i = 0; i < ignore.len; ++i) { - if (strcasecmp(ignore.patterns[i], pattern)) continue; - free(ignore.patterns[i]); - ignore.patterns[i] = ignore.patterns[--ignore.len]; - found = true; - } - return found; -} - -enum Heat ignoreCheck(enum Heat heat, uint id, const struct Message *msg) { - if (!ignore.len) return heat; - char match[512]; - snprintf( - match, sizeof(match), "%s!%s@%s %s %s %s", - msg->nick, msg->user, msg->host, - msg->cmd, idNames[id], (msg->params[1] ?: "") - ); - for (size_t i = 0; i < ignore.len; ++i) { - if (fnmatch(ignore.patterns[i], match, FNM_CASEFOLD)) continue; - return Ice; - } - return heat; -} diff --git a/input.c b/input.c new file mode 100644 index 0000000..6b33b93 --- /dev/null +++ b/input.c @@ -0,0 +1,629 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#define _XOPEN_SOURCE_EXTENDED + +#include <assert.h> +#include <curses.h> +#include <err.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <termios.h> +#include <unistd.h> +#include <wchar.h> +#include <wctype.h> + +#include "chat.h" +#include "edit.h" + +#define ENUM_KEY \ + X(KeyCtrlLeft, "\33[1;5D", NULL) \ + X(KeyCtrlRight, "\33[1;5C", NULL) \ + X(KeyMeta0, "\0330", "\33)") \ + X(KeyMeta1, "\0331", "\33!") \ + X(KeyMeta2, "\0332", "\33@") \ + X(KeyMeta3, "\0333", "\33#") \ + X(KeyMeta4, "\0334", "\33$") \ + X(KeyMeta5, "\0335", "\33%") \ + X(KeyMeta6, "\0336", "\33^") \ + X(KeyMeta7, "\0337", "\33&") \ + X(KeyMeta8, "\0338", "\33*") \ + X(KeyMeta9, "\0339", "\33(") \ + X(KeyMetaA, "\33a", NULL) \ + X(KeyMetaB, "\33b", NULL) \ + X(KeyMetaD, "\33d", NULL) \ + X(KeyMetaF, "\33f", NULL) \ + X(KeyMetaL, "\33l", NULL) \ + X(KeyMetaM, "\33m", NULL) \ + X(KeyMetaN, "\33n", NULL) \ + X(KeyMetaP, "\33p", NULL) \ + X(KeyMetaQ, "\33q", NULL) \ + X(KeyMetaS, "\33s", NULL) \ + X(KeyMetaT, "\33t", NULL) \ + X(KeyMetaU, "\33u", NULL) \ + X(KeyMetaV, "\33v", NULL) \ + X(KeyMetaEnter, "\33\r", "\33\n") \ + X(KeyMetaGt, "\33>", "\33.") \ + X(KeyMetaLt, "\33<", "\33,") \ + X(KeyMetaEqual, "\33=", NULL) \ + X(KeyMetaMinus, "\33-", "\33_") \ + X(KeyMetaPlus, "\33+", NULL) \ + X(KeyMetaSlash, "\33/", "\33?") \ + X(KeyFocusIn, "\33[I", NULL) \ + X(KeyFocusOut, "\33[O", NULL) \ + X(KeyPasteOn, "\33[200~", NULL) \ + X(KeyPasteOff, "\33[201~", NULL) \ + X(KeyPasteManual, "\32p", "\32\20") + +enum { + KeyMax = KEY_MAX, +#define X(id, seq, alt) id, + ENUM_KEY +#undef X +}; + +static struct Edit cut; +static struct Edit edits[IDCap]; + +void inputInit(void) { + for (size_t i = 0; i < ARRAY_LEN(edits); ++i) { + edits[i].cut = &cut; + } + + struct termios term; + int error = tcgetattr(STDOUT_FILENO, &term); + if (error) err(EX_OSERR, "tcgetattr"); + + // Gain use of C-q, C-s, C-c, C-z, C-y, C-v, C-o. + term.c_iflag &= ~IXON; + term.c_cc[VINTR] = _POSIX_VDISABLE; + term.c_cc[VSUSP] = _POSIX_VDISABLE; +#ifdef VDSUSP + term.c_cc[VDSUSP] = _POSIX_VDISABLE; +#endif + term.c_cc[VLNEXT] = _POSIX_VDISABLE; + term.c_cc[VDISCARD] = _POSIX_VDISABLE; + + error = tcsetattr(STDOUT_FILENO, TCSANOW, &term); + if (error) err(EX_OSERR, "tcsetattr"); + + def_prog_mode(); + +#define X(id, seq, alt) define_key(seq, id); if (alt) define_key(alt, id); + ENUM_KEY +#undef X + + keypad(uiInput, true); + nodelay(uiInput, true); +} + +static void inputAdd(struct Style reset, struct Style *style, const char *str) { + while (*str) { + const char *code = str; + size_t len = styleParse(style, &str); + wattr_set(uiInput, A_BOLD | A_REVERSE, 0, NULL); + switch (*code) { + break; case B: waddch(uiInput, 'B'); + break; case C: waddch(uiInput, 'C'); + break; case O: waddch(uiInput, 'O'); + break; case R: waddch(uiInput, 'R'); + break; case I: waddch(uiInput, 'I'); + break; case U: waddch(uiInput, 'U'); + break; case '\n': waddch(uiInput, 'N'); + } + if (str - code > 1) waddnstr(uiInput, &code[1], str - &code[1]); + if (str[0] == '\n') { + *style = reset; + str++; + len--; + } + size_t nl = strcspn(str, "\n"); + if (nl < len) len = nl; + wattr_set(uiInput, uiAttr(*style), uiPair(*style), NULL); + waddnstr(uiInput, str, len); + str += len; + } +} + +static char *inputStop( + struct Style reset, struct Style *style, + const char *str, char *stop +) { + char ch = *stop; + *stop = '\0'; + inputAdd(reset, style, str); + *stop = ch; + return stop; +} + +static size_t cap; +static char *buf; + +void inputUpdate(void) { + uint id = windowID(); + + size_t pos = 0; + const char *ptr = editString(&edits[id], &buf, &cap, &pos); + if (!ptr) err(EX_OSERR, "editString"); + + const char *prefix = ""; + const char *prompt = self.nick; + const char *suffix = ""; + const char *skip = buf; + struct Style stylePrompt = { .fg = self.color, .bg = Default }; + struct Style styleInput = StyleDefault; + + size_t split = commandWillSplit(id, buf); + const char *privmsg = commandIsPrivmsg(id, buf); + const char *notice = commandIsNotice(id, buf); + const char *action = commandIsAction(id, buf); + if (privmsg) { + prefix = "<"; suffix = "> "; + skip = privmsg; + } else if (notice) { + prefix = "-"; suffix = "- "; + styleInput.fg = LightGray; + skip = notice; + } else if (action) { + prefix = "* "; suffix = " "; + stylePrompt.attr |= Italic; + styleInput.attr |= Italic; + skip = action; + } else if (id == Debug && buf[0] != '/') { + prompt = "<< "; + stylePrompt.fg = Gray; + } else { + prompt = ""; + } + if (skip > &buf[pos]) { + prefix = prompt = suffix = ""; + skip = buf; + } + + int y, x; + wmove(uiInput, 0, 0); + if (windowTimeEnable() && id != Network) { + whline(uiInput, ' ', windowTime.width); + wmove(uiInput, 0, windowTime.width); + } + wattr_set(uiInput, uiAttr(stylePrompt), uiPair(stylePrompt), NULL); + waddstr(uiInput, prefix); + waddstr(uiInput, prompt); + waddstr(uiInput, suffix); + getyx(uiInput, y, x); + + int posx; + struct Style style = styleInput; + inputStop(styleInput, &style, skip, &buf[pos]); + getyx(uiInput, y, posx); + wmove(uiInput, y, x); + + ptr = skip; + style = styleInput; + if (split) { + ptr = inputStop(styleInput, &style, ptr, &buf[split]); + style = styleInput; + style.bg = Red; + } + inputAdd(styleInput, &style, ptr); + wclrtoeol(uiInput); + wmove(uiInput, y, posx); +} + +bool inputPending(uint id) { + return edits[id].len; +} + +static const struct { + const wchar_t *name; + const wchar_t *string; +} Macros[] = { + { L"\\banhammer", L"▬▬▬▬▬▬▬▋ Ò╭╮Ó" }, + { L"\\bear", L"ʕっ•ᴥ•ʔっ" }, + { L"\\blush", L"(˶′◡‵˶)" }, + { L"\\com", L"\0038,4\2 ☭ " }, + { L"\\cool", L"(⌐■_■)" }, + { L"\\flip", L"(╯°□°)╯︵ ┻━┻" }, + { L"\\gary", L"ᕕ( ᐛ )ᕗ" }, + { L"\\hug", L"(っ・∀・)っ" }, + { L"\\lenny", L"( ͡° ͜ʖ ͡°)" }, + { L"\\look", L"ಠ_ಠ" }, + { L"\\shrug", L"¯\\_(ツ)_/¯" }, + { L"\\unflip", L"┬─┬ノ(º_ºノ)" }, + { L"\\wave", L"ヾ(^∇^)" }, +}; + +void inputCompletion(void) { + char mbs[256]; + for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) { + size_t n = wcstombs(mbs, Macros[i].name, sizeof(mbs)); + assert(n != (size_t)-1); + completePush(None, mbs, Default); + } +} + +static int macroExpand(struct Edit *e) { + size_t macro = e->pos; + while (macro && e->buf[macro] != L'\\') macro--; + if (macro == e->pos) return 0; + for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) { + if (wcslen(Macros[i].name) != e->pos - macro) continue; + if (wcsncmp(Macros[i].name, &e->buf[macro], e->pos - macro)) continue; + if (wcstombs(NULL, Macros[i].string, 0) == (size_t)-1) continue; + size_t expand = wcslen(Macros[i].string); + int error = 0 + || editDelete(e, false, macro, e->pos - macro) + || editReserve(e, macro, expand); + if (error) return error; + wcsncpy(&e->buf[macro], Macros[i].string, expand); + e->pos = macro + expand; + break; + } + return 0; +} + +static struct { + uint id; + char *pre; + size_t pos; + size_t len; + bool suffix; + struct Cursor curs; +} tab; + +static void tabAccept(void) { + completeAccept(&tab.curs); + tab.len = 0; +} + +static void tabReject(void) { + completeReject(&tab.curs); + tab.len = 0; +} + +static int tabComplete(struct Edit *e, uint id) { + if (tab.len && id != tab.id) { + tabAccept(); + } + + if (!tab.len) { + tab.id = id; + tab.pos = e->pos; + while (tab.pos && !iswspace(e->buf[tab.pos-1])) tab.pos--; + tab.len = e->pos - tab.pos; + if (!tab.len) return 0; + + size_t cap = tab.len * MB_CUR_MAX + 1; + char *buf = realloc(tab.pre, cap); + if (!buf) return -1; + tab.pre = buf; + + const wchar_t *ptr = &e->buf[tab.pos]; + size_t n = wcsnrtombs(tab.pre, &ptr, tab.len, cap-1, NULL); + if (n == (size_t)-1) return -1; + tab.pre[n] = '\0'; + tab.suffix = true; + } + + const char *comp = completePrefix(&tab.curs, id, tab.pre); + if (!comp) { + comp = completePrefix(&tab.curs, id, tab.pre); + tab.suffix ^= true; + } + if (!comp) { + tab.len = 0; + return 0; + } + + size_t cap = strlen(comp) + 1; + wchar_t *wcs = malloc(sizeof(*wcs) * cap); + if (!wcs) return -1; + + size_t n = mbstowcs(wcs, comp, cap); + assert(n != (size_t)-1); + + bool colon = (tab.len >= 2 && e->buf[tab.pos + tab.len - 2] == L':'); + + int error = editDelete(e, false, tab.pos, tab.len); + if (error) goto fail; + + tab.len = n; + if (wcs[0] == L'\\' || wcschr(wcs, L' ')) { + error = editReserve(e, tab.pos, tab.len); + if (error) goto fail; + } else if (wcs[0] != L'/' && tab.suffix && (!tab.pos || colon)) { + tab.len += 2; + error = editReserve(e, tab.pos, tab.len); + if (error) goto fail; + e->buf[tab.pos + n + 0] = L':'; + e->buf[tab.pos + n + 1] = L' '; + } else if (tab.suffix && tab.pos >= 2 && e->buf[tab.pos - 2] == L':') { + tab.len += 2; + error = editReserve(e, tab.pos, tab.len); + if (error) goto fail; + e->buf[tab.pos - 2] = L','; + e->buf[tab.pos + n + 0] = L':'; + e->buf[tab.pos + n + 1] = L' '; + } else { + tab.len++; + error = editReserve(e, tab.pos, tab.len); + if (error) goto fail; + if (!tab.suffix && tab.pos >= 2 && e->buf[tab.pos - 2] == L',') { + e->buf[tab.pos - 2] = L':'; + } + e->buf[tab.pos + n] = L' '; + } + wmemcpy(&e->buf[tab.pos], wcs, n); + e->pos = tab.pos + tab.len; + free(wcs); + return 0; + +fail: + free(wcs); + return -1; +} + +static void inputEnter(void) { + uint id = windowID(); + char *cmd = editString(&edits[id], &buf, &cap, NULL); + if (!cmd) err(EX_OSERR, "editString"); + + tabAccept(); + editFn(&edits[id], EditClear); + command(id, cmd); +} + +static void keyCode(int code) { + int error = 0; + struct Edit *edit = &edits[windowID()]; + switch (code) { + break; case KEY_RESIZE: uiResize(); + break; case KeyFocusIn: windowUnmark(); + break; case KeyFocusOut: windowMark(); + + break; case KeyMetaEnter: error = editInsert(edit, L'\n'); + break; case KeyMetaEqual: windowToggleMute(); + break; case KeyMetaMinus: windowToggleThresh(-1); + break; case KeyMetaPlus: windowToggleThresh(+1); + break; case KeyMetaSlash: windowSwap(); + + break; case KeyMetaGt: windowScroll(ScrollAll, -1); + break; case KeyMetaLt: windowScroll(ScrollAll, +1); + + break; case KeyMeta0 ... KeyMeta9: windowShow(code - KeyMeta0); + break; case KeyMetaA: windowAuto(); + break; case KeyMetaB: error = editFn(edit, EditPrevWord); + break; case KeyMetaD: error = editFn(edit, EditDeleteNextWord); + break; case KeyMetaF: error = editFn(edit, EditNextWord); + break; case KeyMetaL: windowBare(); + break; case KeyMetaM: uiWrite(windowID(), Warm, NULL, ""); + break; case KeyMetaN: windowScroll(ScrollHot, +1); + break; case KeyMetaP: windowScroll(ScrollHot, -1); + break; case KeyMetaQ: error = editFn(edit, EditCollapse); + break; case KeyMetaS: uiSpoilerReveal ^= true; windowUpdate(); + break; case KeyMetaT: windowToggleTime(); + break; case KeyMetaU: windowScroll(ScrollUnread, 0); + break; case KeyMetaV: windowScroll(ScrollPage, +1); + + break; case KeyCtrlLeft: error = editFn(edit, EditPrevWord); + break; case KeyCtrlRight: error = editFn(edit, EditNextWord); + + break; case KEY_BACKSPACE: error = editFn(edit, EditDeletePrev); + break; case KEY_DC: error = editFn(edit, EditDeleteNext); + break; case KEY_DOWN: windowScroll(ScrollOne, -1); + break; case KEY_END: error = editFn(edit, EditTail); + break; case KEY_ENTER: inputEnter(); + break; case KEY_HOME: error = editFn(edit, EditHead); + break; case KEY_LEFT: error = editFn(edit, EditPrev); + break; case KEY_NPAGE: windowScroll(ScrollPage, -1); + break; case KEY_PPAGE: windowScroll(ScrollPage, +1); + break; case KEY_RIGHT: error = editFn(edit, EditNext); + break; case KEY_SEND: windowScroll(ScrollAll, -1); + break; case KEY_SHOME: windowScroll(ScrollAll, +1); + break; case KEY_UP: windowScroll(ScrollOne, +1); + } + if (error) err(EX_OSERR, "editFn"); +} + +static void keyCtrl(wchar_t ch) { + int error = 0; + struct Edit *edit = &edits[windowID()]; + switch (ch ^ L'@') { + break; case L'?': error = editFn(edit, EditDeletePrev); + break; case L'A': error = editFn(edit, EditHead); + break; case L'B': error = editFn(edit, EditPrev); + break; case L'C': raise(SIGINT); + break; case L'D': error = editFn(edit, EditDeleteNext); + break; case L'E': error = editFn(edit, EditTail); + break; case L'F': error = editFn(edit, EditNext); + break; case L'H': error = editFn(edit, EditDeletePrev); + break; case L'I': error = tabComplete(edit, windowID()); + break; case L'J': inputEnter(); + break; case L'K': error = editFn(edit, EditDeleteTail); + break; case L'L': clearok(curscr, true); wrefresh(curscr); + break; case L'N': windowShow(windowNum() + 1); + break; case L'P': windowShow(windowNum() - 1); + break; case L'R': windowSearch(editString(edit, &buf, &cap, NULL), -1); + break; case L'S': windowSearch(editString(edit, &buf, &cap, NULL), +1); + break; case L'T': error = editFn(edit, EditTranspose); + break; case L'U': error = editFn(edit, EditDeleteHead); + break; case L'V': windowScroll(ScrollPage, -1); + break; case L'W': error = editFn(edit, EditDeletePrevWord); + break; case L'X': error = macroExpand(edit); tabAccept(); + break; case L'Y': error = editFn(edit, EditPaste); + } + if (error) err(EX_OSERR, "editFn"); +} + +static void keyStyle(wchar_t ch) { + if (iswcntrl(ch)) ch = towlower(ch ^ L'@'); + char buf[8] = {0}; + enum Color color = Default; + switch (ch) { + break; case L'A': color = Gray; + break; case L'B': color = Blue; + break; case L'C': color = Cyan; + break; case L'G': color = Green; + break; case L'K': color = Black; + break; case L'M': color = Magenta; + break; case L'N': color = Brown; + break; case L'O': color = Orange; + break; case L'P': color = Pink; + break; case L'R': color = Red; + break; case L'W': color = White; + break; case L'Y': color = Yellow; + break; case L'b': buf[0] = B; + break; case L'c': buf[0] = C; + break; case L'i': buf[0] = I; + break; case L'o': buf[0] = O; + break; case L'r': buf[0] = R; + break; case L's': { + snprintf(buf, sizeof(buf), "%c%02d,%02d", C, Black, Black); + } + break; case L'u': buf[0] = U; + } + if (color != Default) { + snprintf(buf, sizeof(buf), "%c%02d", C, color); + } + struct Edit *edit = &edits[windowID()]; + for (char *ch = buf; *ch; ++ch) { + int error = editInsert(edit, *ch); + if (error) err(EX_OSERR, "editInsert"); + } +} + +static bool waiting; + +void inputWait(void) { + waiting = true; +} + +void inputRead(void) { + if (isendwin()) { + if (waiting) { + uiShow(); + flushinp(); + waiting = false; + } else { + return; + } + } + + wint_t ch; + static bool paste, style, literal; + for (int ret; ERR != (ret = wget_wch(uiInput, &ch));) { + bool tabbing = false; + size_t pos = edits[tab.id].pos; + bool spr = uiSpoilerReveal; + + if (ret == KEY_CODE_YES && ch == KeyPasteOn) { + paste = true; + } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) { + paste = false; + } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { + paste ^= true; + } else if (paste || literal) { + int error = editInsert(&edits[windowID()], ch); + if (error) err(EX_OSERR, "editInsert"); + } else if (ret == KEY_CODE_YES) { + keyCode(ch); + } else if (ch == (L'Z' ^ L'@')) { + style = true; + continue; + } else if (style && ch == (L'V' ^ L'@')) { + literal = true; + continue; + } else if (style) { + keyStyle(ch); + } else if (iswcntrl(ch)) { + tabbing = (ch == (L'I' ^ L'@')); + keyCtrl(ch); + } else { + int error = editInsert(&edits[windowID()], ch); + if (error) err(EX_OSERR, "editInsert"); + } + style = false; + literal = false; + + if (!tabbing) { + if (edits[tab.id].pos > pos) { + tabAccept(); + } else if (edits[tab.id].pos < pos) { + tabReject(); + } + } + + if (spr) { + uiSpoilerReveal = false; + windowUpdate(); + } + } + inputUpdate(); +} + +static int writeString(FILE *file, const char *str) { + return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1); +} + +int inputSave(FILE *file) { + int error; + for (uint id = 0; id < IDCap; ++id) { + if (!edits[id].len) continue; + char *ptr = editString(&edits[id], &buf, &cap, NULL); + if (!ptr) return -1; + error = 0 + || writeString(file, idNames[id]) + || writeString(file, ptr); + if (error) return error; + } + return writeString(file, ""); +} + +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 inputLoad(FILE *file, size_t version) { + if (version < 8) return; + while (0 < readString(file, &buf, &cap) && buf[0]) { + uint id = idFor(buf); + readString(file, &buf, &cap); + size_t max = strlen(buf); + int error = editReserve(&edits[id], 0, max); + if (error) err(EX_OSERR, "editReserve"); + size_t len = mbstowcs(edits[id].buf, buf, max); + assert(len != (size_t)-1); + edits[id].len = len; + edits[id].pos = len; + } +} diff --git a/irc.c b/irc.c index 59b467c..1fc2c3f 100644 --- a/irc.c +++ b/irc.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,9 @@ #include <assert.h> #include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> #include <netdb.h> #include <netinet/in.h> #include <stdarg.h> @@ -41,63 +44,59 @@ #include "chat.h" -struct tls *client; +static struct tls *client; +static struct tls_config *config; -static byte *readFile(size_t *len, FILE *file) { - struct stat stat; - int error = fstat(fileno(file), &stat); - if (error) err(EX_IOERR, "fstat"); +void ircConfig( + bool insecure, const char *trust, const char *cert, const char *priv +) { + int error = 0; + char buf[PATH_MAX]; - 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(); + 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 (trust) { + tls_config_insecure_noverifyname(config); + for (int i = 0; configPath(buf, sizeof(buf), trust, i); ++i) { + error = tls_config_set_ca_file(config, buf); + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config)); + } + + // Explicitly load the default CA cert file on OpenBSD now so it doesn't + // need to be unveiled. Other systems might use a CA directory, so avoid + // changing the default behavior. +#ifdef __OpenBSD__ + if (!insecure && !trust) { + const char *ca = tls_default_ca_cert_file(); + error = tls_config_set_ca_file(config, ca); + if (error) errx(EX_OSFILE, "%s: %s", ca, tls_config_error(config)); + } +#endif if (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); + for (int i = 0; configPath(buf, sizeof(buf), cert, i); ++i) { + if (priv) { + error = tls_config_set_cert_file(config, buf); + } else { + error = tls_config_set_keypair_file(config, buf, buf); + } + if (!error) break; } - error = tls_config_set_key_mem(config, buf, len); - if (error) { - errx( - EX_CONFIG, "tls_config_set_key_mem: %s", - tls_config_error(config) - ); + if (error) errx(EX_NOINPUT, "%s: %s", cert, tls_config_error(config)); + } + if (priv) { + for (int i = 0; configPath(buf, sizeof(buf), priv, i); ++i) { + error = tls_config_set_key_file(config, buf); + if (!error) break; } - explicit_bzero(buf, len); - free(buf); + if (error) errx(EX_NOINPUT, "%s: %s", priv, tls_config_error(config)); } client = tls_client(); @@ -105,7 +104,6 @@ void ircConfig(bool insecure, FILE *cert, FILE *priv) { error = tls_configure(client, config); if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); - tls_config_free(config); } int ircConnect(const char *bindHost, const char *host, const char *port) { @@ -152,6 +150,7 @@ int ircConnect(const char *bindHost, const char *host, const char *port) { error = connect(sock, ai->ai_addr, ai->ai_addrlen); if (!error) break; + if (error && errno == EINTR) break; // connect continues asynchronously close(sock); sock = -1; @@ -159,13 +158,29 @@ int ircConnect(const char *bindHost, const char *host, const char *port) { if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port); freeaddrinfo(head); + fcntl(sock, F_SETFD, FD_CLOEXEC); error = tls_connect_socket(client, sock, host); if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client)); - error = tls_handshake(client); + return sock; +} + +void ircHandshake(void) { + int error; + do { + error = tls_handshake(client); + } while (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT); if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client)); - return sock; + tls_config_clear_keys(config); +} + +void ircPrintCert(void) { + size_t len; + ircHandshake(); + const byte *pem = tls_peer_cert_chain_pem(client, &len); + printf("subject= %s\n", tls_peer_cert_subject(client)); + fwrite(pem, len, 1, stdout); } enum { MessageCap = 8191 + 512 }; @@ -235,8 +250,12 @@ static struct Message parse(char *line) { char *key = strsep(&tag, "="); for (uint i = 0; i < TagCap; ++i) { if (strcmp(key, TagNames[i])) continue; - unescape(tag); - msg.tags[i] = tag; + if (tag) { + unescape(tag); + msg.tags[i] = tag; + } else { + msg.tags[i] = ""; + } break; } } diff --git a/log.c b/log.c index c114c41..d6b3f2a 100644 --- a/log.c +++ b/log.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,16 +27,60 @@ #include <assert.h> #include <err.h> +#include <errno.h> +#include <fcntl.h> #include <limits.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> +#include <sys/stat.h> #include <sysexits.h> #include <time.h> +#include <unistd.h> + +#ifdef __FreeBSD__ +#include <capsicum_helpers.h> +#endif #include "chat.h" -bool logEnable; +static int logDir = -1; + +void logOpen(void) { + char buf[PATH_MAX]; + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf); + + error = mkdir(dataPath(buf, sizeof(buf), "log", 0), S_IRWXU); + if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf); + + logDir = open(buf, O_RDONLY | O_CLOEXEC); + if (logDir < 0) err(EX_CANTCREAT, "%s", buf); + +#ifdef __FreeBSD__ + cap_rights_t rights; + cap_rights_init( + &rights, CAP_MKDIRAT, CAP_CREATE, CAP_WRITE, + /* for fdopen(3) */ CAP_FCNTL, CAP_FSTAT + ); + error = caph_rights_limit(logDir, &rights); + if (error) err(EX_OSERR, "cap_rights_limit"); +#endif +} + +static void logMkdir(const char *path) { + int error = mkdirat(logDir, path, S_IRWXU); + if (error && errno != EEXIST) err(EX_CANTCREAT, "log/%s", path); +} + +static void sanitize(char *ptr, char *end) { + for (char *ch = ptr; ch < end && *ch == '.'; ++ch) { + *ch = '_'; + } + for (char *ch = ptr; ch < end; ++ch) { + if (*ch == '/') *ch = '_'; + } +} static struct { int year; @@ -62,44 +106,46 @@ static FILE *logFile(uint id, const struct tm *tm) { logs[id].month = tm->tm_mon; logs[id].day = tm->tm_mday; - char path[PATH_MAX] = "log"; - size_t len = strlen(path); - dataMkdir(""); - dataMkdir(path); + char path[PATH_MAX]; + char *ptr = path, *end = &path[sizeof(path)]; - path[len++] = '/'; - for (const char *ch = network.name; *ch; ++ch) { - path[len++] = (*ch == '/' ? '_' : *ch); - } - path[len] = '\0'; - dataMkdir(path); + ptr = seprintf(ptr, end, "%s", network.name); + sanitize(path, ptr); + logMkdir(path); - path[len++] = '/'; - for (const char *ch = idNames[id]; *ch; ++ch) { - path[len++] = (*ch == '/' ? '_' : *ch); - } - path[len] = '\0'; - dataMkdir(path); + char *name = ptr; + ptr = seprintf(ptr, end, "/%s", idNames[id]); + sanitize(&name[1], ptr); + logMkdir(path); + + size_t len = strftime(ptr, end - ptr, "/%F.log", tm); + if (!len) errx(EX_CANTCREAT, "log path too long"); - strftime(&path[len], sizeof(path) - len, "/%F.log", tm); - logs[id].file = dataOpen(path, "a"); - if (!logs[id].file) exit(EX_CANTCREAT); + int fd = openat( + logDir, path, + O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, + S_IRUSR | S_IWUSR + ); + if (fd < 0) err(EX_CANTCREAT, "log/%s", path); + logs[id].file = fdopen(fd, "a"); + if (!logs[id].file) err(EX_OSERR, "fdopen"); setlinebuf(logs[id].file); return logs[id].file; } void logClose(void) { - if (!logEnable) return; + if (logDir < 0) return; for (uint id = 0; id < IDCap; ++id) { if (!logs[id].file) continue; int error = fclose(logs[id].file); if (error) err(EX_IOERR, "%s", idNames[id]); } + close(logDir); } void logFormat(uint id, const time_t *src, const char *format, ...) { - if (!logEnable) return; + if (logDir < 0) return; time_t ts = (src ? *src : time(NULL)); struct tm *tm = localtime(&ts); @@ -109,15 +155,15 @@ void logFormat(uint id, const time_t *src, const char *format, ...) { char buf[sizeof("0000-00-00T00:00:00+0000")]; strftime(buf, sizeof(buf), "%FT%T%z", tm); - fprintf(file, "[%s] ", buf); - if (ferror(file)) err(EX_IOERR, "%s", idNames[id]); + int n = fprintf(file, "[%s] ", buf); + if (n < 0) err(EX_IOERR, "%s", idNames[id]); va_list ap; va_start(ap, format); - vfprintf(file, format, ap); + n = vfprintf(file, format, ap); va_end(ap); - if (ferror(file)) err(EX_IOERR, "%s", idNames[id]); + if (n < 0) err(EX_IOERR, "%s", idNames[id]); - fprintf(file, "\n"); - if (ferror(file)) err(EX_IOERR, "%s", idNames[id]); + n = fprintf(file, "\n"); + if (n < 0) err(EX_IOERR, "%s", idNames[id]); } diff --git a/scripts/sandman.1 b/sandman.1 index 35765ec..92828c0 100644 --- a/scripts/sandman.1 +++ b/sandman.1 @@ -33,4 +33,4 @@ The default is 8 seconds. .El . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency diff --git a/scripts/sandman.m b/sandman.m index 99899ab..2e5c4db 100644 --- a/scripts/sandman.m +++ b/sandman.m @@ -1,4 +1,4 @@ -/* Copyright (C) 2019, 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2019, 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/scripts/.gitignore b/scripts/.gitignore deleted file mode 100644 index f6dc107..0000000 --- a/scripts/.gitignore +++ /dev/null @@ -1 +0,0 @@ -sandman diff --git a/scripts/build-chroot.sh b/scripts/build-chroot.sh new file mode 100644 index 0000000..a0fcf32 --- /dev/null +++ b/scripts/build-chroot.sh @@ -0,0 +1,74 @@ +#!/bin/sh +set -eux + +CHROOT_USER=$1 +CHROOT_GROUP=$2 + +if [ "$(uname)" = 'OpenBSD' ]; then + install -d -o root -g wheel \ + root \ + root/bin \ + root/etc/ssl \ + root/home \ + root/usr/bin \ + root/usr/lib \ + root/usr/libexec \ + root/usr/share/man + install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} \ + root/home/${CHROOT_USER} \ + root/home/${CHROOT_USER}/.local/share + + cp -fp /bin/sh root/bin + cp -fp /usr/libexec/ld.so root/usr/libexec + export LD_TRACE_LOADED_OBJECTS_FMT1='%p\n' + export LD_TRACE_LOADED_OBJECTS_FMT2='' + for bin in ./catgirl /usr/bin/mandoc /usr/bin/less; do + LD_TRACE_LOADED_OBJECTS=1 $bin | xargs -t -J % cp -fp % root/usr/lib + done + cp -fp /usr/bin/printf /usr/bin/mandoc /usr/bin/less root/usr/bin + make install DESTDIR=root PREFIX=/usr MANDIR=/usr/share/man + install scripts/chroot-prompt.sh root/usr/bin/catgirl-prompt + install scripts/chroot-man.sh root/usr/bin/man + + cp -fp /etc/hosts /etc/resolv.conf root/etc + cp -fp /etc/ssl/cert.pem root/etc/ssl + cp -af /usr/share/locale /usr/share/terminfo root/usr/share + + tar -c -f chroot.tar -C root bin etc home usr + +elif [ "$(uname)" = 'FreeBSD' ]; then + install -d -o root -g wheel \ + root \ + root/bin \ + root/etc \ + root/home \ + root/lib \ + root/libexec \ + root/usr/bin \ + root/usr/local/etc/ssl \ + root/usr/share/man \ + root/usr/share/misc + install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} \ + root/home/${CHROOT_USER} \ + root/home/${CHROOT_USER}/.local/share + + cp -fp /libexec/ld-elf.so.1 root/libexec + ldd -f '%p\n' catgirl /usr/bin/mandoc /usr/bin/less \ + | sort -u | xargs -t -J % cp -fp % root/lib + chflags noschg root/libexec/* root/lib/* + cp -fp /rescue/sh /usr/bin/mandoc /usr/bin/less root/bin + make install DESTDIR=root PREFIX=/usr MANDIR=/usr/share/man + install scripts/chroot-prompt.sh root/usr/bin/catgirl-prompt + install scripts/chroot-man.sh root/usr/bin/man + + cp -fp /etc/hosts /etc/resolv.conf root/etc + cp -fp /usr/local/etc/ssl/cert.pem root/usr/local/etc/ssl + cp -af /usr/share/locale root/usr/share + cp -fp /usr/share/misc/termcap.db root/usr/share/misc + + tar -c -f chroot.tar -C root bin etc home lib libexec usr + +else + echo "Don't know how to build chroot on $(uname)" >&2 + exit 1 +fi diff --git a/scripts/chat.tmux.conf b/scripts/chat.tmux.conf new file mode 100644 index 0000000..3489a19 --- /dev/null +++ b/scripts/chat.tmux.conf @@ -0,0 +1,64 @@ +# use `tmux -L chat -f ./chat.tmux.conf attach-session' (without any other +# options or parameters) to access this session group in its own tmux server, +# not interfering with existing servers/sessions/configurations + +new-session -t chat + +# catgirl(1) puts windows at the top +set-option -g -- status-position top + +# intuitive navigation +set-option -g -- mode-keys vi +set-option -g -- mouse on + +# indicate new messages +set-option -g -- monitor-activity on +set-option -g -- monitor-bell on + +# hardcode names during window creation +set-option -g -- automatic-rename off +set-option -g -- allow-rename off +set-option -g -- set-titles off +set-option -g -- renumber-windows on +# for the F1 binding, make hotkeys match window numbers +set-option -g -- base-index 1 + + +# clients exit on network errors, restart them automatically +# (use `kill-pane'/`C-b x' to destroy windows) +set-option -g -- remain-on-exit on +set-hook -g -- pane-died respawn-pane + + +# disarm ^C to avoid accidentially losing logs +bind-key -n -N 'confirm INTR key' -- C-c \ + confirm-before -p 'Send ^C? (y/N)' -- 'send-keys -- C-c' + +# one-click version of default `C-b w' (shows preview windows) +bind-key -n -N 'pick chat network' -- F1 choose-tree -Z + +# catgirl(1) might run in `-R'/`restrict'ed mode, i.e. `/help' is disabled +bind-key -n -N 'read catgirl help' -- F2 \ + new-window -S -n help -- man -s 1 -- catgirl + +# intuitive refresh, just don't spam it ;-) +bind-key -n -N 'reconnect network' -- F5 \ + confirm-before -p 'reconnect network? (y/N)' -- 'respawn-pane -k' + +# immersive mode ;-) +bind-key -n -N 'toggle fullscreen' -- F11 set status + + +# this configuration is idempotent, i.e. reloading it only changes settings +# and never duplicates already existing windows +bind-key -N 'reload configuration' -- R { + source-file -F -- '#{source_files}' + display-message -- 'configuration reloaded' +} + + +## do not double-quote commands to avoid running through "sh -c" + +# IRC +new-window -d -S -n hackint -- catgirl -- defaults hackint +new-window -d -S -n efnet -- catgirl -- defaults efnet diff --git a/scripts/chroot-prompt.sh b/scripts/chroot-prompt.sh index 3b43841..2b34426 100644 --- a/scripts/chroot-prompt.sh +++ b/scripts/chroot-prompt.sh @@ -3,4 +3,5 @@ set -eu printf 'Name: ' read -r nick rest -exec catgirl -n "$nick" -s "$nick" "$@" +printf '%s %s\n' "$nick" "$SSH_CLIENT" >>nicks.log +exec catgirl -K -n "$nick" -s "${nick##*/}" -u "${SSH_CLIENT%% *}" "$@" diff --git a/ui.c b/ui.c index 59903c9..079ee19 100644 --- a/ui.c +++ b/ui.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,159 +28,35 @@ #define _XOPEN_SOURCE_EXTENDED #include <assert.h> -#include <ctype.h> #include <curses.h> #include <err.h> #include <errno.h> -#include <limits.h> -#include <signal.h> +#include <fcntl.h> +#include <inttypes.h> #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/file.h> #include <sysexits.h> #include <term.h> -#include <termios.h> #include <time.h> #include <unistd.h> -#include <wchar.h> -#include <wctype.h> -#include "chat.h" - -// Annoying stuff from <term.h>: -#undef lines -#undef tab - -#ifndef A_ITALIC -#define A_ITALIC A_NORMAL +#ifdef __FreeBSD__ +#include <capsicum_helpers.h> #endif -enum { - StatusLines = 1, - WindowLines = 1024, - MarkerLines = 1, - SplitLines = 5, - InputLines = 1, - InputCols = 1024, -}; +#include "chat.h" #define BOTTOM (LINES - 1) #define RIGHT (COLS - 1) #define MAIN_LINES (LINES - StatusLines - InputLines) -static WINDOW *status; -static WINDOW *marker; -static WINDOW *input; - -struct Line { - enum Heat heat; - time_t time; - char *str; -}; - -enum { BufferCap = 1024 }; -struct Buffer { - size_t len; - struct Line lines[BufferCap]; -}; -_Static_assert(!(BufferCap & (BufferCap - 1)), "BufferCap is power of two"); - -static void bufferPush( - struct Buffer *buffer, enum Heat heat, time_t time, const char *str -) { - struct Line *line = &buffer->lines[buffer->len++ % BufferCap]; - free(line->str); - line->heat = heat; - line->time = time; - line->str = strdup(str); - if (!line->str) err(EX_OSERR, "strdup"); -} - -static struct Line bufferLine(const struct Buffer *buffer, size_t i) { - return buffer->lines[(buffer->len + i) % BufferCap]; -} - -struct Window { - uint id; - WINDOW *pad; - int scroll; - bool mark; - bool mute; - bool ignore; - enum Heat heat; - uint unreadHard; - uint unreadSoft; - uint unreadWarm; - struct Buffer buffer; -}; - -static struct { - struct Window *ptrs[IDCap]; - uint len; - uint show; - uint swap; - uint user; -} windows; - -static uint windowPush(struct Window *window) { - assert(windows.len < IDCap); - windows.ptrs[windows.len] = window; - return windows.len++; -} - -static uint windowInsert(uint num, struct Window *window) { - assert(windows.len < IDCap); - assert(num <= windows.len); - memmove( - &windows.ptrs[num + 1], - &windows.ptrs[num], - sizeof(*windows.ptrs) * (windows.len - num) - ); - windows.ptrs[num] = window; - windows.len++; - return num; -} - -static struct Window *windowRemove(uint num) { - assert(num < windows.len); - struct Window *window = windows.ptrs[num]; - windows.len--; - memmove( - &windows.ptrs[num], - &windows.ptrs[num + 1], - sizeof(*windows.ptrs) * (windows.len - num) - ); - return window; -} - -static uint windowFor(uint id) { - for (uint num = 0; num < windows.len; ++num) { - if (windows.ptrs[num]->id == id) return num; - } - - struct Window *window = calloc(1, sizeof(*window)); - if (!window) err(EX_OSERR, "malloc"); - - window->id = id; - window->pad = newpad(WindowLines, COLS); - if (!window->pad) err(EX_OSERR, "newpad"); - scrollok(window->pad, true); - wmove(window->pad, WindowLines - 1, 0); - window->mark = true; - window->ignore = true; - - return windowPush(window); -} - -static void windowFree(struct Window *window) { - for (size_t i = 0; i < BufferCap; ++i) { - free(window->buffer.lines[i].str); - } - delwin(window->pad); - free(window); -} +WINDOW *uiStatus; +WINDOW *uiMain; +WINDOW *uiInput; static short colorPairs; @@ -215,71 +91,14 @@ static short colorPair(short fg, short bg) { return colorPairs++; } -#define ENUM_KEY \ - X(KeyMeta0, "\0330", "\33)") \ - X(KeyMeta1, "\0331", "\33!") \ - X(KeyMeta2, "\0332", "\33@") \ - X(KeyMeta3, "\0333", "\33#") \ - X(KeyMeta4, "\0334", "\33$") \ - X(KeyMeta5, "\0335", "\33%") \ - X(KeyMeta6, "\0336", "\33^") \ - X(KeyMeta7, "\0337", "\33&") \ - X(KeyMeta8, "\0338", "\33*") \ - X(KeyMeta9, "\0339", "\33(") \ - X(KeyMetaA, "\33a", NULL) \ - X(KeyMetaB, "\33b", NULL) \ - X(KeyMetaD, "\33d", NULL) \ - X(KeyMetaF, "\33f", NULL) \ - X(KeyMetaL, "\33l", NULL) \ - X(KeyMetaM, "\33m", NULL) \ - X(KeyMetaQ, "\33q", NULL) \ - X(KeyMetaU, "\33u", NULL) \ - X(KeyMetaV, "\33v", NULL) \ - X(KeyMetaEnter, "\33\r", "\33\n") \ - X(KeyMetaGt, "\33>", "\33.") \ - X(KeyMetaLt, "\33<", "\33,") \ - X(KeyMetaEqual, "\33=", "\33+") \ - X(KeyMetaMinus, "\33-", "\33_") \ - X(KeyMetaSlash, "\33/", "\33?") \ - X(KeyFocusIn, "\33[I", NULL) \ - X(KeyFocusOut, "\33[O", NULL) \ - X(KeyPasteOn, "\33[200~", NULL) \ - X(KeyPasteOff, "\33[201~", NULL) - -enum { - KeyMax = KEY_MAX, -#define X(id, seq, alt) id, - ENUM_KEY -#undef X -}; - -// Gain use of C-q, C-s, C-c, C-z, C-y, C-v, 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; -#ifdef VDSUSP - term.c_cc[VDSUSP] = _POSIX_VDISABLE; -#endif - term.c_cc[VLNEXT] = _POSIX_VDISABLE; - term.c_cc[VDISCARD] = _POSIX_VDISABLE; - error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term); - if (error) err(EX_OSERR, "tcsetattr"); -} - // 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"; +static const char *FocusMode[2] = { "\33[?1004l", "\33[?1004h" }; +static const char *PasteMode[2] = { "\33[?2004l", "\33[?2004h" }; static void errExit(void) { - putp(ExitFocusMode); - putp(ExitPasteMode); + putp(FocusMode[false]); + putp(PasteMode[false]); reset_shell_mode(); } @@ -287,75 +106,47 @@ void uiInit(void) { initscr(); cbreak(); noecho(); - acquireKeys(); - def_prog_mode(); - atexit(errExit); colorInit(); + atexit(errExit); + +#ifndef A_ITALIC +#define A_ITALIC A_BLINK + // Force ncurses to use individual enter_attr_mode strings: + set_attributes = NULL; + enter_blink_mode = enter_italics_mode; +#endif if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; from_status_line = "\7"; } -#define X(id, seq, alt) define_key(seq, id); if (alt) define_key(alt, id); - ENUM_KEY -#undef X - - status = newwin(StatusLines, COLS, 0, 0); - if (!status) err(EX_OSERR, "newwin"); + uiStatus = newwin(StatusLines, COLS, 0, 0); + if (!uiStatus) err(EX_OSERR, "newwin"); - marker = newwin( - MarkerLines, COLS, - LINES - InputLines - SplitLines - MarkerLines, 0 - ); - wbkgd(marker, ACS_BULLET); + uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); + if (!uiMain) err(EX_OSERR, "newwin"); - input = newpad(InputLines, InputCols); - if (!input) err(EX_OSERR, "newpad"); - keypad(input, true); - nodelay(input, true); + uiInput = newpad(InputLines, InputCols); + if (!uiInput) err(EX_OSERR, "newpad"); - windowFor(Network); + windowInit(); uiShow(); } static bool hidden = true; -static bool waiting; -static char title[256]; -static char prevTitle[sizeof(title)]; +char uiTitle[TitleCap]; +static char prevTitle[TitleCap]; void uiDraw(void) { if (hidden) return; - wnoutrefresh(status); - const struct Window *window = windows.ptrs[windows.show]; - if (!window->scroll) { - pnoutrefresh( - window->pad, - WindowLines - MAIN_LINES, 0, - StatusLines, 0, - BOTTOM - InputLines, RIGHT - ); - } else { - pnoutrefresh( - window->pad, - WindowLines - window->scroll - MAIN_LINES + MarkerLines, 0, - StatusLines, 0, - BOTTOM - InputLines - SplitLines - MarkerLines, RIGHT - ); - touchwin(marker); - wnoutrefresh(marker); - pnoutrefresh( - window->pad, - WindowLines - SplitLines, 0, - LINES - InputLines - SplitLines, 0, - BOTTOM - InputLines, RIGHT - ); - } + wnoutrefresh(uiStatus); + wnoutrefresh(uiMain); int y, x; - getyx(input, y, x); + getyx(uiInput, y, x); pnoutrefresh( - input, + uiInput, 0, (x + 1 > RIGHT ? x + 1 - RIGHT : 0), LINES - InputLines, 0, BOTTOM, RIGHT @@ -364,20 +155,14 @@ void uiDraw(void) { doupdate(); if (!to_status_line) return; - if (!strcmp(title, prevTitle)) return; - strcpy(prevTitle, title); - putp(to_status_line); - putp(title); + if (!strcmp(uiTitle, prevTitle)) return; + strcpy(prevTitle, uiTitle); + putp(tparm(to_status_line, 0)); + putp(uiTitle); putp(from_status_line); fflush(stdout); } -struct Style { - attr_t attr; - enum Color fg, bg; -}; -static const struct Style Reset = { A_NORMAL, Default, Default }; - static const short Colors[ColorCap] = { [Default] = -1, [White] = 8 + COLOR_WHITE, @@ -405,245 +190,60 @@ static const short Colors[ColorCap] = { 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, }; -enum { B = '\2', C = '\3', O = '\17', R = '\26', I = '\35', U = '\37' }; - -static size_t styleParse(struct Style *style, const char **str) { - switch (**str) { - 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; - 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'; - } - } - return strcspn(*str, (const char[]) { B, C, O, R, I, U, '\0' }); -} - -static void statusAdd(const char *str) { - struct Style style = Reset; - while (*str) { - size_t len = styleParse(&style, &str); - wattr_set( - status, - style.attr | colorAttr(Colors[style.fg]), - colorPair(Colors[style.fg], Colors[style.bg]), - NULL - ); - waddnstr(status, str, len); - str += len; - } -} - -static void statusUpdate(void) { - struct { - uint unread; - enum Heat heat; - } others = { 0, Cold }; - - wmove(status, 0, 0); - for (uint num = 0; num < windows.len; ++num) { - const struct Window *window = windows.ptrs[num]; - if (num != windows.show && !window->scroll) { - if (window->heat < Warm) continue; - if (window->mute && window->heat < Hot) continue; - } - if (num != windows.show) { - others.unread += window->unreadWarm; - if (window->heat > others.heat) others.heat = window->heat; - } - char buf[256] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; - catf( - &cat, "\3%d%s %u ", - idColors[window->id], (num == windows.show ? "\26" : ""), num - ); - if (!window->ignore || window->mute) { - catf(&cat, "%s%s ", &"-"[window->ignore], &"="[!window->mute]); - } - catf(&cat, "%s ", idNames[window->id]); - if (window->mark && window->unreadWarm) { - catf( - &cat, "\3%d+%d\3%d%s", - (window->heat > Warm ? White : idColors[window->id]), - window->unreadWarm, idColors[window->id], - (window->scroll ? "" : " ") - ); - } - if (window->scroll) { - catf(&cat, "~%d ", window->scroll); - } - statusAdd(buf); - } - wclrtoeol(status); - - struct Cat cat = { title, sizeof(title), 0 }; - const struct Window *window = windows.ptrs[windows.show]; - catf(&cat, "%s %s", network.name, idNames[window->id]); - if (window->mark && window->unreadWarm) { - catf(&cat, " +%d%s", window->unreadWarm, &"!"[window->heat < Hot]); - } - if (others.unread) { - catf(&cat, " (+%d%s)", others.unread, &"!"[others.heat < Hot]); - } +uint uiAttr(struct Style style) { + attr_t attr = A_NORMAL; + if (style.attr & Bold) attr |= A_BOLD; + if (style.attr & Reverse) attr |= A_REVERSE; + if (style.attr & Italic) attr |= A_ITALIC; + if (style.attr & Underline) attr |= A_UNDERLINE; + return attr | colorAttr(Colors[style.fg]); } -static void mark(struct Window *window) { - if (window->scroll) return; - window->mark = true; - window->unreadHard = 0; - window->unreadWarm = 0; -} +bool uiSpoilerReveal; -static void unmark(struct Window *window) { - if (!window->scroll) { - window->mark = false; - window->heat = Cold; +short uiPair(struct Style style) { + if (uiSpoilerReveal && style.fg == style.bg) { + return colorPair(Colors[Default], Colors[style.bg]); } - statusUpdate(); + return colorPair(Colors[style.fg], Colors[style.bg]); } void uiShow(void) { if (!hidden) return; prevTitle[0] = '\0'; - putp(EnterFocusMode); - putp(EnterPasteMode); + putp(FocusMode[true]); + putp(PasteMode[true]); fflush(stdout); hidden = false; - unmark(windows.ptrs[windows.show]); + windowUnmark(); } void uiHide(void) { if (hidden) return; - mark(windows.ptrs[windows.show]); + windowMark(); hidden = true; - putp(ExitFocusMode); - putp(ExitPasteMode); + putp(FocusMode[false]); + putp(PasteMode[false]); endwin(); } -static void windowScroll(struct Window *window, int n) { - mark(window); - window->scroll += n; - if (window->scroll > WindowLines - MAIN_LINES) { - window->scroll = WindowLines - MAIN_LINES; - } - if (window->scroll < 0) window->scroll = 0; - unmark(window); -} - -static void windowScrollPage(struct Window *window, int n) { - windowScroll(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1)); -} - -static void windowScrollUnread(struct Window *window) { - window->scroll = 0; - windowScroll(window, window->unreadSoft - MAIN_LINES); -} - -static int wordWidth(const char *str) { - size_t len = strcspn(str, " \t"); - 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; -} - -// XXX: ncurses likes to render zero-width characters as spaces... -static int waddnstrnzw(WINDOW *win, const char *str, int len) { - wchar_t wc; - while (len) { - int n = mbtowc(&wc, str, len); - if (n < 1) return waddnstr(win, str, len); - if (wcwidth(wc)) waddnstr(win, str, n); - str += n; - len -= n; - } - return OK; -} - -static int wordWrap(WINDOW *win, const char *str) { - int y, x, width; - getmaxyx(win, y, width); - waddch(win, '\n'); - - int lines = 1; - int align = 0; - struct Style style = Reset; - while (*str) { - char ch = *str; - if (ch == ' ' || ch == '\t') { - getyx(win, y, x); - const char *word = &str[strspn(str, " \t")]; - if (width - x - 1 <= wordWidth(word)) { - lines += 1 + (align + wordWidth(word)) / width; - waddch(win, '\n'); - getyx(win, y, x); - wmove(win, y, align); - str = word; - } else { - waddch(win, (align ? ch : ' ')); - str++; - } - } - if (ch == '\t' && !align) { - getyx(win, y, align); - } - - size_t len = styleParse(&style, &str); - size_t ws = strcspn(str, " \t"); - if (ws < len) len = ws; - - wattr_set( - win, - style.attr | colorAttr(Colors[style.fg]), - colorPair(Colors[style.fg], Colors[style.bg]), - NULL - ); - waddnstrnzw(win, str, len); - str += len; - } - return lines; -} - struct Util uiNotifyUtil; static void notify(uint id, const char *str) { + if (self.restricted) return; if (!uiNotifyUtil.argc) return; + char buf[1024]; + styleStrip(buf, sizeof(buf), str); + struct Util util = uiNotifyUtil; utilPush(&util, idNames[id]); - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; - while (*str) { - struct Style style = Reset; - size_t len = styleParse(&style, &str); - catf(&cat, "%.*s", (int)len, str); - str += len; - } utilPush(&util, buf); pid_t pid = fork(); if (pid < 0) err(EX_OSERR, "fork"); if (pid) return; + setsid(); close(STDIN_FILENO); dup2(utilPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); @@ -653,27 +253,8 @@ static void notify(uint id, const char *str) { } void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) { - struct Window *window = windows.ptrs[windowFor(id)]; - time_t ts = (src ? *src : time(NULL)); - bufferPush(&window->buffer, heat, ts, str); - if (heat < Cold && window->ignore) return; - - int lines = 0; - if (!window->unreadHard++) window->unreadSoft = 0; - if (window->mark && heat > Cold) { - if (!window->unreadWarm++) { - lines++; - waddch(window->pad, '\n'); - } - if (heat > window->heat) window->heat = heat; - statusUpdate(); - } - - lines += wordWrap(window->pad, str); - window->unreadSoft += lines; - if (window->scroll) windowScroll(window, lines); - - if (window->mark && heat > Warm) { + bool note = windowWrite(id, heat, src, str); + if (note) { beep(); notify(id, str); } @@ -691,486 +272,86 @@ void uiFormat( uiWrite(id, heat, time, buf); } -static void reflow(struct Window *window) { - werase(window->pad); - wmove(window->pad, 0, 0); - - int flowed = 0; - window->unreadSoft = 0; - for (size_t i = 0; i < BufferCap; ++i) { - struct Line line = bufferLine(&window->buffer, i); - if (!line.str) continue; - if (line.heat < Cold && window->ignore) continue; - int lines = 0; - if (i == (size_t)(BufferCap - window->unreadHard)) { - waddch(window->pad, '\n'); - lines++; - } - lines += wordWrap(window->pad, line.str); - if (i >= (size_t)(BufferCap - window->unreadHard)) { - window->unreadSoft += lines; - } - flowed += lines; - } - - if (flowed < WindowLines) { - wscrl(window->pad, -(WindowLines - 1 - flowed)); - wmove(window->pad, WindowLines - 1, RIGHT); - } -} - -static void resize(void) { - mvwin(marker, LINES - InputLines - SplitLines - MarkerLines, 0); - int height, width; - getmaxyx(windows.ptrs[0]->pad, height, width); - if (width == COLS) return; - for (uint num = 0; num < windows.len; ++num) { - wresize(windows.ptrs[num]->pad, BufferCap, COLS); - reflow(windows.ptrs[num]); - } - (void)height; - statusUpdate(); -} - -static void bufferList(const struct Buffer *buffer) { - uiHide(); - waiting = true; - - for (size_t i = 0; i < BufferCap; ++i) { - struct Line line = bufferLine(buffer, i); - if (!line.str) continue; - - struct tm *tm = localtime(&line.time); - if (!tm) err(EX_OSERR, "localtime"); - - char buf[sizeof("00:00:00")]; - strftime(buf, sizeof(buf), "%T", tm); - vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL); - printf("[%s] ", buf); - - bool align = false; - struct Style style = Reset; - while (*line.str) { - if (*line.str == '\t') { - printf("%c", (align ? '\t' : ' ')); - align = true; - line.str++; - } - - size_t len = styleParse(&style, (const char **)&line.str); - size_t tab = strcspn(line.str, "\t"); - if (tab < len) len = tab; - - vid_attr( - style.attr | colorAttr(Colors[style.fg]), - colorPair(Colors[style.fg], Colors[style.bg]), - NULL - ); - printf("%.*s", (int)len, line.str); - line.str += len; - } - printf("\n"); - } -} - -static void inputAdd(struct Style *style, const char *str) { - while (*str) { - const char *code = str; - size_t len = styleParse(style, &str); - wattr_set(input, A_BOLD | A_REVERSE, 0, NULL); - switch (*code) { - 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'); - break; case '\n': waddch(input, 'N'); - } - if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); - if (str[0] == '\n') { - str++; - len--; - } - size_t nl = strcspn(str, "\n"); - if (nl < len) len = nl; - wattr_set( - input, - style->attr | colorAttr(Colors[style->fg]), - colorPair(Colors[style->fg], Colors[style->bg]), - NULL - ); - waddnstr(input, str, len); - str += len; - } -} - -static void inputUpdate(void) { - size_t pos; - char *buf = editBuffer(&pos); - uint id = windows.ptrs[windows.show]->id; - - const char *prefix = ""; - const char *prompt = self.nick; - const char *suffix = ""; - const char *skip = buf; - struct Style stylePrompt = { .fg = self.color, .bg = Default }; - struct Style styleInput = Reset; - - const char *privmsg = commandIsPrivmsg(id, buf); - const char *notice = commandIsNotice(id, buf); - const char *action = commandIsAction(id, buf); - if (privmsg) { - prefix = "<"; suffix = "> "; - skip = privmsg; - } else if (notice) { - prefix = "-"; suffix = "- "; - styleInput.fg = LightGray; - skip = notice; - } else if (action) { - prefix = "* "; suffix = " "; - stylePrompt.attr |= A_ITALIC; - styleInput.attr |= A_ITALIC; - skip = action; - } else if (id == Debug && buf[0] != '/') { - prompt = "<< "; - stylePrompt.fg = Gray; - } else { - prompt = ""; - } - if (skip > &buf[pos]) { - prefix = prompt = suffix = ""; - skip = buf; - } - - int y, x; - wmove(input, 0, 0); - wattr_set( - input, - stylePrompt.attr | colorAttr(Colors[stylePrompt.fg]), - colorPair(Colors[stylePrompt.fg], Colors[stylePrompt.bg]), - NULL - ); - waddstr(input, prefix); - waddstr(input, prompt); - waddstr(input, suffix); - struct Style style = styleInput; - char p = buf[pos]; - buf[pos] = '\0'; - inputAdd(&style, skip); - getyx(input, y, x); - buf[pos] = p; - inputAdd(&style, &buf[pos]); - wclrtoeol(input); - wmove(input, y, x); -} - -static void windowShow(uint num) { - touchwin(windows.ptrs[num]->pad); - windows.swap = windows.show; - windows.show = num; - windows.user = num; - mark(windows.ptrs[windows.swap]); - unmark(windows.ptrs[windows.show]); - inputUpdate(); -} - -void uiShowID(uint id) { - windowShow(windowFor(id)); -} - -void uiShowNum(uint num) { - if (num < windows.len) windowShow(num); -} - -void uiMoveID(uint id, uint num) { - struct Window *window = windowRemove(windowFor(id)); - if (num < windows.len) { - windowShow(windowInsert(num, window)); - } else { - windowShow(windowPush(window)); - } -} - -static void windowClose(uint num) { - if (windows.ptrs[num]->id == Network) return; - struct Window *window = windowRemove(num); - completeClear(window->id); - windowFree(window); - if (windows.swap >= num) windows.swap--; - if (windows.show == num) { - windowShow(windows.swap); - windows.swap = windows.show; - } else if (windows.show > num) { - windows.show--; - } - statusUpdate(); -} - -void uiCloseID(uint id) { - windowClose(windowFor(id)); -} - -void uiCloseNum(uint num) { - if (num < windows.len) windowClose(num); -} - -static void toggleIgnore(struct Window *window) { - window->ignore ^= true; - reflow(window); - statusUpdate(); -} - -static void showAuto(void) { - uint minHot = UINT_MAX, numHot = 0; - uint minWarm = UINT_MAX, numWarm = 0; - for (uint num = 0; num < windows.len; ++num) { - struct Window *window = windows.ptrs[num]; - if (window->heat >= Hot) { - if (window->unreadWarm >= minHot) continue; - minHot = window->unreadWarm; - numHot = num; - } - if (window->heat >= Warm && !window->mute) { - if (window->unreadWarm >= minWarm) continue; - minWarm = window->unreadWarm; - numWarm = num; - } - } - uint user = windows.user; - if (minHot < UINT_MAX) { - windowShow(numHot); - windows.user = user; - } else if (minWarm < UINT_MAX) { - windowShow(numWarm); - windows.user = user; - } else if (user != windows.show) { - windowShow(user); - } -} - -static void keyCode(int code) { - struct Window *window = windows.ptrs[windows.show]; - uint id = window->id; - switch (code) { - break; case KEY_RESIZE: resize(); - break; case KeyFocusIn: unmark(window); - break; case KeyFocusOut: mark(window); - - break; case KeyMetaEnter: edit(id, EditInsert, L'\n'); - break; case KeyMetaEqual: window->mute ^= true; statusUpdate(); - break; case KeyMetaMinus: toggleIgnore(window); - break; case KeyMetaSlash: windowShow(windows.swap); - - break; case KeyMetaGt: windowScroll(window, -WindowLines); - break; case KeyMetaLt: windowScroll(window, +WindowLines); - - break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0); - 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); - break; case KeyMetaL: bufferList(&window->buffer); - break; case KeyMetaM: waddch(window->pad, '\n'); - break; case KeyMetaQ: edit(id, EditCollapse, 0); - break; case KeyMetaU: windowScrollUnread(window); - break; case KeyMetaV: windowScrollPage(window, +1); - - 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: windowScrollPage(window, -1); - break; case KEY_PPAGE: windowScrollPage(window, +1); - break; case KEY_RIGHT: edit(id, EditNext, 0); - break; case KEY_SEND: windowScroll(window, -WindowLines); - break; case KEY_SHOME: windowScroll(window, +WindowLines); - break; case KEY_UP: windowScroll(window, +1); - } +void uiResize(void) { + wclear(uiMain); + wresize(uiMain, MAIN_LINES, COLS); + windowResize(); } -static void keyCtrl(wchar_t ch) { - struct Window *window = windows.ptrs[windows.show]; - uint id = window->id; - switch (ch ^ L'@') { - 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); - 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, EditDeleteTail, 0); - break; case L'L': clearok(curscr, true); - break; case L'N': uiShowNum(windows.show + 1); - break; case L'P': uiShowNum(windows.show - 1); - break; case L'T': edit(id, EditTranspose, 0); - break; case L'U': edit(id, EditDeleteHead, 0); - break; case L'V': windowScrollPage(window, -1); - break; case L'W': edit(id, EditDeletePrevWord, 0); - break; case L'X': edit(id, EditExpand, 0); - break; case L'Y': edit(id, EditPaste, 0); - } -} - -static void keyStyle(wchar_t ch) { - uint id = windows.ptrs[windows.show]->id; - switch (iswcntrl(ch) ? ch ^ L'@' : (wchar_t)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); - } -} +static FILE *saveFile; -void uiRead(void) { - if (hidden) { - if (waiting) { - uiShow(); - flushinp(); - waiting = false; - } else { - return; - } - } - - wint_t ch; - static bool paste, style; - for (int ret; ERR != (ret = wget_wch(input, &ch));) { - if (ret == KEY_CODE_YES && ch == KeyPasteOn) { - paste = true; - } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) { - paste = false; - } else if (paste) { - edit(windows.ptrs[windows.show]->id, EditInsert, ch); - } else if (ret == KEY_CODE_YES) { - keyCode(ch); - } else if (ch == (L'Z' ^ L'@')) { - style = true; - continue; - } else if (style) { - keyStyle(ch); - } else if (iswcntrl(ch)) { - keyCtrl(ch); - } else { - edit(windows.ptrs[windows.show]->id, EditInsert, ch); - } - style = false; - } - inputUpdate(); -} - -static const time_t Signatures[] = { +static const uint64_t Signatures[] = { 0x6C72696774616301, // no heat, unread, unreadWarm 0x6C72696774616302, // no self.pos 0x6C72696774616303, // no buffer line heat 0x6C72696774616304, // no mute - 0x6C72696774616305, + 0x6C72696774616305, // no URLs + 0x6C72696774616306, // no thresh + 0x6C72696774616307, // no window time + 0x6C72696774616308, // no input + 0x6C72696774616309, }; -static size_t signatureVersion(time_t signature) { +static size_t signatureVersion(uint64_t signature) { for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) { if (signature == Signatures[i]) return i; } - err(EX_DATAERR, "unknown file signature %jX", (uintmax_t)signature); + errx(EX_DATAERR, "unknown save file signature %" PRIX64, signature); } -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); +static int writeUint64(FILE *file, uint64_t u) { + return (fwrite(&u, sizeof(u), 1, file) ? 0 : -1); } -int uiSave(const char *name) { - FILE *file = dataOpen(name, "w"); - if (!file) return -1; - - if (writeTime(file, Signatures[4])) return -1; - if (writeTime(file, self.pos)) return -1; - for (uint num = 0; num < windows.len; ++num) { - const struct Window *window = windows.ptrs[num]; - if (writeString(file, idNames[window->id])) return -1; - if (writeTime(file, window->mute)) return -1; - if (writeTime(file, window->heat)) return -1; - if (writeTime(file, window->unreadHard)) return -1; - if (writeTime(file, window->unreadWarm)) return -1; - for (size_t i = 0; i < BufferCap; ++i) { - struct Line line = bufferLine(&window->buffer, i); - if (!line.str) continue; - if (writeTime(file, line.time)) return -1; - if (writeTime(file, line.heat)) return -1; - if (writeString(file, line.str)) return -1; - } - if (writeTime(file, 0)) return -1; - } - return fclose(file); +int uiSave(void) { + return 0 + || ftruncate(fileno(saveFile), 0) + || writeUint64(saveFile, Signatures[8]) + || writeUint64(saveFile, self.pos) + || windowSave(saveFile) + || inputSave(saveFile) + || urlSave(saveFile) + || fclose(saveFile); } -static time_t readTime(FILE *file) { - time_t time; - fread(&time, sizeof(time), 1, file); +static uint64_t readUint64(FILE *file) { + uint64_t u; + fread(&u, sizeof(u), 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; + if (feof(file)) errx(EX_DATAERR, "unexpected end of save file"); + return u; } 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; + int error; + saveFile = dataOpen(name, "a+e"); + if (!saveFile) exit(EX_CANTCREAT); + rewind(saveFile); + +#ifdef __FreeBSD__ + cap_rights_t rights; + cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_FLOCK, CAP_FTRUNCATE); + error = caph_rights_limit(fileno(saveFile), &rights); + if (error) err(EX_OSERR, "cap_rights_limit"); +#endif + + error = flock(fileno(saveFile), LOCK_EX | LOCK_NB); + if (error && errno == EWOULDBLOCK) { + errx(EX_CANTCREAT, "%s: save file in use", name); } time_t signature; - fread(&signature, sizeof(signature), 1, file); - if (ferror(file)) err(EX_IOERR, "fread"); - if (feof(file)) { - fclose(file); + fread(&signature, sizeof(signature), 1, saveFile); + if (ferror(saveFile)) err(EX_IOERR, "fread"); + if (feof(saveFile)) { return; } size_t version = signatureVersion(signature); if (version > 1) { - self.pos = readTime(file); + self.pos = readUint64(saveFile); } - - char *buf = NULL; - size_t cap = 0; - while (0 < readString(file, &buf, &cap)) { - struct Window *window = windows.ptrs[windowFor(idFor(buf))]; - if (version > 3) window->mute = readTime(file); - if (version > 0) { - window->heat = readTime(file); - window->unreadHard = readTime(file); - window->unreadWarm = readTime(file); - } - for (;;) { - time_t time = readTime(file); - if (!time) break; - enum Heat heat = (version > 2 ? readTime(file) : Cold); - readString(file, &buf, &cap); - bufferPush(&window->buffer, heat, time, buf); - } - reflow(window); - } - - free(buf); - fclose(file); + windowLoad(saveFile, version); + inputLoad(saveFile, version); + urlLoad(saveFile, version); } diff --git a/url.c b/url.c index 3f7f512..7da0968 100644 --- a/url.c +++ b/url.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,6 +41,7 @@ static const char *Pattern = { "(" "cvs|" "ftp|" + "gemini|" "git|" "gopher|" "http|" @@ -86,14 +87,19 @@ static void push(uint 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) { url->nick = strdup(nick); if (!url->nick) err(EX_OSERR, "strdup"); } - url->url = strndup(str, len); - if (!url->url) err(EX_OSERR, "strndup"); + url->url = malloc(len + 1); + if (!url->url) err(EX_OSERR, "malloc"); + + char buf[1024]; + snprintf(buf, sizeof(buf), "%.*s", (int)len, str); + styleStrip(url->url, len + 1, buf); } void urlScan(uint id, const char *nick, const char *mesg) { @@ -117,6 +123,7 @@ static void urlOpen(const char *url) { if (pid < 0) err(EX_OSERR, "fork"); if (pid) return; + setsid(); close(STDIN_FILENO); dup2(utilPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); @@ -168,6 +175,7 @@ static void urlCopy(const char *url) { return; } + setsid(); dup2(rw[0], STDIN_FILENO); dup2(utilPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); @@ -225,3 +233,47 @@ void urlCopyMatch(uint id, const char *str) { } } } + +static int writeString(FILE *file, const char *str) { + return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1); +} +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; +} + +int urlSave(FILE *file) { + for (size_t i = 0; i < Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len + i) % Cap]; + if (!url->url) continue; + int error = 0 + || writeString(file, idNames[url->id]) + || writeString(file, (url->nick ?: "")) + || writeString(file, url->url); + if (error) return error; + } + return writeString(file, ""); +} + +void urlLoad(FILE *file, size_t version) { + if (version < 5) return; + size_t cap = 0; + char *buf = NULL; + while (0 < readString(file, &buf, &cap) && buf[0]) { + struct URL *url = &ring.urls[ring.len++ % Cap]; + free(url->nick); + free(url->url); + url->id = idFor(buf); + url->nick = NULL; + readString(file, &buf, &cap); + if (buf[0]) { + url->nick = strdup(buf); + if (!url->nick) err(EX_OSERR, "strdup"); + } + readString(file, &buf, &cap); + url->url = strdup(buf); + if (!url->url) err(EX_OSERR, "strdup"); + } + free(buf); +} diff --git a/window.c b/window.c new file mode 100644 index 0000000..f700fd7 --- /dev/null +++ b/window.c @@ -0,0 +1,659 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#define _XOPEN_SOURCE_EXTENDED + +#include <assert.h> +#include <curses.h> +#include <err.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> + +#include "chat.h" + +#define MAIN_LINES (LINES - StatusLines - InputLines) + +static struct Window { + uint id; + int scroll; + bool mark; + bool mute; + bool time; + enum Heat thresh; + enum Heat heat; + uint unreadSoft; + uint unreadHard; + uint unreadWarm; + struct Buffer *buffer; +} *windows[IDCap]; + +static uint count; +static uint show; +static uint swap; +static uint user; + +static uint windowPush(struct Window *window) { + assert(count < IDCap); + windows[count] = window; + return count++; +} + +static uint windowInsert(uint num, struct Window *window) { + assert(count < IDCap); + assert(num <= count); + memmove( + &windows[num + 1], + &windows[num], + sizeof(*windows) * (count - num) + ); + windows[num] = window; + count++; + return num; +} + +static struct Window *windowRemove(uint num) { + assert(num < count); + struct Window *window = windows[num]; + count--; + memmove( + &windows[num], + &windows[num + 1], + sizeof(*windows) * (count - num) + ); + return window; +} + +static void windowFree(struct Window *window) { + completeRemove(None, idNames[window->id]); + bufferFree(window->buffer); + free(window); +} + +enum Heat windowThreshold = Cold; +struct Time windowTime = { .format = "%X" }; + +uint windowFor(uint id) { + for (uint num = 0; num < count; ++num) { + if (windows[num]->id == id) return num; + } + + struct Window *window = calloc(1, sizeof(*window)); + if (!window) err(EX_OSERR, "malloc"); + + window->id = id; + window->mark = true; + window->time = windowTime.enable; + if (id == Network || id == Debug) { + window->thresh = Cold; + } else { + window->thresh = windowThreshold; + } + window->buffer = bufferAlloc(); + completePush(None, idNames[id], idColors[id]); + + return windowPush(window); +} + +enum { TimeCap = 64 }; + +void windowInit(void) { + char fmt[TimeCap]; + char buf[TimeCap]; + styleStrip(fmt, sizeof(fmt), windowTime.format); + + struct tm *time = localtime(&(time_t) { -22100400 }); + size_t len = strftime(buf, sizeof(buf), fmt, time); + if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", fmt); + + int y; + waddstr(uiMain, buf); + waddch(uiMain, ' '); + getyx(uiMain, y, windowTime.width); + (void)y; + + windowFor(Network); +} + +static int styleAdd(WINDOW *win, struct Style init, const char *str) { + struct Style style = init; + while (*str) { + size_t len = styleParse(&style, &str); + if (!len) continue; + wattr_set(win, uiAttr(style), uiPair(style), NULL); + if (waddnstr(win, str, len) == ERR) + return -1; + str += len; + } + return 0; +} + +static void statusUpdate(void) { + struct { + uint unread; + enum Heat heat; + } others = { 0, Cold }; + + wmove(uiStatus, 0, 0); + for (uint num = 0; num < count; ++num) { + const struct Window *window = windows[num]; + if (num != show && !window->scroll && !inputPending(window->id)) { + if (window->heat < Warm) continue; + if (window->mute && window->heat < Hot) continue; + } + if (num != show) { + others.unread += window->unreadWarm; + if (window->heat > others.heat) others.heat = window->heat; + } + char buf[256], *end = &buf[sizeof(buf)]; + char *ptr = seprintf( + buf, end, "\3%d%s %u%s%s %s ", + idColors[window->id], (num == show ? "\26" : ""), + num, window->thresh[(const char *[]) { "-", "", "+", "++" }], + &"="[!window->mute], idNames[window->id] + ); + if (window->mark && window->unreadWarm) { + ptr = seprintf( + ptr, end, "\3%d+%d\3%d ", + (window->heat > Warm ? White : idColors[window->id]), + window->unreadWarm, idColors[window->id] + ); + } + if (window->scroll) { + ptr = seprintf(ptr, end, "~%d ", window->scroll); + } + if (num != show && inputPending(window->id)) { + ptr = seprintf(ptr, end, "@ "); + } + if (styleAdd(uiStatus, StyleDefault, buf) < 0) break; + } + wclrtoeol(uiStatus); + + const struct Window *window = windows[show]; + char *end = &uiTitle[sizeof(uiTitle)]; + char *ptr = seprintf( + uiTitle, end, "%s %s", network.name, idNames[window->id] + ); + if (window->mark && window->unreadWarm) { + ptr = seprintf( + ptr, end, " +%d%s", window->unreadWarm, &"!"[window->heat < Hot] + ); + } + if (others.unread) { + ptr = seprintf( + ptr, end, " (+%d%s)", others.unread, &"!"[others.heat < Hot] + ); + } +} + +static size_t windowTop(const struct Window *window) { + size_t top = BufferCap - MAIN_LINES - window->scroll; + if (window->scroll) top += MarkerLines; + return top; +} + +static size_t windowBottom(const struct Window *window) { + size_t bottom = BufferCap - (window->scroll ?: 1); + if (window->scroll) bottom -= SplitLines + MarkerLines; + return bottom; +} + +static void mainAdd(int y, bool time, const struct Line *line) { + int ny, nx; + wmove(uiMain, y, 0); + if (!line || !line->str[0]) { + wclrtoeol(uiMain); + return; + } + if (time && line->time) { + char buf[TimeCap]; + strftime(buf, sizeof(buf), windowTime.format, localtime(&line->time)); + struct Style init = { .fg = Gray, .bg = Default }; + styleAdd(uiMain, init, buf); + waddch(uiMain, ' '); + } else if (time) { + whline(uiMain, ' ', windowTime.width); + wmove(uiMain, y, windowTime.width); + } + styleAdd(uiMain, StyleDefault, line->str); + getyx(uiMain, ny, nx); + if (ny != y) return; + wclrtoeol(uiMain); + (void)nx; +} + +static void mainUpdate(void) { + const struct Window *window = windows[show]; + + int y = 0; + int marker = MAIN_LINES - SplitLines - MarkerLines; + for (size_t i = windowTop(window); i < BufferCap; ++i) { + mainAdd(y++, window->time, bufferHard(window->buffer, i)); + if (window->scroll && y == marker) break; + } + if (!window->scroll) return; + + y = MAIN_LINES - SplitLines; + for (size_t i = BufferCap - SplitLines; i < BufferCap; ++i) { + mainAdd(y++, window->time, bufferHard(window->buffer, i)); + } + wattr_set(uiMain, A_NORMAL, 0, NULL); + mvwhline(uiMain, marker, 0, ACS_BULLET, COLS); +} + +void windowUpdate(void) { + statusUpdate(); + mainUpdate(); +} + +void windowBare(void) { + uiHide(); + inputWait(); + + const struct Window *window = windows[show]; + const struct Line *line = bufferHard(window->buffer, windowBottom(window)); + + uint num = 0; + if (line) num = line->num; + for (size_t i = 0; i < BufferCap; ++i) { + line = bufferSoft(window->buffer, i); + if (!line) continue; + if (line->num > num) break; + if (!line->str[0]) { + printf("\n"); + continue; + } + + char buf[TimeCap]; + struct Style style = { .fg = Gray, .bg = Default }; + strftime(buf, sizeof(buf), windowTime.format, localtime(&line->time)); + vid_attr(uiAttr(style), uiPair(style), NULL); + printf("%s ", buf); + + bool align = false; + style = StyleDefault; + for (const char *str = line->str; *str;) { + if (*str == '\t') { + printf("%c", (align ? '\t' : ' ')); + align = true; + str++; + } + + size_t len = styleParse(&style, &str); + size_t tab = strcspn(str, "\t"); + if (tab < len) len = tab; + + vid_attr(uiAttr(style), uiPair(style), NULL); + printf("%.*s", (int)len, str); + str += len; + } + printf("\n"); + } +} + +static void mark(struct Window *window) { + if (window->scroll) return; + window->mark = true; + window->unreadSoft = 0; + window->unreadWarm = 0; +} + +static void unmark(struct Window *window) { + if (!window->scroll) { + window->mark = false; + window->heat = Cold; + } + statusUpdate(); +} + +static void scrollN(struct Window *window, int n) { + mark(window); + window->scroll += n; + if (window->scroll > BufferCap - MAIN_LINES) { + window->scroll = BufferCap - MAIN_LINES; + } + if (window->scroll < 0) window->scroll = 0; + unmark(window); + if (window == windows[show]) mainUpdate(); +} + +static void scrollTo(struct Window *window, int top) { + window->scroll = 0; + scrollN(window, top - MAIN_LINES + MarkerLines); +} + +static int windowCols(const struct Window *window) { + return COLS - (window->time ? windowTime.width : 0); +} + +bool windowWrite(uint id, enum Heat heat, const time_t *src, const char *str) { + struct Window *window = windows[windowFor(id)]; + time_t ts = (src ? *src : time(NULL)); + + if (heat >= window->thresh) { + if (!window->unreadSoft++) window->unreadHard = 0; + } + if (window->mark && heat > Cold) { + if (!window->unreadWarm++) { + int lines = bufferPush( + window->buffer, windowCols(window), + window->thresh, Warm, ts, "" + ); + if (window->scroll) scrollN(window, lines); + if (window->unreadSoft > 1) { + window->unreadSoft++; + window->unreadHard += lines; + } + } + if (heat > window->heat) window->heat = heat; + statusUpdate(); + } + int lines = bufferPush( + window->buffer, windowCols(window), + window->thresh, heat, ts, str + ); + window->unreadHard += lines; + if (window->scroll) scrollN(window, lines); + if (window == windows[show]) mainUpdate(); + + return window->mark && heat > Warm; +} + +static void reflow(struct Window *window) { + uint num = 0; + const struct Line *line = bufferHard(window->buffer, windowTop(window)); + if (line) num = line->num; + window->unreadHard = bufferReflow( + window->buffer, windowCols(window), + window->thresh, window->unreadSoft + ); + if (!window->scroll || !num) return; + for (size_t i = 0; i < BufferCap; ++i) { + line = bufferHard(window->buffer, i); + if (!line || line->num != num) continue; + scrollTo(window, BufferCap - i); + break; + } +} + +void windowResize(void) { + for (uint num = 0; num < count; ++num) { + reflow(windows[num]); + } + windowUpdate(); +} + +uint windowID(void) { + return windows[show]->id; +} + +uint windowNum(void) { + return show; +} + +void windowShow(uint num) { + if (num >= count) return; + if (num != show) { + swap = show; + mark(windows[swap]); + } + show = num; + user = num; + unmark(windows[show]); + mainUpdate(); + inputUpdate(); +} + +void windowAuto(void) { + uint minHot = UINT_MAX, numHot = 0; + uint minWarm = UINT_MAX, numWarm = 0; + for (uint num = 0; num < count; ++num) { + struct Window *window = windows[num]; + if (window->heat >= Hot) { + if (window->unreadWarm >= minHot) continue; + minHot = window->unreadWarm; + numHot = num; + } + if (window->heat >= Warm && !window->mute) { + if (window->unreadWarm >= minWarm) continue; + minWarm = window->unreadWarm; + numWarm = num; + } + } + uint oldUser = user; + if (minHot < UINT_MAX) { + windowShow(numHot); + user = oldUser; + } else if (minWarm < UINT_MAX) { + windowShow(numWarm); + user = oldUser; + } else if (user != show) { + windowShow(user); + } +} + +void windowSwap(void) { + windowShow(swap); +} + +void windowMove(uint from, uint to) { + if (from >= count) return; + struct Window *window = windowRemove(from); + if (to < count) { + windowShow(windowInsert(to, window)); + } else { + windowShow(windowPush(window)); + } +} + +void windowClose(uint num) { + if (num >= count) return; + if (windows[num]->id == Network) return; + struct Window *window = windowRemove(num); + completeRemove(window->id, NULL); + windowFree(window); + if (swap >= num) swap--; + if (show == num) { + windowShow(swap); + swap = show; + } else if (show > num) { + show--; + mainUpdate(); + } + statusUpdate(); +} + +void windowList(void) { + for (uint num = 0; num < count; ++num) { + const struct Window *window = windows[num]; + uiFormat( + Network, Warm, NULL, "\3%02d%u %s", + idColors[window->id], num, idNames[window->id] + ); + } +} + +void windowMark(void) { + mark(windows[show]); +} + +void windowUnmark(void) { + unmark(windows[show]); +} + +void windowToggleMute(void) { + windows[show]->mute ^= true; + statusUpdate(); +} + +void windowToggleTime(void) { + windows[show]->time ^= true; + reflow(windows[show]); + windowUpdate(); + inputUpdate(); +} + +void windowToggleThresh(int n) { + struct Window *window = windows[show]; + if (n > 0 && window->thresh == Hot) return; + if (n < 0 && window->thresh == Ice) { + window->thresh = Cold; + } else { + window->thresh += n; + } + reflow(window); + windowUpdate(); +} + +bool windowTimeEnable(void) { + return windows[show]->time; +} + +void windowScroll(enum Scroll by, int n) { + struct Window *window = windows[show]; + switch (by) { + break; case ScrollOne: { + scrollN(window, n); + } + break; case ScrollPage: { + scrollN(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1)); + } + break; case ScrollAll: { + if (n < 0) { + scrollTo(window, 0); + break; + } + for (size_t i = 0; i < BufferCap; ++i) { + if (!bufferHard(window->buffer, i)) continue; + scrollTo(window, BufferCap - i); + break; + } + } + break; case ScrollUnread: { + scrollTo(window, window->unreadHard); + } + break; case ScrollHot: { + for (size_t i = windowTop(window) + n; i < BufferCap; i += n) { + const struct Line *line = bufferHard(window->buffer, i); + const struct Line *prev = bufferHard(window->buffer, i - 1); + if (!line || line->heat < Hot) continue; + if (prev && prev->heat > Warm) continue; + scrollTo(window, BufferCap - i); + break; + } + } + } +} + +void windowSearch(const char *str, int dir) { + struct Window *window = windows[show]; + for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) { + const struct Line *line = bufferHard(window->buffer, i); + if (!line || !strcasestr(line->str, str)) continue; + scrollTo(window, BufferCap - i); + break; + } +} + +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 windowSave(FILE *file) { + int error; + for (uint num = 0; num < count; ++num) { + const struct Window *window = windows[num]; + error = 0 + || writeString(file, idNames[window->id]) + || writeTime(file, window->mute) + || writeTime(file, window->time) + || writeTime(file, window->thresh) + || writeTime(file, window->heat) + || writeTime(file, window->unreadSoft) + || writeTime(file, window->unreadWarm); + if (error) return error; + for (size_t i = 0; i < BufferCap; ++i) { + const struct Line *line = bufferSoft(window->buffer, i); + if (!line) continue; + error = 0 + || writeTime(file, line->time) + || writeTime(file, line->heat) + || writeString(file, line->str); + if (error) return error; + } + error = writeTime(file, 0); + if (error) return error; + } + return writeString(file, ""); +} + +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 end of save file"); + 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 windowLoad(FILE *file, size_t version) { + size_t cap = 0; + char *buf = NULL; + while (0 < readString(file, &buf, &cap) && buf[0]) { + struct Window *window = windows[windowFor(idFor(buf))]; + if (version > 3) window->mute = readTime(file); + if (version > 6) window->time = readTime(file); + if (version > 5) window->thresh = readTime(file); + if (version > 0) { + window->heat = readTime(file); + window->unreadSoft = readTime(file); + window->unreadWarm = readTime(file); + } + for (;;) { + time_t time = readTime(file); + if (!time) break; + enum Heat heat = (version > 2 ? readTime(file) : Cold); + readString(file, &buf, &cap); + bufferPush(window->buffer, COLS, window->thresh, heat, time, buf); + } + reflow(window); + } + free(buf); +} diff --git a/xdg.c b/xdg.c index 2500bac..75ee871 100644 --- a/xdg.c +++ b/xdg.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2019, 2020 C. McEnroe <june@causal.agency> +/* Copyright (C) 2019, 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,126 +32,100 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sysexits.h> #include "chat.h" #define SUBDIR "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/" SUBDIR "/%s", configHome, path); - } else { - if (!home) goto local; - snprintf(buf, sizeof(buf), "%s/.config/" SUBDIR "/%s", home, path); +struct Base { + const char *envHome; + const char *envDirs; + const char *defHome; + const char *defDirs; +}; + +static const struct Base Config = { + .envHome = "XDG_CONFIG_HOME", + .envDirs = "XDG_CONFIG_DIRS", + .defHome = ".config", + .defDirs = "/etc/xdg", +}; + +static const struct Base Data = { + .envHome = "XDG_DATA_HOME", + .envDirs = "XDG_DATA_DIRS", + .defHome = ".local/share", + .defDirs = "/usr/local/share:/usr/share", +}; + +static char *basePath( + struct Base base, char *buf, size_t cap, const char *path, int i +) { + if (path[strspn(path, ".")] == '/') { + if (i > 0) return NULL; + snprintf(buf, cap, "%s", path); + return buf; } - FILE *file = fopen(buf, mode); - if (file) return file; - if (errno != ENOENT) warn("%s", buf); - if (!configDirs) configDirs = "/etc/xdg"; - while (*configDirs) { - size_t len = strcspn(configDirs, ":"); + if (i > 0) { + const char *dirs = getenv(base.envDirs); + if (!dirs) dirs = base.defDirs; + for (; i > 1; --i) { + dirs += strcspn(dirs, ":"); + dirs += (*dirs == ':'); + } + if (!*dirs) return NULL; snprintf( - buf, sizeof(buf), "%.*s/" SUBDIR "/%s", - (int)len, configDirs, path + buf, cap, "%.*s/" SUBDIR "/%s", + (int)strcspn(dirs, ":"), dirs, path ); - file = fopen(buf, mode); - if (file) return file; - if (errno != ENOENT) warn("%s", buf); - configDirs += len; - if (*configDirs) configDirs++; + return buf; } -local: - file = fopen(path, mode); - if (!file) warn("%s", path); - return file; + const char *home = getenv("HOME"); + const char *baseHome = getenv(base.envHome); + if (baseHome) { + snprintf(buf, cap, "%s/" SUBDIR "/%s", baseHome, path); + } else if (home) { + snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path); + } else { + errx(EX_USAGE, "HOME unset"); + } + return buf; } -FILE *dataOpen(const char *path, const char *mode) { - if (path[0] == '/' || path[0] == '.') goto local; +char *configPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Config, buf, cap, path, i); +} - const char *home = getenv("HOME"); - const char *dataHome = getenv("XDG_DATA_HOME"); - const char *dataDirs = getenv("XDG_DATA_DIRS"); +char *dataPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Data, buf, cap, path, i); +} - char homePath[PATH_MAX]; - if (dataHome) { - snprintf( - homePath, sizeof(homePath), - "%s/" SUBDIR "/%s", dataHome, path - ); - } else { - if (!home) goto local; - snprintf( - homePath, sizeof(homePath), - "%s/.local/share/" SUBDIR "/%s", home, path - ); +FILE *configOpen(const char *path, const char *mode) { + char buf[PATH_MAX]; + for (int i = 0; configPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) warn("%s", buf); } - FILE *file = fopen(homePath, mode); - if (file) return file; - if (errno != ENOENT) warn("%s", homePath); + warn("%s", configPath(buf, sizeof(buf), path, 0)); + return NULL; +} +FILE *dataOpen(const char *path, const char *mode) { char buf[PATH_MAX]; - if (!dataDirs) dataDirs = "/usr/local/share:/usr/share"; - while (*dataDirs) { - size_t len = strcspn(dataDirs, ":"); - snprintf( - buf, sizeof(buf), "%.*s/" SUBDIR "/%s", - (int)len, dataDirs, path - ); - file = fopen(buf, mode); + for (int i = 0; dataPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); if (file) return file; if (errno != ENOENT) warn("%s", buf); - 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; + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) warn("%s", buf); } - -local: - file = fopen(path, mode); - if (!file) warn("%s", path); + FILE *file = fopen(dataPath(buf, sizeof(buf), path, 0), mode); + if (!file) warn("%s", buf); return file; } - -void dataMkdir(const char *path) { - const char *home = getenv("HOME"); - const char *dataHome = getenv("XDG_DATA_HOME"); - - char homePath[PATH_MAX]; - if (dataHome) { - snprintf( - homePath, sizeof(homePath), - "%s/" SUBDIR "/%s", dataHome, path - ); - } else { - if (!home) return; - snprintf( - homePath, sizeof(homePath), - "%s/.local/share/" SUBDIR "/%s", home, path - ); - } - - int error = mkdir(homePath, S_IRWXU); - if (error && errno != EEXIST) warn("%s", homePath); -} |