diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 95 | ||||
-rw-r--r-- | README.7 | 97 | ||||
-rw-r--r-- | buffer.c | 20 | ||||
-rw-r--r-- | catgirl.1 | 488 | ||||
-rw-r--r-- | chat.c | 283 | ||||
-rw-r--r-- | chat.h | 209 | ||||
-rw-r--r-- | command.c | 304 | ||||
-rw-r--r-- | compat_readpassphrase.c | 206 | ||||
-rw-r--r-- | complete.c | 163 | ||||
-rw-r--r-- | config.c | 2 | ||||
-rwxr-xr-x | configure | 27 | ||||
-rw-r--r-- | edit.c | 442 | ||||
-rw-r--r-- | edit.h | 74 | ||||
-rw-r--r-- | filter.c | 31 | ||||
-rw-r--r-- | handle.c | 465 | ||||
-rw-r--r-- | input.c | 629 | ||||
-rw-r--r-- | irc.c | 72 | ||||
-rw-r--r-- | log.c | 92 | ||||
-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 | 59 | ||||
-rw-r--r-- | scripts/chroot-prompt.sh | 3 | ||||
-rw-r--r-- | ui.c | 949 | ||||
-rw-r--r-- | url.c | 7 | ||||
-rw-r--r-- | window.c | 659 | ||||
-rw-r--r-- | xdg.c | 92 |
29 files changed, 3616 insertions, 1933 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 5caa3ba..66fb408 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,21 @@ 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 @@ -15,80 +24,64 @@ OBJS += config.o OBJS += edit.o OBJS += filter.o OBJS += handle.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 a67ec77..a26d270 100644 --- a/README.7 +++ b/README.7 @@ -1,7 +1,7 @@ -.Dd January 25, 2021 +.\" 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 @@ -45,6 +53,17 @@ is highlighted. 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 @@ -98,32 +117,32 @@ provided by either .Lk https://git.causal.agency/libretls/about LibreTLS (for OpenSSL) or by LibreSSL. -. -.Pp It targets .Fx , .Ox , macOS and Linux. -On -.Ox , -.Xr pledge 2 -is used to limit system operations, -and with -.Nm Fl R , -.Xr unveil 2 -is used to limit filesystem access. -On BSD systems, -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 @@ -144,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 @@ -154,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 @@ -170,10 +189,14 @@ 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 @@ -190,6 +213,8 @@ chat logging configuration parsing .It Pa xdg.c XDG base directories +.It Pa sandman.m +sleep/wake wrapper for macOS .El . .Pp @@ -199,11 +224,14 @@ example .Xr tmux 1 configuration for multiple networks and automatic reconnects -.It Pa scripts/sandman.m -sleep/wake wrapper for macOS .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 @@ -221,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 index 47d0955..f82e553 100644 --- a/buffer.c +++ b/buffer.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 @@ -82,17 +82,18 @@ const struct Line *bufferHard(const struct Buffer *buffer, size_t i) { } enum { StyleCap = 10 }; -static void styleCat(struct Cat *cat, struct Style style) { - catf( - cat, "%s%s%s%s", +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) { - catf(cat, "\3%02d,%02d", style.fg, style.bg); + ptr = seprintf(ptr, end, "\3%02d,%02d", style.fg, style.bg); } + return ptr; } static const wchar_t ZWS = L'\u200B'; @@ -186,12 +187,11 @@ static int flow(struct Lines *hard, int cols, const struct Line *soft) { line->str = malloc(cap); if (!line->str) err(EX_OSERR, "malloc"); - struct Cat cat = { line->str, cap, 0 }; - catf(&cat, "%*s", (width = align), ""); - styleCat(&cat, wrapStyle); - str = &line->str[cat.len]; + char *end = &line->str[cap]; + str = seprintf(line->str, end, "%*s", (width = align), ""); + str = styleCopy(str, end, wrapStyle); style = wrapStyle; - catf(&cat, "%s", &wrap[n]); + seprintf(str, end, "%s", &wrap[n]); *wrap = '\0'; wrap = NULL; diff --git a/catgirl.1 b/catgirl.1 index b09d55e..815eade 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd January 25, 2021 +.Dd October 11, 2023 .Dt CATGIRL 1 .Os . @@ -8,19 +8,21 @@ . .Sh SYNOPSIS .Nm -.Op Fl KRelv +.Op Fl Relqv .Op Fl C Ar copy .Op Fl H Ar hash .Op Fl I Ar highlight .Op Fl N Ar notify .Op Fl O Ar open .Op Fl S Ar bind +.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 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 @@ -67,11 +69,21 @@ 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 \&./ or .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 # @@ -79,30 +91,37 @@ 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 init,bound , Cm hash = Ar init,bound -Set the initial value of -the nick color hash function -and the maximum IRC color value used. +.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 I Ar pattern , Cm highlight = Ar pattern +.It Fl I Ar pattern | Cm highlight No = Ar pattern Add a case-insensitive message highlight pattern, which may contain .Ql * , @@ -110,17 +129,13 @@ which may contain 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 Oo Ar !user@host -.Oo Ar command -.Oo Ar channel -.Oo Ar message -.Oc Oc Oc Oc +.Ar nick Ns Op Ar !user@host Op Ar command Op Ar channel Op Ar message .Ed .Pp -The commands which can be filtered are: +The commands which can be matched are: .Sy INVITE , .Sy JOIN , .Sy NICK , @@ -130,40 +145,34 @@ The commands which can be filtered are: .Sy QUIT , .Sy SETNAME . . -.It Fl K , Cm kiosk -Disable the -.Ic /copy , -.Ic /debug , -.Ic /exec , -.Ic /join , -.Ic /list , -.Ic /msg , -.Ic /open , -.Ic /part , -.Ic /query , -.Ic /quote -commands. -. -.It Fl N Ar util , Cm notify = Ar util +.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 /exec @@ -171,28 +180,43 @@ and .Ic /open commands, the -.Fl N +.Cm notify option, and viewing this manual with .Ic /help . . -.It Fl S Ar host , Cm bind = Ar host +.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 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 = Ar user : Ns Ar pass +.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 @@ -201,19 +225,19 @@ 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 @@ -222,11 +246,11 @@ 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 +.It Fl i Ar pattern | Cm ignore No = Ar pattern Add a case-insensitive message ignore pattern, which may contain .Ql * , @@ -234,17 +258,13 @@ which may contain 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 Oo Ar !user@host -.Oo Ar command -.Oo Ar channel -.Oo Ar message -.Oc Oc Oc Oc +.Ar nick Ns Op Ar !user@host Op Ar command Op Ar channel Op Ar message .Ed .Pp -The commands which can be filtered are: +The commands which can be matched are: .Sy INVITE , .Sy JOIN , .Sy NICK , @@ -254,11 +274,13 @@ The commands which can be filtered are: .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 @@ -266,31 +288,48 @@ The 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 +.It Fl s Ar name | Cm save No = Ar name Save and load the contents of windows from .Ar name in @@ -303,10 +342,11 @@ starts with or .Ql \&../ . . -.It Fl t Ar path , Cm trust = Ar path -Trust the certificate loaded from -.Ar path . -Server name verification is disabled. +.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 @@ -314,21 +354,24 @@ as configuration files. See .Sx Connecting to Servers with Self-signed Certificates . . -.It Fl u Ar user , Cm user = Ar user +.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 @@ -336,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 @@ -350,16 +393,17 @@ 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 . @@ -369,7 +413,7 @@ sasl-external 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 +$ catgirl -o -h irc.example.org > ~/.config/catgirl/example.pem .Ed .It Configure @@ -377,10 +421,108 @@ Configure to trust the certificate: .Bd -literal -offset indent trust = example.pem -# or: catgirl -t 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, @@ -396,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 @@ -422,10 +566,9 @@ 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 @@ -436,8 +579,8 @@ 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 @@ -477,15 +620,22 @@ 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 -.Fl I . +To permanently add a pattern, +use the +.Cm highlight +option. .It Ic /ignore Op Ar pattern 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 @@ -499,9 +649,12 @@ or matching Temporarily remove a message highlight pattern. .It Ic /unignore Ar pattern Temporarily remove a message ignore pattern. -.It Ic /window Ar name -Switch to window by name. -.It Ic /window Ar num , Ic / Ns Ar num +.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 . @@ -589,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 @@ -607,11 +763,13 @@ Scroll down a page. .It Ic M-+ Raise message visibility threshold, hiding ignored messages, -general events, +general events +(joins, quits, etc.), or non-highlighted messages. .It Ic M-- Lower message visibility threshold, -showing ignored messages. +showing ignored messages +and unknown replies. .It Ic M-= Toggle mute. Muted windows do not appear in the status line @@ -640,6 +798,10 @@ Insert a blank line in the window. 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 @@ -647,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 @@ -656,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 @@ -689,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 @@ -735,26 +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 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 Create a configuration file in -.Pa ~/.config/catgirl/freenode : +.Pa ~/.config/catgirl/tilde : .Bd -literal -offset indent -host = chat.freenode.net +host = irc.tilde.chat join = #ascii.town .Ed .Pp Load the configuration file: .Bd -literal -offset indent -catgirl freenode +$ catgirl tilde .Ed . .Sh STANDARDS @@ -763,13 +940,15 @@ catgirl freenode .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 @@ -781,16 +960,16 @@ catgirl freenode .%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 Kiyoshi Aman -.%T IRCv3.1 extended-join Extension +.%T extended-join Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/extended-join-3.1 +.%U https://ircv3.net/specs/extensions/extended-join .Re .It .Rs @@ -804,9 +983,11 @@ catgirl freenode .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 @@ -814,19 +995,22 @@ catgirl freenode .%A St\('ephan Kochen .%A Alexey Sokolov .%A James Wheare -.%T IRCv3.2 server-time Extension +.%T server-time Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/server-time-3.2 +.%U https://ircv3.net/specs/extensions/server-time .Re .It .Rs .%A Lee Hardy .%A Perry Lorier .%A Kevin L. Mitchell +.%A Attila Molnar +.%A Daniel Oakley .%A William Pitcock -.%T IRCv3.1 Client Capability Negotiation +.%A James Wheare +.%T Client Capability Negotiation .%I IRCv3 Working Group -.%U https://ircv3.net/specs/core/capability-negotiation-3.1.html +.%U https://ircv3.net/specs/core/capability-negotiation .Re .It .Rs @@ -849,30 +1033,30 @@ catgirl freenode .It .Rs .%A Janne Mareike Koschinski -.%T IRCv3 setname Extension +.%T setname Extension .%I IRCv3 Working Group .%U https://ircv3.net/specs/extensions/setname .Re .It .Rs .%A Mantas Mikul\[u0117]nas -.%T IRCv3.2 userhost-in-names Extension +.%T userhost-in-names Extension .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/userhost-in-names-3.2 +.%U https://ircv3.net/specs/extensions/userhost-in-names .Re .It .Rs .%A Daniel Oaks -.%T Standard Replies Extension -.%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/standard-replies +.%T IRC Formatting +.%I ircdocs +.%U https://modern.ircdocs.horse/formatting.html .Re .It .Rs .%A Daniel Oaks -.%T IRC Formatting -.%I ircdocs -.%U https://modern.ircdocs.horse/formatting.html +.%T Standard Replies Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/standard-replies .Re .It .Rs @@ -890,14 +1074,28 @@ catgirl freenode .%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/message-ids +.Re +.It +.Rs +.%A James Wheare +.%T reply Client Tag .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/multi-prefix-3.1 +.%U https://ircv3.net/specs/client-tags/reply .Re .It .Rs @@ -913,17 +1111,17 @@ catgirl freenode .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 @@ -931,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 6458925..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,11 +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 @@ -81,7 +90,7 @@ 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); @@ -124,50 +133,94 @@ static void parseHash(char *str) { if (*str) hashBound = strtoul(&str[1], NULL, 0); } -#ifdef __OpenBSD__ +static void parsePlain(char *str) { + self.plainUser = strsep(&str, ":"); + if (!str) errx(EX_USAGE, "SASL PLAIN missing colon"); + self.plainPass = str; +} -static void unveilConfig(const char *name) { - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, name));) { - int error = unveil(path, "r"); - if (error && errno != ENOENT) err(EX_NOINPUT, "%s", path); - } +static volatile sig_atomic_t signals[NSIG]; +static void signalHandler(int signal) { + signals[signal] = 1; } -static void unveilData(const char *name) { - const char *dirs = NULL; - for (const char *path; NULL != (path = dataPath(&dirs, name));) { - int error = unveil(path, "rwc"); - if (error && errno != ENOENT) err(EX_CANTCREAT, "%s", path); +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"); } -} -static void unveilAll(const char *trust, const char *cert, const char *priv) { - dataMkdir(""); - unveilData(""); - if (trust) unveilConfig(trust); - if (cert) unveilConfig(cert); - if (priv) unveilConfig(priv); - if (save) unveilData(save); - struct { - const char *path; - const char *perm; - } paths[] = { - { "/usr/share/terminfo", "r" }, - { tls_default_ca_cert_file(), "r" }, - }; - for (size_t i = 0; i < ARRAY_LEN(paths); ++i) { - int error = unveil(paths[i].path, paths[i].perm); - if (error) err(EX_OSFILE, "%s", paths[i].path); + 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"); } -#endif /* __OpenBSD__ */ +static void sandboxLate(int irc) { + (void)irc; + *promisesInitial = '\0'; + int error = pledge(promises, NULL); + if (error) err(EX_OSERR, "pledge"); +} -static volatile sig_atomic_t signals[NSIG]; -static void signalHandler(int signal) { - signals[signal] = 1; +#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, ""); @@ -181,9 +234,9 @@ int main(int argc, char *argv[]) { 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; @@ -192,11 +245,11 @@ int main(int argc, char *argv[]) { { .val = 'C', .name = "copy", required_argument }, { .val = 'H', .name = "hash", required_argument }, { .val = 'I', .name = "highlight", required_argument }, - { .val = 'K', .name = "kiosk", no_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 }, @@ -206,9 +259,11 @@ 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 }, @@ -217,10 +272,11 @@ int main(int argc, char *argv[]) { { .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));) { @@ -229,12 +285,15 @@ int main(int argc, char *argv[]) { break; case 'C': utilPush(&urlCopyUtil, optarg); break; case 'H': parseHash(optarg); break; case 'I': filterAdd(Hot, optarg); - break; case 'K': self.kiosk = true; 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); @@ -242,10 +301,16 @@ int main(int argc, char *argv[]) { 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 'o': insecure = true; printCert = true; + 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; @@ -257,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, "#&"); @@ -274,29 +365,17 @@ int main(int argc, char *argv[]) { set(&network.name, host); set(&self.nick, "*"); - editCompleteAdd(); - commandCompleteAdd(); - -#ifdef __OpenBSD__ - if (self.restricted) unveilAll(trust, cert, priv); - int error = pledge("stdio rpath wpath cpath inet dns tty proc exec", NULL); - if (error) err(EX_OSERR, "pledge"); -#endif + inputCompletion(); ircConfig(insecure, trust, cert, priv); - if (printCert) { - ircConnect(bind, host, port); - ircPrintCert(); - ircClose(); - return EX_OK; - } - uiInitEarly(); + 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^ " @@ -305,33 +384,33 @@ int main(int argc, char *argv[]) { ); uiFormat(Network, Cold, NULL, "Traveling..."); uiDraw(); - - int irc = ircConnect(bind, host, port); -#ifdef __OpenBSD__ - error = pledge("stdio rpath wpath cpath tty proc exec", NULL); - if (error) err(EX_OSERR, "pledge"); -#endif - if (pass) ircFormat("PASS :%s\r\n", pass); + sandboxEarly(log); + int irc = ircConnect(bind, host, port); + 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); - uiInitLate(); + // 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); - bool pipes = !self.kiosk && !self.restricted; - if (pipes) { - int error = pipe(utilPipe); - if (error) err(EX_OSERR, "pipe"); - error = pipe(execPipe); + if (!self.restricted) { + int error = pipe(utilPipe) || pipe(execPipe); if (error) err(EX_OSERR, "pipe"); fcntl(utilPipe[0], F_SETFD, FD_CLOEXEC); @@ -340,15 +419,7 @@ int main(int argc, char *argv[]) { fcntl(execPipe[1], F_SETFD, FD_CLOEXEC); } -#ifdef __OpenBSD__ - char promises[64] = "stdio tty"; - struct Cat cat = { promises, sizeof(promises), strlen(promises) }; - if (save || logEnable) catf(&cat, " rpath wpath cpath"); - if (!self.restricted) catf(&cat, " proc exec"); - error = pledge(promises, NULL); - if (error) err(EX_OSERR, "pledge"); -#endif - + bool ping = false; struct pollfd fds[] = { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, @@ -356,10 +427,10 @@ int main(int argc, char *argv[]) { { .events = POLLIN, .fd = execPipe[0] }, }; while (!self.quit) { - int nfds = poll(fds, (pipes ? ARRAY_LEN(fds) : 2), -1); + 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(); @@ -368,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);) { @@ -390,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 6ecd91a..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 @@ -34,6 +34,7 @@ #include <stdint.h> #include <stdio.h> #include <string.h> +#include <strings.h> #include <sysexits.h> #include <time.h> #include <wchar.h> @@ -44,20 +45,16 @@ 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 { @@ -105,11 +102,13 @@ static inline size_t styleParse(struct Style *style, const char **str) { return strcspn(*str, (const char[]) { B, C, O, R, I, U, '\0' }); } -static inline void styleStrip(struct Cat *cat, const char *str) { +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); - catf(cat, "%.*s", (int)len, str); + ptr = seprintf(ptr, end, "%.*s", (int)len, str); str += len; } } @@ -121,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; } @@ -138,7 +137,7 @@ static inline uint idFor(const char *name) { extern uint32_t hashInit; extern uint32_t hashBound; -static inline enum Color hash(const char *str) { +static inline uint32_t _hash(const char *str) { if (*str == '~') str++; uint32_t hash = hashInit; for (; *str; ++str) { @@ -146,7 +145,11 @@ static inline enum Color hash(const char *str) { hash ^= *str; hash *= 0x27220A95; } - return Blue + hash % (hashBound + 1 - Blue); + 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 { @@ -154,6 +157,7 @@ extern struct Network { uint userLen; uint hostLen; char *chanTypes; + char *statusmsg; char *prefixes; char *prefixModes; char *listModes; @@ -164,16 +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("setname", CapSetname) \ - X("userhost-in-names", CapUserhostInNames) + X("userhost-in-names", CapUserhostInNames) \ + X("znc.in/self-message", CapSelfMessage) enum Cap { #define X(name, id) BIT(id), @@ -183,16 +202,19 @@ enum Cap { extern struct Self { bool debug; - bool kiosk; 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; @@ -203,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 { @@ -227,6 +251,7 @@ 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); @@ -262,8 +287,9 @@ enum Reply { ReplyList, ReplyMode, ReplyNames, + ReplyNamesAuto, ReplyTopic, - ReplyWho, + ReplyTopicAuto, ReplyWhois, ReplyWhowas, ReplyCap, @@ -276,27 +302,90 @@ 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 uiInitEarly(void); -void uiInitLate(void); +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); +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; @@ -318,41 +407,22 @@ int bufferReflow( struct Buffer *buffer, int cols, enum Heat thresh, size_t tail ); -enum Edit { - EditHead, - EditTail, - EditPrev, - EditNext, - EditPrevWord, - EditNextWord, - EditDeleteHead, - EditDeleteTail, - EditDeletePrev, - EditDeleteNext, - EditDeletePrevWord, - EditDeleteNextWord, - EditPaste, - EditTranspose, - EditCollapse, - EditInsert, - EditComplete, - EditExpand, - EditEnter, +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; @@ -376,16 +446,15 @@ 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); -const char *configPath(const char **dirs, const char *path); -const char *dataPath(const char **dirs, const char *path); +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 b1b4af4..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,16 +118,38 @@ 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) { @@ -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) { @@ -182,8 +219,31 @@ static void commandNames(uint id, char *params) { static void commandOps(uint id, char *params) { (void)params; - ircFormat("WHO %s\r\n", idNames[id]); - replies[ReplyWho]++; + 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) { @@ -212,6 +272,12 @@ static void commandMode(uint id, char *params) { } } 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]); @@ -233,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]); } } @@ -245,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]); } } @@ -307,7 +373,7 @@ static void commandList(uint id, char *params) { static void commandWhois(uint id, char *params) { (void)id; - if (!params) return; + if (!params) params = self.nick; uint count = 1; for (char *ch = params; *ch; ++ch) { if (*ch == ',') count++; @@ -325,28 +391,42 @@ static void commandWhowas(uint id, char *params) { 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; + } } } @@ -355,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)); } } @@ -439,6 +519,7 @@ 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); @@ -468,7 +549,7 @@ static void commandHelp(uint id, char *params) { if (pid) return; char buf[256]; - snprintf(buf, sizeof(buf), "%spCOMMANDS$", (getenv("LESS") ?: "")); + snprintf(buf, sizeof(buf), "%sp^COMMANDS$", (getenv("LESS") ?: "")); setenv("LESS", buf, 1); execlp("man", "man", "1", "catgirl", NULL); dup2(utilPipe[1], STDERR_FILENO); @@ -479,60 +560,60 @@ static void commandHelp(uint id, char *params) { enum Flag { BIT(Multiline), BIT(Restrict), - BIT(Kiosk), }; 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, Restrict | Kiosk }, - { "/cs", commandCS, 0 }, - { "/debug", commandDebug, Kiosk }, - { "/deop", commandDeop, 0 }, - { "/devoice", commandDevoice, 0 }, - { "/except", commandExcept, 0 }, - { "/exec", commandExec, Multiline | Restrict }, - { "/help", commandHelp, 0 }, // Restrict special case. - { "/highlight", commandHighlight, 0 }, - { "/ignore", commandIgnore, 0 }, - { "/invex", commandInvex, 0 }, - { "/invite", commandInvite, 0 }, - { "/join", commandJoin, Kiosk }, - { "/kick", commandKick, 0 }, - { "/list", commandList, Kiosk }, - { "/me", commandMe, 0 }, - { "/mode", commandMode, 0 }, - { "/move", commandMove, 0 }, - { "/msg", commandMsg, Multiline | Kiosk }, - { "/names", commandNames, 0 }, - { "/nick", commandNick, 0 }, - { "/notice", commandNotice, Multiline }, - { "/ns", commandNS, 0 }, - { "/o", commandOpen, Restrict | Kiosk }, - { "/op", commandOp, 0 }, - { "/open", commandOpen, Restrict | Kiosk }, - { "/ops", commandOps, 0 }, - { "/part", commandPart, Kiosk }, - { "/query", commandQuery, Kiosk }, - { "/quit", commandQuit, 0 }, - { "/quote", commandQuote, Multiline | Kiosk }, - { "/say", commandPrivmsg, Multiline }, - { "/setname", commandSetname, 0 }, - { "/topic", commandTopic, 0 }, - { "/unban", commandUnban, 0 }, - { "/unexcept", commandUnexcept, 0 }, - { "/unhighlight", commandUnhighlight, 0 }, - { "/unignore", commandUnignore, 0 }, - { "/uninvex", commandUninvex, 0 }, - { "/voice", commandVoice, 0 }, - { "/whois", commandWhois, 0 }, - { "/whowas", commandWhowas, 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) { @@ -561,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); @@ -575,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( @@ -589,10 +705,7 @@ void command(uint id, char *input) { uiFormat(id, Warm, NULL, "No such command %s", cmd); return; } - if ( - (self.restricted && handler->flags & Restrict) || - (self.kiosk && handler->flags & Kiosk) - ) { + if (!commandAvailable(handler)) { uiFormat(id, Warm, NULL, "Command %s is unavailable", cmd); return; } @@ -609,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 d6eaa45..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 diff --git a/configure b/configure index 02bf093..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 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 d9b7673..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; - wmemmove(&buf[index + count], &buf[index], len - index); - len += count; - return true; -} - -static void delete(bool copy, size_t index, size_t count) { - if (index + count > len) return; - if (copy) { - wmemcpy(cut.buf, &buf[index], count); - cut.len = count; +int editReserve(struct Edit *e, size_t index, size_t count) { + if (index > e->len) { + errno = EINVAL; + return -1; } - wmemmove(&buf[index], &buf[index + count], 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; - } - - bool colon = (tab.len >= 2 && buf[tab.pos + tab.len - 2] == L':'); - - 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 || colon)) { - 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' '; - } - wmemcpy(&buf[tab.pos], 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)) { - wmemcpy(&buf[pos], cut.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; +} + +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> - if (pos < init) { - tabReject(); - } else { - tabAccept(); +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 index a648d8b..a7f9a29 100644 --- a/filter.c +++ b/filter.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 @@ -94,12 +94,39 @@ static bool filterTest( 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)) return filters[i].heat; + 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 eae5451..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 @@ -63,13 +63,16 @@ static enum Cap capParse(const char *list) { return caps; } -static const char *capList(struct Cat *cat, enum Cap caps) { +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", (cat->len ? " " : ""), CapNames[i]); + ptr = seprintf( + ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i] + ); } } - return cat->buf; } static void require(struct Message *msg, bool origin, uint len) { @@ -88,7 +91,7 @@ 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; } @@ -124,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); } @@ -152,16 +178,18 @@ static void handleCap(struct Message *msg) { caps &= ~CapConsumer; } if (caps) { - char buf[512] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; - ircFormat("CAP REQ :%s\r\n", capList(&cat, 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")) { @@ -200,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) { @@ -237,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[ReplyJoin] += count; - replies[ReplyTopic] += count; - replies[ReplyNames] += 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); @@ -270,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], ")"); @@ -332,13 +372,13 @@ static void handleJoin(struct Message *msg) { set(&self.host, msg->host); } idColors[id] = hash(msg->params[0]); - completeTouch(None, msg->params[0], idColors[id]); + completePull(None, msg->params[0], idColors[id]); if (replies[ReplyJoin]) { - uiShowID(id); + 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; } @@ -348,7 +388,7 @@ static void handleJoin(struct Message *msg) { 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]); @@ -370,7 +410,7 @@ 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 = filterCheck(Cold, id, msg); @@ -392,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), @@ -409,16 +449,17 @@ 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]); } @@ -433,15 +474,16 @@ 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); - for (uint id; (id = completeID(msg->nick));) { + 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)", + "\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] ); @@ -450,7 +492,8 @@ static void handleSetname(struct Message *msg) { static void handleQuit(struct Message *msg) { require(msg, true, 0); - for (uint id; (id = completeID(msg->nick));) { + 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( @@ -472,6 +515,7 @@ 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, filterCheck(Hot, Network, msg), tagTime(msg), "\3%02d%s\3\tinvites you to \3%02d%s\3", @@ -495,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, @@ -507,7 +550,7 @@ 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, Warm, tagTime(msg), @@ -520,53 +563,40 @@ 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[ReplyNames]) 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 (!cat.len) return; + if (ptr == buf) return; uiFormat( - id, Warm, tagTime(msg), + id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg), "In \3%02d%s\3 are %s", hash(msg->params[2]), msg->params[2], buf ); } -static char whoBuf[1024]; -static struct Cat whoCat = { whoBuf, sizeof(whoBuf), 0 }; - -static void handleReplyWho(struct Message *msg) { - require(msg, false, 7); - if (!whoCat.len) { - catf( - &whoCat, "The operators of \3%02d%s\3 are ", - hash(msg->params[1]), msg->params[1] - ); +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies[ReplyNamesAuto]) { + replies[ReplyNamesAuto]--; + } else if (replies[ReplyNames]) { + replies[ReplyNames]--; } - char *prefixes = &msg->params[6][1]; - if (prefixes[0] == '*') prefixes++; - prefixes[strspn(prefixes, network.prefixes)] = '\0'; - if (!prefixes[0] || prefixes[0] == '+') return; - catf( - &whoCat, "%s\3%02d%s%s\3%s", - (whoCat.buf[whoCat.len - 1] == ' ' ? "" : ", "), - hash(msg->params[2]), prefixes, msg->params[5], - (msg->params[6][0] == 'H' ? "" : " (away)") - ); -} - -static void handleReplyEndOfWho(struct Message *msg) { - require(msg, false, 2); - uiWrite(idFor(msg->params[1]), Warm, tagTime(msg), whoBuf); - whoCat.len = 0; } static void handleReplyNoTopic(struct Message *msg) { @@ -580,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); } } @@ -595,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[ReplyTopic]) return; - replies[ReplyTopic]--; + if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return; urlScan(id, NULL, msg->params[2]); uiFormat( - id, Warm, 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] ); @@ -607,6 +637,11 @@ 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) { @@ -615,6 +650,28 @@ static void swap(wchar_t *a, wchar_t *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]); @@ -632,10 +689,8 @@ static void handleTopic(struct Message *msg) { return; } - char buf[1024]; - struct Cat cat = { buf, sizeof(buf), 0 }; - const char *prev = complete(id, "/topic "); - completeReject(); + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); if (prev) { prev += 7; } else { @@ -649,38 +704,44 @@ static void handleTopic(struct Message *msg) { size_t pre; for (pre = 0; old[pre] && new[pre] && old[pre] == new[pre]; ++pre); - wchar_t *osuf = &old[wcslen(old)]; - wchar_t *nsuf = &new[wcslen(new)]; - while (osuf > &old[pre] && nsuf > &new[pre] && osuf[-1] == nsuf[-1]) { + size_t osuf = wcslen(old); + size_t nsuf = wcslen(new); + while (osuf > pre && nsuf > pre && old[osuf-1] == new[nsuf-1]) { osuf--; nsuf--; } - wchar_t nul = L'\0'; - swap(&new[pre], &nul); - catf(&cat, "%ls", new); - swap(&new[pre], &nul); - swap(osuf, &nul); - catf(&cat, "\399,%02d%ls", Brown, &old[pre]); - swap(osuf, &nul); - swap(nsuf, &nul); - catf(&cat, "\399,%02d%ls", Green, &new[pre]); - swap(nsuf, &nul); - catf(&cat, "\399,99%ls", 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: - 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], - (cat.len ? cat.buf : msg->params[1]) + 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] = { @@ -693,17 +754,19 @@ static const char *UserModes[256] = { static void handleReplyUserModeIs(struct Message *msg) { require(msg, false, 2); - 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") ); } @@ -725,8 +788,8 @@ static const char *ChanModes[256] = { static void handleReplyChannelModeIs(struct Message *msg) { require(msg, false, 3); 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]; @@ -735,14 +798,14 @@ 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 ?: "") ); } @@ -751,7 +814,7 @@ static void handleReplyChannelModeIs(struct Message *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") ); } @@ -800,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", @@ -986,22 +1055,22 @@ static void handleReplyInviteList(struct Message *msg) { } static void handleReplyList(struct Message *msg) { - require(msg, false, 4); + 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 handleReplyWhoisUser(struct Message *msg) { require(msg, false, 6); - 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] ); @@ -1045,12 +1114,16 @@ static void handleReplyWhoisIdle(struct Message *msg) { static void handleReplyWhoisChannels(struct Message *msg) { require(msg, false, 3); - 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), @@ -1083,7 +1156,7 @@ static void handleReplyEndOfWhois(struct Message *msg) { static void handleReplyWhowasUser(struct Message *msg) { require(msg, false, 6); - 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\twas %s!%s@%s (%s)", @@ -1102,14 +1175,9 @@ static void handleReplyEndOfWhowas(struct Message *msg) { 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] ); @@ -1125,18 +1193,26 @@ static void handleReplyNowAway(struct Message *msg) { } 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 = strstr(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; @@ -1146,43 +1222,54 @@ static bool isMention(const struct Message *msg) { return false; } -static void colorMentions(struct Cat *cat, 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'; - - 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); + +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); @@ -1197,45 +1284,50 @@ static void handlePrivmsg(struct Message *msg) { } bool notice = (msg->cmd[0] == 'N'); - bool action = isAction(msg); + bool action = !notice && isAction(msg); bool highlight = !mine && isMention(msg); - enum Heat heat = filterCheck((highlight || query ? Hot : Warm), id, 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) { - completeTouch(id, msg->nick, hash(msg->user)); + completePull(id, msg->nick, hash(msg->user)); } if (heat > Ice) urlScan(id, msg->nick, msg->params[1]); - char buf[1024] = ""; - struct Cat cat = { buf, sizeof(buf), 0 }; + 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, filterCheck(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]); - colorMentions(&cat, id, msg); - uiFormat( - id, heat, tagTime(msg), - "%s\35\3%d* %s\17\35\t%s%s", - (highlight ? "\26" : ""), hash(msg->user), msg->nick, - buf, 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]); - colorMentions(&cat, id, msg); - uiFormat( - id, heat, tagTime(msg), - "%s\3%d<%s>\17\t%s%s", - (highlight ? "\26" : ""), hash(msg->user), msg->nick, - buf, 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) { @@ -1265,24 +1357,25 @@ static const struct Handler { { "312", 0, handleReplyWhoisServer }, { "313", +ReplyWhois, handleReplyWhoisGeneric }, { "314", +ReplyWhowas, handleReplyWhowasUser }, - { "315", -ReplyWho, handleReplyEndOfWho }, { "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 }, - { "352", +ReplyWho, handleReplyWho }, { "353", 0, handleReplyNames }, - { "366", -ReplyNames, NULL }, + { "366", 0, handleReplyEndOfNames }, { "367", +ReplyBan, handleReplyBanList }, { "368", -ReplyBan, NULL }, { "369", -ReplyWhowas, handleReplyEndOfWhowas }, @@ -1345,5 +1438,7 @@ void handle(struct Message *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/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 c98193a..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,21 +44,17 @@ #include "chat.h" -struct tls *client; +static struct tls *client; +static struct tls_config *config; void ircConfig( bool insecure, const char *trust, const char *cert, const char *priv ) { - struct tls_config *config = tls_config_new(); - if (!config) errx(EX_SOFTWARE, "tls_config_new"); + int error = 0; + char buf[PATH_MAX]; - int error = tls_config_set_ciphers(config, "compat"); - if (error) { - errx( - EX_SOFTWARE, "tls_config_set_ciphers: %s", - tls_config_error(config) - ); - } + config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); if (insecure) { tls_config_insecure_noverifycert(config); @@ -63,30 +62,38 @@ void ircConfig( } if (trust) { tls_config_insecure_noverifyname(config); - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, trust));) { - error = tls_config_set_ca_file(config, path); + 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) { - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, cert));) { + for (int i = 0; configPath(buf, sizeof(buf), cert, i); ++i) { if (priv) { - error = tls_config_set_cert_file(config, path); + error = tls_config_set_cert_file(config, buf); } else { - error = tls_config_set_keypair_file(config, path, path); + error = tls_config_set_keypair_file(config, buf, buf); } if (!error) break; } if (error) errx(EX_NOINPUT, "%s: %s", cert, tls_config_error(config)); } if (priv) { - const char *dirs = NULL; - for (const char *path; NULL != (path = configPath(&dirs, priv));) { - error = tls_config_set_key_file(config, path); + for (int i = 0; configPath(buf, sizeof(buf), priv, i); ++i) { + error = tls_config_set_key_file(config, buf); if (!error) break; } if (error) errx(EX_NOINPUT, "%s: %s", priv, tls_config_error(config)); @@ -97,7 +104,6 @@ void ircConfig( 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) { @@ -144,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; @@ -151,17 +158,26 @@ 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); @@ -234,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 11edb52..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); 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 index 9191b1a..3489a19 100644 --- a/scripts/chat.tmux.conf +++ b/scripts/chat.tmux.conf @@ -1,31 +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 +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 +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 +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 +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 -n efnet -- catgirl efnet -new-window -n freenode -- catgirl freenode -new-window -n hackint -- catgirl hackint +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 5d3f070..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,123 +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, - 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 *main; -static WINDOW *input; - -struct Window { - uint id; - int scroll; - bool mark; - bool mute; - enum Heat thresh; - enum Heat heat; - uint unreadSoft; - uint unreadHard; - 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->mark = true; - window->thresh = Cold; - window->buffer = bufferAlloc(); - - return windowPush(window); -} - -static void windowFree(struct Window *window) { - bufferFree(window->buffer); - free(window); -} +WINDOW *uiStatus; +WINDOW *uiMain; +WINDOW *uiInput; static short colorPairs; @@ -179,127 +91,62 @@ 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(KeyMetaN, "\33n", NULL) \ - X(KeyMetaP, "\33p", 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=", 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) - -enum { - KeyMax = KEY_MAX, -#define X(id, seq, alt) id, - ENUM_KEY -#undef X -}; - // 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(); } -void uiInitEarly(void) { +void uiInit(void) { initscr(); cbreak(); noecho(); 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 + uiStatus = newwin(StatusLines, COLS, 0, 0); + if (!uiStatus) err(EX_OSERR, "newwin"); - status = newwin(StatusLines, COLS, 0, 0); - if (!status) err(EX_OSERR, "newwin"); + uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); + if (!uiMain) err(EX_OSERR, "newwin"); - main = newwin(MAIN_LINES, COLS, StatusLines, 0); - if (!main) err(EX_OSERR, "newwin"); + uiInput = newpad(InputLines, InputCols); + if (!uiInput) err(EX_OSERR, "newpad"); - input = newpad(InputLines, InputCols); - if (!input) err(EX_OSERR, "newpad"); - keypad(input, true); - nodelay(input, true); - - windowFor(Network); + windowInit(); uiShow(); } -// Avoid disabling VINTR until main loop. -void uiInitLate(void) { - 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(); -} - 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); - wnoutrefresh(main); + 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 @@ -308,10 +155,10 @@ 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); } @@ -343,7 +190,7 @@ static const short Colors[ColorCap] = { 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, }; -static attr_t styleAttr(struct Style style) { +uint uiAttr(struct Style style) { attr_t attr = A_NORMAL; if (style.attr & Bold) attr |= A_BOLD; if (style.attr & Reverse) attr |= A_REVERSE; @@ -352,168 +199,41 @@ static attr_t styleAttr(struct Style style) { return attr | colorAttr(Colors[style.fg]); } -static short stylePair(struct Style style) { - return colorPair(Colors[style.fg], Colors[style.bg]); -} +bool uiSpoilerReveal; -static void styleAdd(WINDOW *win, const char *str) { - struct Style style = StyleDefault; - while (*str) { - size_t len = styleParse(&style, &str); - wattr_set(win, styleAttr(style), stylePair(style), NULL); - waddnstr(win, str, len); - str += len; +short uiPair(struct Style style) { + if (uiSpoilerReveal && style.fg == style.bg) { + return colorPair(Colors[Default], Colors[style.bg]); } -} - -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->thresh != Cold || window->mute) { - const char *thresh[] = { "-", "", "+", "++" }; - catf(&cat, "%s%s ", thresh[window->thresh], &"="[!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); - } - styleAdd(status, 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]); - } -} - -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(); + 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 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, const char *str) { - int ny, nx; - wmove(main, y, 0); - styleAdd(main, str); - getyx(main, ny, nx); - if (ny == y) wclrtoeol(main); - (void)nx; -} - -static void mainUpdate(void) { - struct Window *window = windows.ptrs[windows.show]; - - int y = 0; - int marker = MAIN_LINES - SplitLines - MarkerLines; - for (size_t i = windowTop(window); i < BufferCap; ++i) { - const struct Line *line = bufferHard(window->buffer, i); - mainAdd(y++, (line ? line->str : "")); - if (window->scroll && y == marker) break; - } - if (!window->scroll) return; - - y = MAIN_LINES - SplitLines; - for (size_t i = BufferCap - SplitLines; i < BufferCap; ++i) { - const struct Line *line = bufferHard(window->buffer, i); - mainAdd(y++, (line ? line->str : "")); - } - wattr_set(main, A_NORMAL, 0, NULL); - mvwhline(main, marker, 0, ACS_BULLET, COLS); -} - -static void windowScroll(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.ptrs[windows.show]) mainUpdate(); -} - struct Util uiNotifyUtil; static void notify(uint id, const char *str) { if (self.restricted) return; if (!uiNotifyUtil.argc) return; - char buf[1024] = ""; - styleStrip(&(struct Cat) { buf, sizeof(buf), 0 }, str); + char buf[1024]; + styleStrip(buf, sizeof(buf), str); struct Util util = uiNotifyUtil; utilPush(&util, idNames[id]); @@ -523,6 +243,7 @@ static void notify(uint id, const char *str) { if (pid < 0) err(EX_OSERR, "fork"); if (pid) return; + setsid(); close(STDIN_FILENO); dup2(utilPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); @@ -532,30 +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)); - - if (heat >= window->thresh) { - if (!window->unreadSoft++) window->unreadHard = 0; - } - if (window->mark && heat > Cold) { - if (!window->unreadWarm++) { - int lines = bufferPush(window->buffer, COLS, false, Warm, ts, ""); - if (window->scroll) windowScroll(window, lines); - if (window->unreadSoft > 1) { - window->unreadSoft++; - window->unreadHard += lines; - } - } - if (heat > window->heat) window->heat = heat; - statusUpdate(); - } - int lines = bufferPush(window->buffer, COLS, window->thresh, heat, ts, str); - window->unreadHard += lines; - if (window->scroll) windowScroll(window, lines); - if (window == windows.ptrs[windows.show]) mainUpdate(); - - if (window->mark && heat > Warm) { + bool note = windowWrite(id, heat, src, str); + if (note) { beep(); notify(id, str); } @@ -573,522 +272,86 @@ void uiFormat( uiWrite(id, heat, time, buf); } -static void scrollTo(struct Window *window, int top) { - window->scroll = 0; - windowScroll(window, top - MAIN_LINES + MarkerLines); -} - -static void windowReflow(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, COLS, 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; - } -} - -static void resize(void) { - statusUpdate(); - wclear(main); - wresize(main, MAIN_LINES, COLS); - for (uint num = 0; num < windows.len; ++num) { - windowReflow(windows.ptrs[num]); - } - mainUpdate(); +void uiResize(void) { + wclear(uiMain); + wresize(uiMain, MAIN_LINES, COLS); + windowResize(); } -static void windowList(const struct Window *window) { - uiHide(); - waiting = true; - - uint num = 0; - const struct Line *line = bufferHard(window->buffer, windowBottom(window)); - 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; - } +static FILE *saveFile; - 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 = 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(styleAttr(style), stylePair(style), NULL); - printf("%.*s", (int)len, str); - 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, styleAttr(*style), stylePair(*style), 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 = StyleDefault; - - 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(input, 0, 0); - wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), 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) { - windows.user = num; - if (windows.show == num) return; - windows.swap = windows.show; - windows.show = num; - mark(windows.ptrs[windows.swap]); - unmark(windows.ptrs[windows.show]); - mainUpdate(); - 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--; - mainUpdate(); - } - statusUpdate(); -} - -void uiCloseID(uint id) { - windowClose(windowFor(id)); -} - -void uiCloseNum(uint num) { - if (num < windows.len) windowClose(num); -} - -static void scrollPage(struct Window *window, int n) { - windowScroll(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1)); -} - -static void scrollHot(struct Window *window, int dir) { - for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) { - 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; - } -} - -static void scrollSearch(struct Window *window, const char *str, int dir) { - 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 void incThresh(struct Window *window, int n) { - if (n > 0 && window->thresh == Hot) return; - if (n < 0 && window->thresh == Ice) { - window->thresh = Cold; - } else { - window->thresh += n; - } - windowReflow(window); - mainUpdate(); - 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: incThresh(window, -1); - break; case KeyMetaPlus: incThresh(window, +1); - break; case KeyMetaSlash: windowShow(windows.swap); - - break; case KeyMetaGt: scrollTo(window, 0); - break; case KeyMetaLt: scrollTo(window, BufferCap); - - 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: windowList(window); - break; case KeyMetaM: uiWrite(id, Warm, NULL, ""); - break; case KeyMetaN: scrollHot(window, +1); - break; case KeyMetaP: scrollHot(window, -1); - break; case KeyMetaQ: edit(id, EditCollapse, 0); - break; case KeyMetaU: scrollTo(window, window->unreadHard); - break; case KeyMetaV: scrollPage(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: scrollPage(window, -1); - break; case KEY_PPAGE: scrollPage(window, +1); - break; case KEY_RIGHT: edit(id, EditNext, 0); - break; case KEY_SEND: scrollTo(window, 0); - break; case KEY_SHOME: scrollTo(window, BufferCap); - break; case KEY_UP: windowScroll(window, +1); - } -} - -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'R': scrollSearch(window, editBuffer(NULL), -1); - break; case L'S': scrollSearch(window, editBuffer(NULL), +1); - break; case L'T': edit(id, EditTranspose, 0); - break; case L'U': edit(id, EditDeleteHead, 0); - break; case L'V': scrollPage(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); - } -} - -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, // no URLs 0x6C72696774616306, // no thresh - 0x6C72696774616307, + 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; - - int error = 0 - || writeTime(file, Signatures[6]) - || writeTime(file, self.pos); - if (error) return error; - for (uint num = 0; num < windows.len; ++num) { - const struct Window *window = windows.ptrs[num]; - error = 0 - || writeString(file, idNames[window->id]) - || writeTime(file, window->mute) - || 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; - } +int uiSave(void) { return 0 - || writeString(file, "") - || urlSave(file) - || fclose(file); -} - -static time_t readTime(FILE *file) { - time_t time; - fread(&time, sizeof(time), 1, file); + || ftruncate(fileno(saveFile), 0) + || writeUint64(saveFile, Signatures[8]) + || writeUint64(saveFile, self.pos) + || windowSave(saveFile) + || inputSave(saveFile) + || urlSave(saveFile) + || fclose(saveFile); +} + +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) && buf[0]) { - struct Window *window = windows.ptrs[windowFor(idFor(buf))]; - if (version > 3) window->mute = 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); - } - windowReflow(window); - } - urlLoad(file, version); - - free(buf); - fclose(file); + windowLoad(saveFile, version); + inputLoad(saveFile, version); + urlLoad(saveFile, version); } diff --git a/url.c b/url.c index 21f946c..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|" @@ -98,7 +99,7 @@ static void push(uint id, const char *nick, const char *str, size_t len) { char buf[1024]; snprintf(buf, sizeof(buf), "%.*s", (int)len, str); - styleStrip(&(struct Cat) { url->url, len + 1, 0 }, buf); + styleStrip(url->url, len + 1, buf); } void urlScan(uint id, const char *nick, const char *mesg) { @@ -122,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); @@ -173,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); 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 b206427..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,6 +32,7 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sysexits.h> #include "chat.h" @@ -58,80 +59,73 @@ static const struct Base Data = { .defDirs = "/usr/local/share:/usr/share", }; -static const char * -basePath(struct Base base, const char **dirs, const char *path) { - static char buf[PATH_MAX]; - - if (*dirs) { - if (!**dirs) return NULL; - size_t len = strcspn(*dirs, ":"); - snprintf(buf, sizeof(buf), "%.*s/" SUBDIR "/%s", (int)len, *dirs, path); - *dirs += len; - if (**dirs) *dirs += 1; +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; } - if (path[strspn(path, ".")] == '/') { - *dirs = ""; - return path; + 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, cap, "%.*s/" SUBDIR "/%s", + (int)strcspn(dirs, ":"), dirs, path + ); + return buf; } - *dirs = getenv(base.envDirs); - if (!*dirs) *dirs = base.defDirs; - const char *home = getenv("HOME"); const char *baseHome = getenv(base.envHome); if (baseHome) { - snprintf(buf, sizeof(buf), "%s/" SUBDIR "/%s", baseHome, path); + snprintf(buf, cap, "%s/" SUBDIR "/%s", baseHome, path); } else if (home) { - snprintf( - buf, sizeof(buf), "%s/%s/" SUBDIR "/%s", - home, base.defHome, path - ); + snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path); } else { - errx(EX_CONFIG, "HOME unset"); + errx(EX_USAGE, "HOME unset"); } return buf; } -const char *configPath(const char **dirs, const char *path) { - return basePath(Config, dirs, path); +char *configPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Config, buf, cap, path, i); } -const char *dataPath(const char **dirs, const char *path) { - return basePath(Data, dirs, path); +char *dataPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Data, buf, cap, path, i); } FILE *configOpen(const char *path, const char *mode) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = configPath(&dirs, path));) { - FILE *file = fopen(abs, 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", abs); + if (errno != ENOENT) warn("%s", buf); } - dirs = NULL; - warn("%s", configPath(&dirs, path)); + warn("%s", configPath(buf, sizeof(buf), path, 0)); return NULL; } -void dataMkdir(const char *path) { - const char *dirs = NULL; - path = dataPath(&dirs, path); - int error = mkdir(path, S_IRWXU); - if (error && errno != EEXIST) warn("%s", path); -} - FILE *dataOpen(const char *path, const char *mode) { - const char *dirs = NULL; - for (const char *abs; NULL != (abs = dataPath(&dirs, path));) { - FILE *file = fopen(abs, mode); + char buf[PATH_MAX]; + 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", abs); + if (errno != ENOENT) warn("%s", buf); + } + if (mode[0] != 'r') { + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) warn("%s", buf); } - if (mode[0] != 'r') dataMkdir(""); - dirs = NULL; - path = dataPath(&dirs, path); - FILE *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; } |