diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 52 | ||||
-rw-r--r-- | README.7 | 49 | ||||
-rw-r--r-- | buffer.c | 2 | ||||
-rw-r--r-- | catgirl.1 | 279 | ||||
-rw-r--r-- | chat.c | 73 | ||||
-rw-r--r-- | chat.h | 161 | ||||
-rw-r--r-- | command.c | 186 | ||||
-rw-r--r-- | compat_readpassphrase.c | 206 | ||||
-rw-r--r-- | complete.c | 172 | ||||
-rw-r--r-- | config.c | 2 | ||||
-rwxr-xr-x | configure | 12 | ||||
-rw-r--r-- | edit.c | 454 | ||||
-rw-r--r-- | edit.h | 74 | ||||
-rw-r--r-- | filter.c | 2 | ||||
-rw-r--r-- | handle.c | 350 | ||||
-rw-r--r-- | input.c | 629 | ||||
-rw-r--r-- | irc.c | 2 | ||||
-rw-r--r-- | log.c | 32 | ||||
-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-- | ui.c | 1007 | ||||
-rw-r--r-- | url.c | 2 | ||||
-rw-r--r-- | window.c | 659 | ||||
-rw-r--r-- | xdg.c | 2 |
26 files changed, 2710 insertions, 1704 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 48fc350..66fb408 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,18 @@ BINDIR ?= ${PREFIX}/bin MANDIR ?= ${PREFIX}/man CEXTS = gnu-case-range gnu-conditional-omitted-operand -CFLAGS += -std=c11 -Wall -Wextra -Wpedantic ${CEXTS:%=-Wno-%} +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 @@ -19,45 +24,54 @@ 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 + +dev: tags all check -all: catgirl +all: ${BINS} catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ ${OBJS}: chat.h +edit.o edit.t input.o: edit.h + +sandman: ${OBJS.sandman} + ${CC} ${LDFLAGS} ${OBJS.$@} ${LDLIBS.$@} -o $@ + +check: ${TESTS} + +.SUFFIXES: .t + +.c.t: + ${CC} ${CFLAGS} -DTEST ${LDFLAGS} $< ${LDLIBS} -o $@ + ./$@ || rm $@ + tags: *.[ch] ctags -w *.[ch] clean: - rm -f catgirl ${OBJS} tags + rm -f ${BINS} ${OBJS} ${OBJS.sandman} ${TESTS} tags -install: catgirl catgirl.1 +install: ${BINS} ${MANS} install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 - install catgirl ${DESTDIR}${BINDIR} - install -m 644 catgirl.1 ${DESTDIR}${MANDIR}/man1 + install ${BINS} ${DESTDIR}${BINDIR} + install -m 644 ${MANS} ${DESTDIR}${MANDIR}/man1 uninstall: - rm -f ${DESTDIR}${BINDIR}/catgirl ${DESTDIR}${MANDIR}/man1/catgirl.1 - -scripts/sandman: scripts/sandman.o - ${CC} ${LDFLAGS} scripts/sandman.o -framework Cocoa -o $@ - -install-sandman: scripts/sandman scripts/sandman.1 - install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 - install scripts/sandman ${DESTDIR}${BINDIR} - install -m 644 scripts/sandman.1 ${DESTDIR}${MANDIR}/man1 - -uninstall-sandman: - rm -f ${DESTDIR}${BINDIR}/sandman ${DESTDIR}${MANDIR}/man1/sandman.1 + rm -f ${BINS:%=${DESTDIR}${BINDIR}/%} + rm -f ${MANS:%=${DESTDIR}${MANDIR}/man1/%} CHROOT_USER = chat CHROOT_GROUP = ${CHROOT_USER} diff --git a/README.7 b/README.7 index c717aa9..a26d270 100644 --- a/README.7 +++ b/README.7 @@ -1,5 +1,5 @@ -.\" To view this file, run: man ./README.7 -.Dd June 28, 2021 +.\" To view this file: $ man ./README.7 +.Dd July 9, 2023 .Dt README 7 .Os "Causal Agency" . @@ -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 @@ -124,15 +132,15 @@ 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 edit.c . +.Pa input.c . . .Pp If installing @@ -155,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 @@ -165,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 @@ -181,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 @@ -201,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 @@ -210,8 +224,6 @@ 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 @@ -245,17 +257,18 @@ 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 b87816e..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 diff --git a/catgirl.1 b/catgirl.1 index bb425cc..815eade 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd July 20, 2021 +.Dd October 11, 2023 .Dt CATGIRL 1 .Os . @@ -8,7 +8,7 @@ . .Sh SYNOPSIS .Nm -.Op Fl KRelqv +.Op Fl Relqv .Op Fl C Ar copy .Op Fl H Ar hash .Op Fl I Ar highlight @@ -135,7 +135,7 @@ The format of the pattern is as follows: .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 , @@ -145,22 +145,6 @@ 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. -Replace the username -with a hash of its original value. -. .It Fl N Ar util | Cm notify No = Ar util Send notifications using a utility. Subsequent @@ -196,7 +180,7 @@ and .Ic /open commands, the -.Fl N +.Cm notify option, and viewing this manual with .Ic /help . @@ -205,12 +189,20 @@ and viewing this manual with 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 . . @@ -220,11 +212,9 @@ Authenticate as with .Ar pass using SASL PLAIN. -Since this requires the account password -in plain text, -it is recommended to use CertFP instead. -See -.Sx Configuring CertFP . +Leave +.Ar pass +blank to prompt for the password. . .It Fl c Ar path | Cm cert No = Ar path Load the TLS client certificate from @@ -235,9 +225,9 @@ 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 . @@ -246,7 +236,7 @@ Certificates can be generated with Authenticate using SASL EXTERNAL, also known as CertFP. The TLS client certificate is loaded with -.Fl c . +.Cm cert . See .Sx Configuring CertFP . . @@ -274,7 +264,7 @@ The format of the pattern is as follows: .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 , @@ -306,12 +296,17 @@ Log chat events to files in paths Set the user .Ar mode . . -.It Fl n Ar nick | Cm nick No = Ar nick +.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 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 @@ -326,7 +321,8 @@ The default port is 6697. .It Fl q | Cm quiet Raise the default message visibility threshold for new windows, -hiding general events. +hiding general events +(joins, quits, etc.). . .It Fl r Ar real | Cm real No = Ar real Set realname to @@ -373,6 +369,9 @@ if it is not a terminal. .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 @@ -380,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 @@ -404,7 +403,7 @@ when connecting .Bd -literal -offset indent cert = example.pem sasl-external -# or: catgirl -e -c example.pem +# or: $ catgirl -e -c example.pem .Ed .El . @@ -414,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 @@ -422,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, @@ -469,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 @@ -524,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 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 @@ -660,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 @@ -693,6 +798,8 @@ 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 @@ -717,6 +824,8 @@ Reset formatting. 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 @@ -760,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 @@ -813,7 +919,7 @@ Join .Li #ascii.town from the command line: .Bd -literal -offset indent -catgirl -h irc.tilde.chat -j '#ascii.town' +$ catgirl -h irc.tilde.chat -j '#ascii.town' .Ed .Pp Create a configuration file in @@ -825,7 +931,7 @@ join = #ascii.town .Pp Load the configuration file: .Bd -literal -offset indent -catgirl tilde +$ catgirl tilde .Ed . .Sh STANDARDS @@ -834,13 +940,15 @@ catgirl tilde .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 @@ -852,16 +960,16 @@ catgirl tilde .%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 @@ -875,9 +983,11 @@ catgirl tilde .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 @@ -885,19 +995,22 @@ catgirl tilde .%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 @@ -920,30 +1033,30 @@ catgirl tilde .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 @@ -961,28 +1074,28 @@ catgirl tilde .%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-3.1 +.%U https://ircv3.net/specs/extensions/multi-prefix .Re .It .Rs .%A James Wheare -.%T Reply Client Tag +.%T Message IDs .%I IRCv3 Working Group -.%U https://ircv3.net/specs/client-tags/reply +.%U https://ircv3.net/specs/extensions/message-ids .Re .It .Rs .%A James Wheare -.%T Message IDs +.%T reply Client Tag .%I IRCv3 Working Group -.%U https://ircv3.net/specs/extensions/message-ids +.%U https://ircv3.net/specs/client-tags/reply .Re .It .Rs @@ -1008,7 +1121,7 @@ The consumer position is stored in the file. . .Sh AUTHORS -.An June Bug Aq Mt june@causal.agency +.An June McEnroe Aq Mt june@causal.agency . .Sh BUGS Send mail to diff --git a/chat.c b/chat.c index ba6c9a1..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 @@ -50,6 +50,8 @@ #include <capsicum_helpers.h> #endif +char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags); + #include "chat.h" #ifndef OPENSSL_BIN @@ -131,6 +133,12 @@ static void parseHash(char *str) { if (*str) hashBound = strtoul(&str[1], NULL, 0); } +static void parsePlain(char *str) { + self.plainUser = strsep(&str, ":"); + if (!str) errx(EX_USAGE, "SASL PLAIN missing colon"); + self.plainPass = str; +} + static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { signals[signal] = 1; @@ -229,7 +237,6 @@ int main(int argc, char *argv[]) { bool log = false; bool sasl = false; char *pass = NULL; - const char *nick = NULL; const char *user = NULL; const char *real = NULL; @@ -238,7 +245,6 @@ 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 }, @@ -279,16 +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 'T': { - uiTime.enable = true; - if (optarg) uiTime.format = optarg; + windowTime.enable = true; + if (optarg) windowTime.format = optarg; } - break; case 'a': sasl = true; self.plain = optarg; + break; case 'a': sasl = true; parsePlain(optarg); break; case 'c': cert = optarg; break; case 'e': sasl = true; break; case 'g': genCert(optarg); @@ -298,10 +303,14 @@ int main(int argc, char *argv[]) { break; case 'k': priv = optarg; break; case 'l': log = true; logOpen(); break; case 'm': self.mode = optarg; - break; case 'n': nick = 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': uiThreshold = Warm; + break; case 'q': windowThreshold = Warm; break; case 'r': real = optarg; break; case 's': save = optarg; break; case 't': trust = optarg; @@ -325,16 +334,23 @@ int main(int argc, char *argv[]) { return EX_OK; } - if (!nick) nick = getenv("USER"); - if (!nick) errx(EX_CONFIG, "USER unset"); - if (!user) user = nick; - if (!real) real = nick; + 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.kiosk) { - char *hash; - int n = asprintf(&hash, "%08" PRIx32, _hash(user)); - if (n < 0) err(EX_OSERR, "asprintf"); - user = hash; + 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: @@ -349,18 +365,17 @@ int main(int argc, char *argv[]) { set(&network.name, host); set(&self.nick, "*"); - editCompleteAdd(); - commandCompleteAdd(); + inputCompletion(); ircConfig(insecure, trust, cert, priv); - 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^ " @@ -383,18 +398,18 @@ int main(int argc, char *argv[]) { } 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); - bool pipes = !self.kiosk && !self.restricted; - if (pipes) { + if (!self.restricted) { int error = pipe(utilPipe) || pipe(execPipe); if (error) err(EX_OSERR, "pipe"); @@ -412,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(); @@ -467,7 +482,7 @@ int main(int argc, char *argv[]) { cursesWinch(SIGWINCH); // 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 19d2b18..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 @@ -168,6 +168,19 @@ 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) \ @@ -189,13 +202,14 @@ enum Cap { extern struct Self { bool debug; - bool kiosk; bool restricted; size_t pos; enum Cap caps; - char *plain; - char *mode; - char *join; + const char *plainUser; + char *plainPass; + const char *nicks[8]; + const char *mode; + const char *join; char *nick; char *user; char *host; @@ -276,7 +290,6 @@ enum Reply { ReplyNamesAuto, ReplyTopic, ReplyTopicAuto, - ReplyWho, ReplyWhois, ReplyWhowas, ReplyCap, @@ -290,29 +303,36 @@ const char *commandIsPrivmsg(uint id, const char *input); const char *commandIsNotice(uint id, const char *input); const char *commandIsAction(uint id, const char *input); size_t commandWillSplit(uint id, const char *input); -void commandCompleteAdd(void); +void commandCompletion(void); -enum Heat { Ice, Cold, Warm, Hot }; -enum { TimeCap = 64 }; -extern enum Heat uiThreshold; -extern struct Time { - bool enable; - const char *format; - int width; -} uiTime; +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 uiWindows(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, ... @@ -320,6 +340,53 @@ void uiFormat( void uiLoad(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; struct Line { @@ -340,42 +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); -const char *completeSubstr(uint id, const char *substr); -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; diff --git a/command.c b/command.c index 335c396..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 @@ -144,7 +144,7 @@ static void commandMsg(uint id, char *params) { if (params) { splitMessage("PRIVMSG", msg, params); } else { - uiShowID(msg); + windowShow(windowFor(msg)); } } @@ -176,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) { @@ -213,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) { @@ -376,25 +405,26 @@ static void commandQuery(uint id, char *params) { if (idColors[query] == Default) { idColors[query] = completeColor(id, params); } - uiShowID(query); + windowShow(windowFor(query)); } static void commandWindow(uint id, char *params) { if (!params) { - uiWindows(); + windowList(); } else if (isdigit(params[0])) { - uiShowNum(strtoul(params, NULL, 10)); + windowShow(strtoul(params, NULL, 10)); } else { id = idFind(params); if (id) { - uiShowID(id); + windowShow(windowFor(id)); return; } - for (const char *match; (match = completeSubstr(None, params));) { - id = idFind(match); + struct Cursor curs = {0}; + for (const char *str; (str = completeSubstr(&curs, None, params));) { + id = idFind(str); if (!id) continue; - completeAccept(); - uiShowID(id); + completeAccept(&curs); + windowShow(windowFor(id)); break; } } @@ -405,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)); } } @@ -530,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 | Kiosk }, - { "/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, Multiline }, - { "/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) { @@ -639,6 +669,14 @@ size_t commandWillSplit(uint id, const char *input) { 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); @@ -653,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( @@ -667,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; } @@ -687,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 9e59db5..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,76 +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; - } - return NULL; -} - -const char *completeSubstr(uint id, const char *substr) { - for (match = (match ? match->next : head); match; match = match->next) { - if (match->id && match->id != id) continue; - if (!strstr(match->str, substr)) 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)); } } @@ -164,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 4199980..07e3245 100755 --- a/configure +++ b/configure @@ -1,6 +1,8 @@ #!/bin/sh set -eu +: ${PKG_CONFIG:=pkg-config} + cflags() { echo "CFLAGS += $*" } @@ -8,16 +10,16 @@ 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 + ${PKG_CONFIG} --print-errors "$@" + cflags $(${PKG_CONFIG} --cflags "$@") + for lib; do ldadd $lib $(${PKG_CONFIG} --libs $lib); done } exec >config.mk @@ -27,6 +29,7 @@ for opt; do (--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 @@ -43,6 +46,7 @@ case "$(uname)" in 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 71c5cca..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,276 +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"\\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 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 && buf[macro] != L'\\') 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; - if (wcstombs(NULL, Macros[i].string, 0) == (size_t)-1) 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; - bool suffix; -} 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; - tab.suffix = true; - } - - 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); - tab.suffix ^= true; +int editDelete(struct Edit *e, bool cut, size_t index, size_t count) { + if (index + count > e->len) { + errno = EINVAL; + return -1; } - if (!comp) { - tab.len = 0; - return; - } - - 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.suffix && (!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.suffix && 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); - if (!tab.suffix && tab.pos >= 2 && buf[tab.pos - 2] == L',') { - buf[tab.pos - 2] = L':'; - } - 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: { - char mb[MB_LEN_MAX]; - if (wctomb(mb, ch) < 0) return; - 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; +} - if (pos < init) { - tabReject(); - } else { - tabAccept(); +#ifdef TEST +#undef NDEBUG +#include <assert.h> +#include <string.h> + +static void fix(struct Edit *e, const char *str) { + assert(0 == editFn(e, EditClear)); + for (const char *ch = str; *ch; ++ch) { + assert(0 == editInsert(e, (wchar_t)*ch)); } } + +static bool eq(struct Edit *e, const char *str1) { + size_t pos; + static size_t cap; + static char *buf; + assert(NULL != editString(e, &buf, &cap, &pos)); + const char *str2 = &str1[strlen(str1) + 1]; + return pos == strlen(str1) + && !strncmp(buf, str1, pos) + && !strcmp(&buf[pos], str2); +} + +#define editFn(...) assert(0 == editFn(__VA_ARGS__)) + +int main(void) { + struct Edit cut = {0}; + struct Edit e = { .cut = &cut }; + + fix(&e, "foo bar"); + editFn(&e, EditHead); + assert(eq(&e, "\0foo bar")); + editFn(&e, EditTail); + assert(eq(&e, "foo bar\0")); + editFn(&e, EditPrev); + assert(eq(&e, "foo ba\0r")); + editFn(&e, EditNext); + assert(eq(&e, "foo bar\0")); + + fix(&e, "foo, bar"); + editFn(&e, EditPrevWord); + assert(eq(&e, "foo, \0bar")); + editFn(&e, EditPrevWord); + assert(eq(&e, "\0foo, bar")); + editFn(&e, EditNextWord); + assert(eq(&e, "foo, \0bar")); + editFn(&e, EditNextWord); + assert(eq(&e, "foo, bar\0")); + + fix(&e, "foo bar"); + editFn(&e, EditPrevWord); + editFn(&e, EditDeleteHead); + assert(eq(&e, "\0bar")); + + fix(&e, "foo bar"); + editFn(&e, EditPrevWord); + editFn(&e, EditDeleteTail); + assert(eq(&e, "foo \0")); + + fix(&e, "foo bar"); + editFn(&e, EditDeletePrev); + assert(eq(&e, "foo ba\0")); + editFn(&e, EditHead); + editFn(&e, EditDeleteNext); + assert(eq(&e, "\0oo ba")); + + fix(&e, "foo, bar"); + editFn(&e, EditDeletePrevWord); + assert(eq(&e, "foo, \0")); + editFn(&e, EditDeletePrevWord); + assert(eq(&e, "\0")); + + fix(&e, "foo, bar"); + editFn(&e, EditHead); + editFn(&e, EditDeleteNextWord); + assert(eq(&e, "\0, bar")); + editFn(&e, EditDeleteNextWord); + assert(eq(&e, "\0")); + + fix(&e, "foo bar"); + editFn(&e, EditDeletePrevWord); + editFn(&e, EditPaste); + assert(eq(&e, "foo bar\0")); + editFn(&e, EditPaste); + assert(eq(&e, "foo barbar\0")); + + fix(&e, "bar"); + editFn(&e, EditTranspose); + assert(eq(&e, "bra\0")); + editFn(&e, EditHead); + editFn(&e, EditTranspose); + assert(eq(&e, "rb\0a")); + editFn(&e, EditTranspose); + assert(eq(&e, "rab\0")); + + fix(&e, " foo bar "); + editFn(&e, EditCollapse); + assert(eq(&e, "foo bar\0")); +} + +#endif /* TEST */ diff --git a/edit.h b/edit.h new file mode 100644 index 0000000..41966b8 --- /dev/null +++ b/edit.h @@ -0,0 +1,74 @@ +/* Copyright (C) 2022 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <stdbool.h> +#include <stddef.h> + +struct Edit { + wchar_t *buf; + size_t pos; + size_t len; + size_t cap; + struct Edit *cut; +}; + +enum EditFn { + EditHead, + EditTail, + EditPrev, + EditNext, + EditPrevWord, + EditNextWord, + EditDeleteHead, + EditDeleteTail, + EditDeletePrev, + EditDeleteNext, + EditDeletePrevWord, + EditDeleteNextWord, + EditPaste, + EditTranspose, + EditCollapse, + EditClear, +}; + +// Perform an editing function. +int editFn(struct Edit *e, enum EditFn fn); + +// Insert a character at the cursor. +int editInsert(struct Edit *e, wchar_t ch); + +// Convert the buffer to a multi-byte string. +char *editString(const struct Edit *e, char **buf, size_t *cap, size_t *pos); + +// Reserve a range in the buffer. +int editReserve(struct Edit *e, size_t index, size_t count); + +// Copy a range of the buffer into e->cut. +int editCopy(struct Edit *e, size_t index, size_t count); + +// Delete a range from the buffer. If cut is true, copy the deleted portion. +int editDelete(struct Edit *e, bool cut, size_t index, size_t count); diff --git a/filter.c b/filter.c index 63c3369..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 diff --git a/handle.c b/handle.c index 7721745..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 @@ -127,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); } @@ -164,7 +187,9 @@ static void handleCap(struct Message *msg) { } 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")) { @@ -203,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] = {0}; - size_t len = 1 + strlen(self.plain); + 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.plain, len - 1); - byte *sep = memchr(buf, ':', len); - if (!sep) errx(EX_USAGE, "SASL PLAIN missing colon"); - *sep = 0; + 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) { @@ -240,7 +266,7 @@ 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; @@ -252,19 +278,26 @@ static void handleReplyWelcome(struct Message *msg) { 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); @@ -339,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; } @@ -377,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); @@ -399,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), @@ -416,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]); } @@ -440,12 +474,13 @@ 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\17)", @@ -457,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( @@ -535,7 +571,12 @@ static void handleReplyNames(struct Message *msg) { char *nick = &prefixes[strspn(prefixes, network.prefixes)]; char *user = strsep(&name, "@"); enum Color color = (user ? hash(user) : Default); - completeAdd(id, nick, color); + 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 @@ -558,41 +599,6 @@ static void handleReplyEndOfNames(struct Message *msg) { } } -static struct { - char buf[1024]; - char *ptr; - char *end; -} who = { - .ptr = who.buf, - .end = &who.buf[sizeof(who.buf)], -}; - -static void handleReplyWho(struct Message *msg) { - require(msg, false, 7); - if (who.ptr == who.buf) { - who.ptr = seprintf( - who.ptr, who.end, "The council of \3%02d%s\3 are ", - hash(msg->params[1]), msg->params[1] - ); - } - char *prefixes = &msg->params[6][1]; - if (prefixes[0] == '*') prefixes++; - prefixes[strspn(prefixes, network.prefixes)] = '\0'; - if (!prefixes[0] || prefixes[0] == '+') return; - who.ptr = seprintf( - who.ptr, who.end, "%s\3%02d%s%s\3%s", - (who.ptr[-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), who.buf); - who.ptr = who.buf; -} - static void handleReplyNoTopic(struct Message *msg) { require(msg, false, 2); uiFormat( @@ -604,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); } } @@ -643,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]); @@ -660,10 +689,8 @@ static void handleTopic(struct Message *msg) { return; } - char buf[1024]; - char *ptr = buf, *end = &buf[sizeof(buf)]; - const char *prev = complete(id, "/topic "); - completeReject(); + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); if (prev) { prev += 7; } else { @@ -675,45 +702,46 @@ static void handleTopic(struct Message *msg) { if (swprintf(old, ARRAY_LEN(old), L"%s", prev) < 0) goto plain; if (swprintf(new, ARRAY_LEN(new), L"%s", msg->params[1]) < 0) goto plain; - if (!hashBound) { - ptr = seprintf(ptr, end, "%c%ls%c -> %c%ls%c", R, old, O, R, new, O); - goto plain; - } - 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); - ptr = seprintf(ptr, end, "%ls", new); - swap(&new[pre], &nul); - swap(osuf, &nul); - ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Brown, &old[pre]); - swap(osuf, &nul); - swap(nsuf, &nul); - ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Green, &new[pre]); - swap(nsuf, &nul); - ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Default, 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], - (ptr > buf ? 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] = { @@ -835,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", @@ -1021,19 +1055,19 @@ 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\17)", @@ -1122,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)", @@ -1141,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] ); @@ -1179,11 +1208,11 @@ static bool isAction(struct Message *msg) { 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; @@ -1193,48 +1222,53 @@ static bool isMention(const struct Message *msg) { return false; } -static void colorMentions(char *buf, size_t cap, uint id, struct Message *msg) { - *buf = '\0'; +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; +} - char *split = strstr(msg->params[1], ": "); +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'; - char *ptr = buf, *end = &buf[cap]; - while (*mention) { - size_t skip = strspn(mention, ",<> "); - ptr = seprintf(ptr, end, "%.*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) { - ptr = seprintf(ptr, end, "\3%02d%s\3", color, mention); + ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg); } else { - ptr = seprintf(ptr, end, "%s", mention); + ptr = p; } - mention[len] = punct; - mention += len; + msg += len; } - seprintf(ptr, end, "%c", delimit); + +rest: + return seprintf(ptr, end, "%s", msg); } static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); - if (network.statusmsg) { - msg->params[0] += strspn(msg->params[0], network.statusmsg); + 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, '.'); @@ -1252,42 +1286,48 @@ static void handlePrivmsg(struct Message *msg) { bool notice = (msg->cmd[0] == 'N'); 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]; + 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(buf, sizeof(buf), 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(buf, sizeof(buf), 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) { @@ -1317,7 +1357,6 @@ 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 }, @@ -1335,7 +1374,6 @@ static const struct Handler { { "347", -ReplyInvex, NULL }, { "348", +ReplyExcepts, handleReplyExceptList }, { "349", -ReplyExcepts, NULL }, - { "352", +ReplyWho, handleReplyWho }, { "353", 0, handleReplyNames }, { "366", 0, handleReplyEndOfNames }, { "367", +ReplyBan, handleReplyBanList }, @@ -1400,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 20e9f91..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 diff --git a/log.c b/log.c index 32d771f..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 @@ -73,6 +73,15 @@ static void logMkdir(const char *path) { 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; int month; @@ -97,22 +106,21 @@ static FILE *logFile(uint id, const struct tm *tm) { logs[id].month = tm->tm_mon; logs[id].day = tm->tm_mday; - size_t len = 0; char path[PATH_MAX]; - for (const char *ch = network.name; *ch; ++ch) { - path[len++] = (*ch == '/' ? '_' : *ch); - } - path[len] = '\0'; + char *ptr = path, *end = &path[sizeof(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'; + char *name = ptr; + ptr = seprintf(ptr, end, "/%s", idNames[id]); + sanitize(&name[1], ptr); logMkdir(path); - strftime(&path[len], sizeof(path) - len, "/%F.log", tm); + size_t len = strftime(ptr, end - ptr, "/%F.log", tm); + if (!len) errx(EX_CANTCREAT, "log path too long"); + int fd = openat( logDir, path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 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/ui.c b/ui.c index 2789bd7..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,26 +28,21 @@ #define _XOPEN_SOURCE_EXTENDED #include <assert.h> -#include <ctype.h> #include <curses.h> #include <err.h> #include <errno.h> #include <fcntl.h> -#include <limits.h> -#include <signal.h> +#include <inttypes.h> #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sysexits.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> #ifdef __FreeBSD__ #include <capsicum_helpers.h> @@ -55,105 +50,13 @@ #include "chat.h" -// Annoying stuff from <term.h>: -#undef lines -#undef tab - -enum { - StatusLines = 1, - MarkerLines = 1, - SplitLines = 5, - InputLines = 1, - InputCols = 1024, -}; - #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; - bool time; - 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; -} - -enum Heat uiThreshold = Cold; - -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->time = uiTime.enable; - if (id == Network || id == Debug) { - window->thresh = Cold; - } else { - window->thresh = uiThreshold; - } - window->buffer = bufferAlloc(); - completeAdd(None, idNames[id], idColors[id]); - return windowPush(window); -} - -static void windowFree(struct Window *window) { - completeRemove(None, idNames[window->id]); - bufferFree(window->buffer); - free(window); -} +WINDOW *uiStatus; +WINDOW *uiMain; +WINDOW *uiInput; static short colorPairs; @@ -188,65 +91,18 @@ static short colorPair(short fg, short bg) { return colorPairs++; } -#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(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 -}; - // XXX: Assuming terminals will be fine with these even if they're unsupported, // since they're "private" modes. static const char *FocusMode[2] = { "\33[?1004l", "\33[?1004h" }; static const char *PasteMode[2] = { "\33[?2004l", "\33[?2004h" }; -struct Time uiTime = { .format = "%X" }; - static void errExit(void) { putp(FocusMode[false]); putp(PasteMode[false]); reset_shell_mode(); } -void uiInitEarly(void) { +void uiInit(void) { initscr(); cbreak(); noecho(); @@ -265,73 +121,32 @@ void uiInitEarly(void) { from_status_line = "\7"; } -#define X(id, seq, alt) define_key(seq, id); if (alt) define_key(alt, id); - ENUM_KEY -#undef X - - status = newwin(StatusLines, COLS, 0, 0); - if (!status) err(EX_OSERR, "newwin"); + uiStatus = newwin(StatusLines, COLS, 0, 0); + if (!uiStatus) err(EX_OSERR, "newwin"); - main = newwin(MAIN_LINES, COLS, StatusLines, 0); - if (!main) err(EX_OSERR, "newwin"); + uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); + if (!uiMain) err(EX_OSERR, "newwin"); - int y; - char fmt[TimeCap]; - char buf[TimeCap]; - styleStrip(fmt, sizeof(fmt), uiTime.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); - waddstr(main, buf); - waddch(main, ' '); - getyx(main, y, uiTime.width); - (void)y; + 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 @@ -340,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); } @@ -375,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; @@ -384,91 +199,13 @@ 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]); -} - -static int styleAdd(WINDOW *win, struct Style init, const char *str) { - struct Style style = init; - while (*str) { - size_t len = styleParse(&style, &str); - wattr_set(win, styleAttr(style), stylePair(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(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], *end = &buf[sizeof(buf)]; - char *ptr = seprintf( - buf, end, "\3%d%s %u%s%s %s ", - idColors[window->id], (num == windows.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%s", - (window->heat > Warm ? White : idColors[window->id]), - window->unreadWarm, idColors[window->id], - (window->scroll ? "" : " ") - ); - } - if (window->scroll) { - ptr = seprintf(ptr, end, "~%d ", window->scroll); - } - if (styleAdd(status, StyleDefault, buf) < 0) break; - } - wclrtoeol(status); - - const struct Window *window = windows.ptrs[windows.show]; - char *end = &title[sizeof(title)]; - char *ptr = seprintf( - title, 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 void mark(struct Window *window) { - if (window->scroll) return; - window->mark = true; - window->unreadSoft = 0; - window->unreadWarm = 0; -} +bool uiSpoilerReveal; -static void unmark(struct Window *window) { - if (!window->scroll) { - window->mark = false; - window->heat = Cold; +short uiPair(struct Style style) { + if (uiSpoilerReveal && style.fg == style.bg) { + return colorPair(Colors[Default], Colors[style.bg]); } - statusUpdate(); + return colorPair(Colors[style.fg], Colors[style.bg]); } void uiShow(void) { @@ -478,88 +215,18 @@ void uiShow(void) { 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(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 int windowCols(const struct Window *window) { - return COLS - (window->time ? uiTime.width : 0); -} - -static void mainAdd(int y, bool time, const struct Line *line) { - int ny, nx; - wmove(main, y, 0); - if (!line || !line->str[0]) { - wclrtoeol(main); - return; - } - if (time && line->time) { - char buf[TimeCap]; - strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time)); - struct Style init = { .fg = Gray, .bg = Default }; - styleAdd(main, init, buf); - waddch(main, ' '); - } else if (time) { - whline(main, ' ', uiTime.width); - wmove(main, y, uiTime.width); - } - styleAdd(main, StyleDefault, line->str); - getyx(main, ny, nx); - if (ny != y) return; - 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) { - 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(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; @@ -586,36 +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, windowCols(window), - window->thresh, 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, windowCols(window), - 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); } @@ -633,488 +272,15 @@ 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, 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; - } -} - -static void resize(void) { - wclear(main); - wresize(main, MAIN_LINES, COLS); - for (uint num = 0; num < windows.len; ++num) { - windowReflow(windows.ptrs[num]); - } - statusUpdate(); - mainUpdate(); -} - -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; - } - - char buf[TimeCap]; - strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time)); - 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 reset, 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') { - *style = reset; - 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 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 void inputUpdate(void) { - size_t pos; - char *buf = editBuffer(&pos); - struct Window *window = windows.ptrs[windows.show]; - - 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(window->id, buf); - const char *privmsg = commandIsPrivmsg(window->id, buf); - const char *notice = commandIsNotice(window->id, buf); - const char *action = commandIsAction(window->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 (window->id == Debug && buf[0] != '/') { - prompt = "<< "; - stylePrompt.fg = Gray; - } else { - prompt = ""; - } - if (skip > &buf[pos]) { - prefix = prompt = suffix = ""; - skip = buf; - } - - wmove(input, 0, 0); - if (window->time && window->id != Network) { - whline(input, ' ', uiTime.width); - wmove(input, 0, uiTime.width); - } - wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), NULL); - waddstr(input, prefix); - waddstr(input, prompt); - waddstr(input, suffix); - - int y, x; - const char *ptr = skip; - struct Style style = styleInput; - if (split && split < pos) { - ptr = inputStop(styleInput, &style, ptr, &buf[split]); - style = styleInput; - style.bg = Red; - } - ptr = inputStop(styleInput, &style, ptr, &buf[pos]); - getyx(input, y, x); - if (split && split >= pos) { - ptr = inputStop(styleInput, &style, ptr, &buf[split]); - style = styleInput; - style.bg = Red; - } - inputAdd(styleInput, &style, ptr); - wclrtoeol(input); - wmove(input, y, x); -} - -void uiWindows(void) { - for (uint num = 0; num < windows.len; ++num) { - const struct Window *window = windows.ptrs[num]; - uiFormat( - Network, Warm, NULL, "\3%02d%u %s", - idColors[window->id], num, idNames[window->id] - ); - } -} - -static void windowShow(uint num) { - if (num != windows.show) { - windows.swap = windows.show; - mark(windows.ptrs[windows.swap]); - } - windows.show = num; - windows.user = num; - 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 scrollTop(struct Window *window) { - for (size_t i = 0; i < BufferCap; ++i) { - if (!bufferHard(window->buffer, i)) continue; - scrollTo(window, BufferCap - i); - break; - } -} - -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 toggleTime(struct Window *window) { - window->time ^= true; - windowReflow(window); - statusUpdate(); - mainUpdate(); - inputUpdate(); -} - -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); - statusUpdate(); - 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: scrollTop(window); - - 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 KeyMetaT: toggleTime(window); - break; case KeyMetaU: scrollTo(window, window->unreadHard); - break; case KeyMetaV: scrollPage(window, +1); - - break; case KeyCtrlLeft: edit(id, EditPrevWord, 0); - break; case KeyCtrlRight: edit(id, EditNextWord, 0); - - 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); - } +void uiResize(void) { + wclear(uiMain); + wresize(uiMain, MAIN_LINES, COLS); + windowResize(); } -static void keyCtrl(wchar_t ch) { - struct Window *window = windows.ptrs[windows.show]; - uint id = window->id; - switch (ch ^ L'@') { - break; case L'?': edit(id, EditDeletePrev, 0); - break; case L'A': edit(id, EditHead, 0); - break; case L'B': edit(id, EditPrev, 0); - break; case L'C': raise(SIGINT); - break; case L'D': edit(id, EditDeleteNext, 0); - break; case L'E': edit(id, EditTail, 0); - break; case L'F': edit(id, EditNext, 0); - break; case L'H': edit(id, EditDeletePrev, 0); - break; case L'I': edit(id, EditComplete, 0); - break; case L'J': edit(id, EditEnter, 0); - break; case L'K': edit(id, EditDeleteTail, 0); - break; case L'L': clearok(curscr, true); - break; case L'N': uiShowNum(windows.show + 1); - break; case L'P': uiShowNum(windows.show - 1); - break; case L'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; - if (iswcntrl(ch)) ch = towlower(ch ^ L'@'); - 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': 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); - } - if (color != Default) { - char buf[4]; - snprintf(buf, sizeof(buf), "%c%02d", C, color); - for (char *ch = buf; *ch; ++ch) { - edit(id, EditInsert, *ch); - } - } -} - -void uiRead(void) { - if (hidden) { - if (waiting) { - uiShow(); - flushinp(); - waiting = false; - } else { - return; - } - } - - wint_t ch; - static bool paste, style, literal; - 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 (ret == KEY_CODE_YES && ch == KeyPasteManual) { - paste ^= true; - } else if (paste || literal) { - 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 && ch == (L'V' ^ L'@')) { - literal = true; - continue; - } else if (style) { - keyStyle(ch); - } else if (iswcntrl(ch)) { - keyCtrl(ch); - } else { - edit(windows.ptrs[windows.show]->id, EditInsert, ch); - } - style = false; - literal = false; - } - inputUpdate(); -} +static FILE *saveFile; -static const time_t Signatures[] = { +static const uint64_t Signatures[] = { 0x6C72696774616301, // no heat, unread, unreadWarm 0x6C72696774616302, // no self.pos 0x6C72696774616303, // no buffer line heat @@ -1122,71 +288,38 @@ static const time_t Signatures[] = { 0x6C72696774616305, // no URLs 0x6C72696774616306, // no thresh 0x6C72696774616307, // no window time - 0x6C72696774616308, + 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; } - errx(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); } -static FILE *saveFile; - int uiSave(void) { - int error = 0 - || ftruncate(fileno(saveFile), 0) - || writeTime(saveFile, Signatures[7]) - || writeTime(saveFile, self.pos); - if (error) return error; - for (uint num = 0; num < windows.len; ++num) { - const struct Window *window = windows.ptrs[num]; - error = 0 - || writeString(saveFile, idNames[window->id]) - || writeTime(saveFile, window->mute) - || writeTime(saveFile, window->time) - || writeTime(saveFile, window->thresh) - || writeTime(saveFile, window->heat) - || writeTime(saveFile, window->unreadSoft) - || writeTime(saveFile, 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(saveFile, line->time) - || writeTime(saveFile, line->heat) - || writeString(saveFile, line->str); - if (error) return error; - } - error = writeTime(saveFile, 0); - if (error) return error; - } return 0 - || writeString(saveFile, "") + || ftruncate(fileno(saveFile), 0) + || writeUint64(saveFile, Signatures[8]) + || writeUint64(saveFile, self.pos) + || windowSave(saveFile) + || inputSave(saveFile) || urlSave(saveFile) || fclose(saveFile); } -static time_t readTime(FILE *file) { - time_t time; - fread(&time, sizeof(time), 1, file); +static uint64_t readUint64(FILE *file) { + uint64_t u; + fread(&u, sizeof(u), 1, file); if (ferror(file)) err(EX_IOERR, "fread"); - if (feof(file)) errx(EX_DATAERR, "unexpected eof"); - return time; -} -static ssize_t readString(FILE *file, char **buf, size_t *cap) { - ssize_t len = getdelim(buf, cap, '\0', file); - if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim"); - return len; + if (feof(file)) errx(EX_DATAERR, "unexpected end of save file"); + return u; } void uiLoad(const char *name) { @@ -1216,31 +349,9 @@ void uiLoad(const char *name) { size_t version = signatureVersion(signature); if (version > 1) { - self.pos = readTime(saveFile); - } - - char *buf = NULL; - size_t cap = 0; - while (0 < readString(saveFile, &buf, &cap) && buf[0]) { - struct Window *window = windows.ptrs[windowFor(idFor(buf))]; - if (version > 3) window->mute = readTime(saveFile); - if (version > 6) window->time = readTime(saveFile); - if (version > 5) window->thresh = readTime(saveFile); - if (version > 0) { - window->heat = readTime(saveFile); - window->unreadSoft = readTime(saveFile); - window->unreadWarm = readTime(saveFile); - } - for (;;) { - time_t time = readTime(saveFile); - if (!time) break; - enum Heat heat = (version > 2 ? readTime(saveFile) : Cold); - readString(saveFile, &buf, &cap); - bufferPush(window->buffer, COLS, window->thresh, heat, time, buf); - } - windowReflow(window); + self.pos = readUint64(saveFile); } + windowLoad(saveFile, version); + inputLoad(saveFile, version); urlLoad(saveFile, version); - - free(buf); } diff --git a/url.c b/url.c index 219a83c..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 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 06c242c..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 |