From 9c384de6dbe526374af5075e06260063a9764cb9 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Wed, 9 Feb 2022 17:55:33 -0500 Subject: Treat any amount of space and punctuation as word boundaries This matches behaviour of, e.g. zsh -o emacs. --- edit.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/edit.c b/edit.c index 71c5cca..9aa1c86 100644 --- a/edit.c +++ b/edit.c @@ -207,6 +207,10 @@ static void tabReject(void) { tab.len = 0; } +static bool isword(wchar_t ch) { + return !iswspace(ch) && !iswpunct(ch); +} + void edit(uint id, enum Edit op, wchar_t ch) { size_t init = pos; switch (op) { @@ -215,12 +219,12 @@ void edit(uint id, enum Edit op, wchar_t ch) { break; case EditPrev: if (pos) pos--; break; case EditNext: if (pos < len) pos++; break; case EditPrevWord: { - if (pos) pos--; - while (pos && !iswspace(buf[pos - 1])) pos--; + while (pos && !isword(buf[pos - 1])) pos--; + while (pos && isword(buf[pos - 1])) pos--; } break; case EditNextWord: { - if (pos < len) pos++; - while (pos < len && !iswspace(buf[pos])) pos++; + while (pos < len && !isword(buf[pos])) pos++; + while (pos < len && isword(buf[pos])) pos++; } break; case EditDeleteHead: delete(true, 0, pos); pos = 0; @@ -229,15 +233,17 @@ void edit(uint id, enum Edit op, wchar_t ch) { break; case EditDeleteNext: delete(false, pos, 1); break; case EditDeletePrevWord: { if (!pos) break; - size_t word = pos - 1; - while (word && !iswspace(buf[word - 1])) word--; + size_t word = pos; + while (word && !isword(buf[word - 1])) word--; + while (word && isword(buf[word - 1])) word--; delete(true, word, pos - word); pos = word; } break; case EditDeleteNextWord: { if (pos == len) break; - size_t word = pos + 1; - while (word < len && !iswspace(buf[word])) word++; + size_t word = pos; + while (word < len && !isword(buf[word])) word++; + while (word < len && isword(buf[word])) word++; delete(true, pos, word - pos); } break; case EditPaste: { -- cgit 1.4.1 From 397b4ce6bd9064aa36f8b9c264e4bbc226822d6f Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 12 Feb 2022 13:26:38 -0500 Subject: Prompt for empty server or SASL passwords --- catgirl.1 | 13 +++++++------ chat.c | 24 +++++++++++++++++++++++- chat.h | 3 ++- handle.c | 18 ++++++++++-------- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/catgirl.1 b/catgirl.1 index 34b9718..065002b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 3, 2022 +.Dd February 12, 2022 .Dt CATGIRL 1 .Os . @@ -222,11 +222,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 @@ -375,6 +373,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 diff --git a/chat.c b/chat.c index ba6c9a1..4898411 100644 --- a/chat.c +++ b/chat.c @@ -50,6 +50,8 @@ #include #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; @@ -288,7 +296,7 @@ int main(int argc, char *argv[]) { uiTime.enable = true; if (optarg) uiTime.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); @@ -337,6 +345,20 @@ int main(int argc, char *argv[]) { user = hash; } + if (pass && !pass[0]) { + char *buf = malloc(512); + if (!buf) err(EX_OSERR, "malloc"); + pass = readpassphrase("Server password: ", buf, 512, 0); + if (!pass) errx(EX_IOERR, "unable to read passphrase"); + } + + if (self.plainPass && !self.plainPass[0]) { + char *buf = malloc(512); + if (!buf) err(EX_OSERR, "malloc"); + self.plainPass = readpassphrase("Account password: ", buf, 512, 0); + if (!self.plainPass) errx(EX_IOERR, "unable to read passphrase"); + } + // Modes defined in RFC 1459: set(&network.chanTypes, "#&"); set(&network.prefixes, "@+"); diff --git a/chat.h b/chat.h index 19d2b18..753d1a3 100644 --- a/chat.h +++ b/chat.h @@ -193,7 +193,8 @@ extern struct Self { bool restricted; size_t pos; enum Cap caps; - char *plain; + char *plainUser; + char *plainPass; char *mode; char *join; char *nick; diff --git a/handle.c b/handle.c index e460c7c..d663ca1 100644 --- a/handle.c +++ b/handle.c @@ -164,7 +164,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,18 +205,18 @@ 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); @@ -224,7 +226,7 @@ static void handleAuthenticate(struct Message *msg) { 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) { -- cgit 1.4.1 From 6013ae15134ae09d9ebeec7fa1dcfc5450d4a8ba Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 12 Feb 2022 13:27:12 -0500 Subject: Import compat_readpassphrase.c from oconfigure At 5d08070de6aadd979e3752263d2ec1e709bf037e. --- compat_readpassphrase.c | 194 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 compat_readpassphrase.c diff --git a/compat_readpassphrase.c b/compat_readpassphrase.c new file mode 100644 index 0000000..353868a --- /dev/null +++ b/compat_readpassphrase.c @@ -0,0 +1,194 @@ +/* + * Original: readpassphrase.c in OpenSSH portable + */ +/* + * Copyright (c) 2000-2002, 2007, 2010 + * Todd C. Miller + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +} -- cgit 1.4.1 From edad0218c1d7fc10686d26050829865d0894026a Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 12 Feb 2022 13:35:23 -0500 Subject: Copy RPP defines from oconfigure We don't use any flags, so just define them in the compat source file. --- compat_readpassphrase.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compat_readpassphrase.c b/compat_readpassphrase.c index 353868a..3bb2045 100644 --- a/compat_readpassphrase.c +++ b/compat_readpassphrase.c @@ -32,6 +32,18 @@ #include #include +/* + * 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 -- cgit 1.4.1 From c95c8cd1b47d9b2031bc3c05d661168a7f346c56 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 12 Feb 2022 13:30:15 -0500 Subject: Use compat_readpassphrase.c on Linux --- configure | 1 + 1 file changed, 1 insertion(+) diff --git a/configure b/configure index 3459f94..9465b77 100755 --- a/configure +++ b/configure @@ -45,6 +45,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 -- cgit 1.4.1 From 28a27a2c0fdbd05328a4c30fbb63a4e3c6e12432 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 12 Feb 2022 20:19:18 -0500 Subject: Move sandman build to scripts/Makefile --- Makefile | 11 ----------- README.7 | 6 +++--- scripts/Makefile | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 scripts/Makefile diff --git a/Makefile b/Makefile index 48fc350..83ead95 100644 --- a/Makefile +++ b/Makefile @@ -48,17 +48,6 @@ install: catgirl catgirl.1 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 - CHROOT_USER = chat CHROOT_GROUP = ${CHROOT_USER} diff --git a/README.7 b/README.7 index 5a614e8..8f13f7c 100644 --- a/README.7 +++ b/README.7 @@ -1,5 +1,5 @@ .\" To view this file, run: man ./README.7 -.Dd June 28, 2021 +.Dd February 12, 2022 .Dt README 7 .Os "Causal Agency" . @@ -167,8 +167,8 @@ to stop and start on system sleep and wake. Install it as follows: .Bd -literal -offset indent -$ make scripts/sandman -# make install-sandman +$ make -C scripts sandman +# make -C scripts install .Ed . .Sh FILES diff --git a/scripts/Makefile b/scripts/Makefile new file mode 100644 index 0000000..179a2d3 --- /dev/null +++ b/scripts/Makefile @@ -0,0 +1,22 @@ +PREFIX ?= /usr/local +BINDIR ?= ${PREFIX}/bin +MANDIR ?= ${PREFIX}/man + +CFLAGS += -Wall -Wextra + +-include ../config.mk + +LDLIBS = -framework Cocoa + +all: sandman + +clean: + rm -f sandman + +install: sandman sandman.1 + install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 + install sandman ${DESTDIR}${BINDIR} + install -m 644 sandman.1 ${DESTDIR}${MANDIR}/man1 + +uninstall: + rm -f ${DESTDIR}${BINDIR}/sandman ${DESTDIR}/man/man1/sandman.1 -- cgit 1.4.1 From 0036e6e9f093c623f87def43b45cb87efb2823e6 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Fri, 18 Feb 2022 22:40:16 -0500 Subject: Fix M-f ordering --- edit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit.c b/edit.c index 9aa1c86..66b5302 100644 --- a/edit.c +++ b/edit.c @@ -223,8 +223,8 @@ void edit(uint id, enum Edit op, wchar_t ch) { while (pos && isword(buf[pos - 1])) pos--; } break; case EditNextWord: { - while (pos < len && !isword(buf[pos])) pos++; while (pos < len && isword(buf[pos])) pos++; + while (pos < len && !isword(buf[pos])) pos++; } break; case EditDeleteHead: delete(true, 0, pos); pos = 0; -- cgit 1.4.1 From 573bc855a68766f38de2acb48bebc9d0adbf0a1f Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Fri, 18 Feb 2022 23:39:46 -0500 Subject: Simplify cursor positioning in input Do some extra work by adding the portion before the cursor to the input window twice, but simplify the interaction with the split point. This fixes the awkward behaviour when moving the cursor across colour codes where the code would be partially interpreted up to the cursor. --- ui.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui.c b/ui.c index 369ed75..3df0cd6 100644 --- a/ui.c +++ b/ui.c @@ -791,6 +791,7 @@ static void inputUpdate(void) { skip = buf; } + int y, x; wmove(input, 0, 0); if (window->time && window->id != Network) { whline(input, ' ', uiTime.width); @@ -800,25 +801,24 @@ static void inputUpdate(void) { waddstr(input, prefix); waddstr(input, prompt); waddstr(input, suffix); + getyx(input, y, x); - int y, x; - const char *ptr = skip; + int posx; 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) { + inputStop(styleInput, &style, skip, &buf[pos]); + getyx(input, y, posx); + wmove(input, y, x); + + style = styleInput; + const char *ptr = skip; + if (split) { ptr = inputStop(styleInput, &style, ptr, &buf[split]); style = styleInput; style.bg = Red; } inputAdd(styleInput, &style, ptr); wclrtoeol(input); - wmove(input, y, x); + wmove(input, y, posx); } void uiWindows(void) { -- cgit 1.4.1 From 1a2477ef7a34cc24c7bc18d7b6326643ce0995a2 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Fri, 18 Feb 2022 23:46:06 -0500 Subject: Implement new line editing "library" Losing tab complete and text macros, for now. This new implementation works on an instance of a struct and does not interact with the rest of catgirl, making it possible to copy into another project. Unlike existing line editing libraries, this one is entirely abstract and can be rendered externally. My goal with this library is to be able to implement vi mode. Since it operates on struct instances rather than globals, it might also be possible to give catgirl separate line editing buffers for each window, which would be a nice UX improvement. --- Makefile | 2 +- chat.c | 1 - chat.h | 25 ---- edit.c | 452 +++++++++++++++++++++++++++++++-------------------------------- edit.h | 91 +++++++++++++ ui.c | 88 +++++++------ 6 files changed, 358 insertions(+), 301 deletions(-) create mode 100644 edit.h diff --git a/Makefile b/Makefile index 83ead95..9e6392b 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ all: catgirl catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ -${OBJS}: chat.h +${OBJS}: chat.h edit.h tags: *.[ch] ctags -w *.[ch] diff --git a/chat.c b/chat.c index 4898411..454ae31 100644 --- a/chat.c +++ b/chat.c @@ -371,7 +371,6 @@ int main(int argc, char *argv[]) { set(&network.name, host); set(&self.nick, "*"); - editCompleteAdd(); commandCompleteAdd(); ircConfig(insecure, trust, cert, priv); diff --git a/chat.h b/chat.h index 753d1a3..8a9a48f 100644 --- a/chat.h +++ b/chat.h @@ -341,31 +341,6 @@ 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, -}; -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); diff --git a/edit.c b/edit.c index 66b5302..21061d7 100644 --- a/edit.c +++ b/edit.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe +/* Copyright (C) 2020, 2022 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -13,8 +13,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * 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 @@ -25,282 +23,272 @@ * covered work. */ -#include +#include #include #include -#include #include #include #include -#include "chat.h" +#include "edit.h" + +static bool isword(wchar_t ch) { + return !iswspace(ch) && !iswpunct(ch); +} -enum { Cap = 1024 }; -static wchar_t buf[Cap]; -static size_t len; -static size_t pos; +void editFree(struct Edit *e) { + free(e->buf); + free(e->cut.buf); + free(e->mbs.buf); + e->pos = e->len = e->cap = 0; + e->cut.len = 0; + e->mbs.pos = e->mbs.len = 0; +} -char *editBuffer(size_t *mbsPos) { - static char mbs[MB_LEN_MAX * Cap]; +char *editString(struct Edit *e) { + size_t cap = e->len * MB_CUR_MAX + 1; + char *buf = realloc(e->mbs.buf, cap); + if (!buf) return NULL; + e->mbs.buf = buf; - 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; + e->mbs.len = wcsnrtombs(e->mbs.buf, &ptr, e->pos, cap-1, NULL); + if (e->mbs.len == (size_t)-1) return NULL; + e->mbs.pos = e->mbs.len; - ptr = &buf[pos]; + ptr = &e->buf[e->pos]; size_t n = wcsnrtombs( - &mbs[mbsLen], &ptr, len - pos, sizeof(mbs) - mbsLen - 1, NULL + &e->mbs.buf[e->mbs.len], &ptr, e->len - e->pos, + cap-1 - e->mbs.len, NULL ); - assert(n != (size_t)-1); - mbsLen += n; - - mbs[mbsLen] = '\0'; - return mbs; -} - -static struct { - wchar_t buf[Cap]; - size_t len; -} cut; + if (n == (size_t)-1) return NULL; + e->mbs.len += n; -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; + e->mbs.buf[e->mbs.len] = '\0'; + return e->mbs.buf; } -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 ? e->cap * 2 : 256); + 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; } + wchar_t *buf = realloc(e->cut.buf, sizeof(*buf) * count); + if (!buf) return -1; + e->cut.buf = buf; + wmemcpy(e->cut.buf, &e->buf[index], count); + e->cut.len = 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; - } - 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' '; +int editDelete(struct Edit *e, bool cut, size_t index, size_t count) { + if (index + count > e->len) { + errno = EINVAL; + return -1; } - wmemcpy(&buf[tab.pos], wcs, n); - pos = tab.pos + tab.len; -} - -static void tabAccept(void) { - completeAccept(); - tab.len = 0; -} - -static void tabReject(void) { - completeReject(); - tab.len = 0; -} - -static bool isword(wchar_t ch) { - return !iswspace(ch) && !iswpunct(ch); + 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; } -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: if (e->pos) e->pos--; + break; case EditNext: if (e->pos < e->len) e->pos++; break; case EditPrevWord: { - while (pos && !isword(buf[pos - 1])) pos--; - while (pos && isword(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: { - while (pos < len && isword(buf[pos])) pos++; - while (pos < len && !isword(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: { + if (e->pos) editDelete(e, false, --e->pos, 1); + } + break; case EditDeleteNext: { + editDelete(e, false, e->pos, 1); + } break; case EditDeletePrevWord: { - if (!pos) break; - size_t word = pos; - while (word && !isword(buf[word - 1])) word--; - while (word && isword(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; - while (word < len && !isword(buf[word])) word++; - while (word < len && isword(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; + 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; +} + +#ifdef TEST +#undef NDEBUG +#include +#include - if (pos < init) { - tabReject(); - } else { - tabAccept(); +static void fix(struct Edit *e, const char *str) { + editFn(e, EditClear); + for (const char *ch = str; *ch; ++ch) { + editInsert(e, (wchar_t)*ch); } } + +static bool eq(struct Edit *e, const char *str1) { + const char *str2 = &str1[strlen(str1) + 1]; + const char *buf = editString(e); + return e->mbs.pos == strlen(str1) + && !strncmp(buf, str1, e->mbs.pos) + && !strcmp(&buf[e->mbs.pos], str2); +} + +int main(void) { + struct Edit e = { .mode = EditEmacs }; + + 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..57edfc1 --- /dev/null +++ b/edit.h @@ -0,0 +1,91 @@ +/* Copyright (C) 2022 June McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 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 +#include + +enum EditMode { + EditEmacs, +}; + +struct Edit { + enum EditMode mode; + wchar_t *buf; + size_t pos; + size_t len; + size_t cap; + struct { + wchar_t *buf; + size_t len; + } cut; + struct { + char *buf; + size_t pos; + size_t len; + } mbs; +}; + +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); + +// Perform a vi-mode editing function. +int editVi(struct Edit *e, wchar_t ch); + +// Insert a character at the cursor. +int editInsert(struct Edit *e, wchar_t ch); + +// Convert the buffer to a multi-byte string stored in e->mbs. +char *editString(struct Edit *e); + +// Free all buffers. +void editFree(struct Edit *e); + +// 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/ui.c b/ui.c index 3df0cd6..212d205 100644 --- a/ui.c +++ b/ui.c @@ -54,6 +54,7 @@ #endif #include "chat.h" +#include "edit.h" // Annoying stuff from : #undef lines @@ -752,9 +753,10 @@ static char *inputStop( return stop; } +static struct Edit edit; + static void inputUpdate(void) { - size_t pos; - char *buf = editBuffer(&pos); + char *buf = editString(&edit); struct Window *window = windows.ptrs[windows.show]; const char *prefix = ""; @@ -786,7 +788,7 @@ static void inputUpdate(void) { } else { prompt = ""; } - if (skip > &buf[pos]) { + if (skip > &buf[edit.mbs.pos]) { prefix = prompt = suffix = ""; skip = buf; } @@ -803,10 +805,10 @@ static void inputUpdate(void) { waddstr(input, suffix); getyx(input, y, x); - int posx; + int pos; struct Style style = styleInput; - inputStop(styleInput, &style, skip, &buf[pos]); - getyx(input, y, posx); + inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]); + getyx(input, y, pos); wmove(input, y, x); style = styleInput; @@ -818,7 +820,7 @@ static void inputUpdate(void) { } inputAdd(styleInput, &style, ptr); wclrtoeol(input); - wmove(input, y, posx); + wmove(input, y, pos); } void uiWindows(void) { @@ -965,6 +967,11 @@ static void showAuto(void) { } } +static void inputEnter(uint id) { + command(id, editString(&edit)); + editFn(&edit, EditClear); +} + static void keyCode(int code) { struct Window *window = windows.ptrs[windows.show]; uint id = window->id; @@ -973,7 +980,7 @@ static void keyCode(int code) { break; case KeyFocusIn: unmark(window); break; case KeyFocusOut: mark(window); - break; case KeyMetaEnter: edit(id, EditInsert, L'\n'); + break; case KeyMetaEnter: editInsert(&edit, L'\n'); break; case KeyMetaEqual: window->mute ^= true; statusUpdate(); break; case KeyMetaMinus: incThresh(window, -1); break; case KeyMetaPlus: incThresh(window, +1); @@ -984,32 +991,32 @@ static void keyCode(int code) { 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 KeyMetaB: editFn(&edit, EditPrevWord); + break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); + break; case KeyMetaF: editFn(&edit, EditNextWord); 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 KeyMetaQ: editFn(&edit, EditCollapse); break; case KeyMetaS: spoilerReveal ^= true; mainUpdate(); 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 KeyCtrlLeft: editFn(&edit, EditPrevWord); + break; case KeyCtrlRight: editFn(&edit, EditNextWord); - break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); - break; case KEY_DC: edit(id, EditDeleteNext, 0); + break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); + break; case KEY_DC: editFn(&edit, EditDeleteNext); 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_END: editFn(&edit, EditTail); + break; case KEY_ENTER: inputEnter(id); + break; case KEY_HOME: editFn(&edit, EditHead); + break; case KEY_LEFT: editFn(&edit, EditPrev); 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_RIGHT: editFn(&edit, EditNext); break; case KEY_SEND: scrollTo(window, 0); break; case KEY_SHOME: scrollTo(window, BufferCap); break; case KEY_UP: windowScroll(window, +1); @@ -1020,33 +1027,30 @@ 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'?': editFn(&edit, EditDeletePrev); + break; case L'A': editFn(&edit, EditHead); + break; case L'B': editFn(&edit, EditPrev); 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'D': editFn(&edit, EditDeleteNext); + break; case L'E': editFn(&edit, EditTail); + break; case L'F': editFn(&edit, EditNext); + break; case L'H': editFn(&edit, EditDeletePrev); + break; case L'J': inputEnter(id); + break; case L'K': editFn(&edit, EditDeleteTail); 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'R': scrollSearch(window, editString(&edit), -1); + break; case L'S': scrollSearch(window, editString(&edit), +1); + break; case L'T': editFn(&edit, EditTranspose); + break; case L'U': editFn(&edit, EditDeleteHead); 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); + break; case L'W': editFn(&edit, EditDeletePrevWord); + break; case L'Y': editFn(&edit, EditPaste); } } static void keyStyle(wchar_t ch) { - uint id = windows.ptrs[windows.show]->id; if (iswcntrl(ch)) ch = towlower(ch ^ L'@'); char buf[8] = {0}; enum Color color = Default; @@ -1077,7 +1081,7 @@ static void keyStyle(wchar_t ch) { snprintf(buf, sizeof(buf), "%c%02d", C, color); } for (char *ch = buf; *ch; ++ch) { - edit(id, EditInsert, *ch); + editInsert(&edit, *ch); } } @@ -1103,7 +1107,7 @@ void uiRead(void) { } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { paste ^= true; } else if (paste || literal) { - edit(windows.ptrs[windows.show]->id, EditInsert, ch); + editInsert(&edit, ch); } else if (ret == KEY_CODE_YES) { keyCode(ch); } else if (ch == (L'Z' ^ L'@')) { @@ -1117,7 +1121,7 @@ void uiRead(void) { } else if (iswcntrl(ch)) { keyCtrl(ch); } else { - edit(windows.ptrs[windows.show]->id, EditInsert, ch); + editInsert(&edit, ch); } style = false; literal = false; -- cgit 1.4.1 From 06d17aa8f114360f2b5a3e6e1fa011bfc2d806ba Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 00:01:34 -0500 Subject: Run line editing tests I know, it feels wrong. --- .gitignore | 1 + Makefile | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e96e0c1..b31d1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.o +*.t catgirl chroot.tar config.mk diff --git a/Makefile b/Makefile index 9e6392b..fccee7f 100644 --- a/Makefile +++ b/Makefile @@ -25,20 +25,30 @@ OBJS += ui.o OBJS += url.o OBJS += xdg.o -dev: tags all +TESTS += edit.t + +dev: tags all check all: catgirl catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ -${OBJS}: chat.h edit.h +${OBJS} ${TESTS}: chat.h edit.h + +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 catgirl ${OBJS} ${TESTS} tags install: catgirl catgirl.1 install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1 -- cgit 1.4.1 From a2a118c85792f7da85630a07d343ad49204e8ae6 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 14:51:26 -0500 Subject: Fix edit.[ch] license notice additional permissions --- edit.c | 2 ++ edit.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/edit.c b/edit.c index 21061d7..5766b29 100644 --- a/edit.c +++ b/edit.c @@ -13,6 +13,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * + * 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 diff --git a/edit.h b/edit.h index 57edfc1..d15e5fb 100644 --- a/edit.h +++ b/edit.h @@ -13,6 +13,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * + * 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 -- cgit 1.4.1 From 70f627bc47e944a63f32fcf60db73913845772c6 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 17:46:07 -0500 Subject: Enable -Wmissing-prototypes In other words, warn when a function is missing static. I don't see why this isn't in -Wextra. --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fccee7f..fd06260 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ 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 -- cgit 1.4.1 From 3359a5d69b0fe3c08812f7db83e27958ffec820f Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 18:28:45 -0500 Subject: Factor out window management to window.c --- Makefile | 1 + README.7 | 4 +- chat.c | 8 +- chat.h | 74 +++++-- command.c | 22 +- handle.c | 2 +- ui.c | 735 ++++++++------------------------------------------------------ window.c | 656 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 828 insertions(+), 674 deletions(-) create mode 100644 window.c diff --git a/Makefile b/Makefile index fd06260..dffe4e8 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ OBJS += irc.o OBJS += log.o OBJS += ui.o OBJS += url.o +OBJS += window.o OBJS += xdg.o TESTS += edit.t diff --git a/README.7 b/README.7 index 8f13f7c..b98b589 100644 --- a/README.7 +++ b/README.7 @@ -1,5 +1,5 @@ .\" To view this file, run: man ./README.7 -.Dd February 12, 2022 +.Dd February 19, 2022 .Dt README 7 .Os "Causal Agency" . @@ -181,6 +181,8 @@ startup and event loop IRC connection and parsing .It Pa ui.c curses interface +.It Pa window.c +window management .It Pa handle.c IRC message handling .It Pa command.c diff --git a/chat.c b/chat.c index 454ae31..57cae6d 100644 --- a/chat.c +++ b/chat.c @@ -293,8 +293,8 @@ int main(int argc, char *argv[]) { 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; parsePlain(optarg); break; case 'c': cert = optarg; @@ -309,7 +309,7 @@ int main(int argc, char *argv[]) { break; case 'n': nick = 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; @@ -381,7 +381,7 @@ int main(int argc, char *argv[]) { uiLoad(save); atexit(exitSave); } - uiShowID(Network); + windowShow(windowFor(Network)); uiFormat( Network, Cold, NULL, "\3%dcatgirl\3\tis GPLv3 fwee softwawe ^w^ " diff --git a/chat.h b/chat.h index 8a9a48f..e9a2926 100644 --- a/chat.h +++ b/chat.h @@ -293,26 +293,34 @@ const char *commandIsAction(uint id, const char *input); size_t commandWillSplit(uint id, const char *input); void commandCompleteAdd(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 Util uiNotifyUtil; void uiInitEarly(void); void uiInitLate(void); +uint uiAttr(struct Style style); +short uiPair(struct Style style); +void uiUpdate(void); void uiShow(void); void uiHide(void); +void uiWait(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 uiWrite(uint id, enum Heat heat, const time_t *time, const char *str); void uiFormat( @@ -321,6 +329,44 @@ void uiFormat( void uiLoad(const char *name); int uiSave(void); +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 { diff --git a/command.c b/command.c index 335c396..a127af3 100644 --- a/command.c +++ b/command.c @@ -144,7 +144,7 @@ static void commandMsg(uint id, char *params) { if (params) { splitMessage("PRIVMSG", msg, params); } else { - uiShowID(msg); + windowShow(windowFor(msg)); } } @@ -376,25 +376,25 @@ 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); if (!id) continue; completeAccept(); - uiShowID(id); + windowShow(windowFor(id)); break; } } @@ -405,20 +405,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)); } } diff --git a/handle.c b/handle.c index d663ca1..f4cf68e 100644 --- a/handle.c +++ b/handle.c @@ -343,7 +343,7 @@ static void handleJoin(struct Message *msg) { idColors[id] = hash(msg->params[0]); completeTouch(None, msg->params[0], idColors[id]); if (replies[ReplyJoin]) { - uiShowID(id); + windowShow(windowFor(id)); replies[ReplyJoin]--; } } diff --git a/ui.c b/ui.c index 212d205..85b5a72 100644 --- a/ui.c +++ b/ui.c @@ -33,15 +33,15 @@ #include #include #include -#include +#include #include #include #include #include #include #include -#include #include +#include #include #include #include @@ -60,102 +60,14 @@ #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; +WINDOW *uiStatus; +WINDOW *uiMain; 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); -} - static short colorPairs; static void colorInit(void) { @@ -240,8 +152,6 @@ enum { 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]); @@ -271,30 +181,18 @@ void uiInitEarly(void) { ENUM_KEY #undef X - status = newwin(StatusLines, COLS, 0, 0); - if (!status) err(EX_OSERR, "newwin"); - - main = newwin(MAIN_LINES, COLS, StatusLines, 0); - if (!main) 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; + uiStatus = newwin(StatusLines, COLS, 0, 0); + if (!uiStatus) err(EX_OSERR, "newwin"); + + uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); + if (!uiMain) err(EX_OSERR, "newwin"); input = newpad(InputLines, InputCols); if (!input) err(EX_OSERR, "newpad"); keypad(input, true); nodelay(input, true); - windowFor(Network); + windowInit(); uiShow(); } @@ -323,13 +221,13 @@ void uiInitLate(void) { 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); pnoutrefresh( @@ -342,10 +240,10 @@ void uiDraw(void) { doupdate(); if (!to_status_line) return; - if (!strcmp(title, prevTitle)) return; - strcpy(prevTitle, title); + if (!strcmp(uiTitle, prevTitle)) return; + strcpy(prevTitle, uiTitle); putp(to_status_line); - putp(title); + putp(uiTitle); putp(from_status_line); fflush(stdout); } @@ -377,7 +275,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; @@ -388,96 +286,13 @@ static attr_t styleAttr(struct Style style) { static bool spoilerReveal; -static short stylePair(struct Style style) { +short uiPair(struct Style style) { if (spoilerReveal && style.fg == style.bg) { return colorPair(Colors[Default], Colors[style.bg]); } 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; -} - -static void unmark(struct Window *window) { - if (!window->scroll) { - window->mark = false; - window->heat = Cold; - } - statusUpdate(); -} - void uiShow(void) { if (!hidden) return; prevTitle[0] = '\0'; @@ -485,86 +300,20 @@ 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(); +void uiWait(void) { + waiting = true; } struct Util uiNotifyUtil; @@ -593,36 +342,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); } @@ -640,78 +361,10 @@ 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"); - } + wclear(uiMain); + wresize(uiMain, MAIN_LINES, COLS); + windowResize(); } static void inputAdd(struct Style reset, struct Style *style, const char *str) { @@ -736,7 +389,7 @@ static void inputAdd(struct Style reset, struct Style *style, const char *str) { } size_t nl = strcspn(str, "\n"); if (nl < len) len = nl; - wattr_set(input, styleAttr(*style), stylePair(*style), NULL); + wattr_set(input, uiAttr(*style), uiPair(*style), NULL); waddnstr(input, str, len); str += len; } @@ -755,9 +408,9 @@ static char *inputStop( static struct Edit edit; -static void inputUpdate(void) { +void uiUpdate(void) { char *buf = editString(&edit); - struct Window *window = windows.ptrs[windows.show]; + uint id = windowID(); const char *prefix = ""; const char *prompt = self.nick; @@ -766,10 +419,10 @@ static void inputUpdate(void) { 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); + 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; @@ -782,7 +435,7 @@ static void inputUpdate(void) { stylePrompt.attr |= Italic; styleInput.attr |= Italic; skip = action; - } else if (window->id == Debug && buf[0] != '/') { + } else if (id == Debug && buf[0] != '/') { prompt = "<< "; stylePrompt.fg = Gray; } else { @@ -795,11 +448,11 @@ static void inputUpdate(void) { int y, x; wmove(input, 0, 0); - if (window->time && window->id != Network) { - whline(input, ' ', uiTime.width); - wmove(input, 0, uiTime.width); + if (windowTimeEnable() && id != Network) { + whline(input, ' ', windowTime.width); + wmove(input, 0, windowTime.width); } - wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), NULL); + wattr_set(input, uiAttr(stylePrompt), uiPair(stylePrompt), NULL); waddstr(input, prefix); waddstr(input, prompt); waddstr(input, suffix); @@ -823,209 +476,61 @@ static void inputUpdate(void) { wmove(input, y, pos); } -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 inputEnter(uint id) { - command(id, editString(&edit)); +static void inputEnter(void) { + command(windowID(), editString(&edit)); editFn(&edit, EditClear); } 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 KeyFocusIn: windowUnmark(); + break; case KeyFocusOut: windowMark(); break; case KeyMetaEnter: editInsert(&edit, 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 KeyMetaEqual: windowToggleMute(); + break; case KeyMetaMinus: windowToggleThresh(-1); + break; case KeyMetaPlus: windowToggleThresh(+1); + break; case KeyMetaSlash: windowSwap(); - break; case KeyMetaGt: scrollTo(window, 0); - break; case KeyMetaLt: scrollTop(window); + break; case KeyMetaGt: windowScroll(ScrollAll, -1); + break; case KeyMetaLt: windowScroll(ScrollAll, +1); - break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0); - break; case KeyMetaA: showAuto(); + break; case KeyMeta0 ... KeyMeta9: windowShow(code - KeyMeta0); + break; case KeyMetaA: windowAuto(); break; case KeyMetaB: editFn(&edit, EditPrevWord); break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); break; case KeyMetaF: editFn(&edit, EditNextWord); - 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 KeyMetaL: windowBare(); + break; case KeyMetaM: uiWrite(windowID(), Warm, NULL, ""); + break; case KeyMetaN: windowScroll(ScrollHot, +1); + break; case KeyMetaP: windowScroll(ScrollHot, -1); break; case KeyMetaQ: editFn(&edit, EditCollapse); - break; case KeyMetaS: spoilerReveal ^= true; mainUpdate(); - break; case KeyMetaT: toggleTime(window); - break; case KeyMetaU: scrollTo(window, window->unreadHard); - break; case KeyMetaV: scrollPage(window, +1); + break; case KeyMetaS: spoilerReveal ^= true; windowUpdate(); + break; case KeyMetaT: windowToggleTime(); + break; case KeyMetaU: windowScroll(ScrollUnread, 0); + break; case KeyMetaV: windowScroll(ScrollPage, +1); break; case KeyCtrlLeft: editFn(&edit, EditPrevWord); break; case KeyCtrlRight: editFn(&edit, EditNextWord); break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); break; case KEY_DC: editFn(&edit, EditDeleteNext); - break; case KEY_DOWN: windowScroll(window, -1); + break; case KEY_DOWN: windowScroll(ScrollOne, -1); break; case KEY_END: editFn(&edit, EditTail); - break; case KEY_ENTER: inputEnter(id); + break; case KEY_ENTER: inputEnter(); break; case KEY_HOME: editFn(&edit, EditHead); break; case KEY_LEFT: editFn(&edit, EditPrev); - break; case KEY_NPAGE: scrollPage(window, -1); - break; case KEY_PPAGE: scrollPage(window, +1); + break; case KEY_NPAGE: windowScroll(ScrollPage, -1); + break; case KEY_PPAGE: windowScroll(ScrollPage, +1); break; case KEY_RIGHT: editFn(&edit, EditNext); - break; case KEY_SEND: scrollTo(window, 0); - break; case KEY_SHOME: scrollTo(window, BufferCap); - break; case KEY_UP: windowScroll(window, +1); + break; case KEY_SEND: windowScroll(ScrollAll, -1); + break; case KEY_SHOME: windowScroll(ScrollAll, +1); + break; case KEY_UP: windowScroll(ScrollOne, +1); } } static void keyCtrl(wchar_t ch) { - struct Window *window = windows.ptrs[windows.show]; - uint id = window->id; switch (ch ^ L'@') { break; case L'?': editFn(&edit, EditDeletePrev); break; case L'A': editFn(&edit, EditHead); @@ -1035,16 +540,16 @@ static void keyCtrl(wchar_t ch) { break; case L'E': editFn(&edit, EditTail); break; case L'F': editFn(&edit, EditNext); break; case L'H': editFn(&edit, EditDeletePrev); - break; case L'J': inputEnter(id); + break; case L'J': inputEnter(); break; case L'K': editFn(&edit, EditDeleteTail); 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, editString(&edit), -1); - break; case L'S': scrollSearch(window, editString(&edit), +1); + break; case L'N': windowShow(windowNum() + 1); + break; case L'P': windowShow(windowNum() - 1); + break; case L'R': windowSearch(editString(&edit), -1); + break; case L'S': windowSearch(editString(&edit), +1); break; case L'T': editFn(&edit, EditTranspose); break; case L'U': editFn(&edit, EditDeleteHead); - break; case L'V': scrollPage(window, -1); + break; case L'V': windowScroll(ScrollPage, -1); break; case L'W': editFn(&edit, EditDeletePrevWord); break; case L'Y': editFn(&edit, EditPaste); } @@ -1127,13 +632,15 @@ void uiRead(void) { literal = false; if (spr) { spoilerReveal = false; - mainUpdate(); + windowUpdate(); } } - inputUpdate(); + uiUpdate(); } -static const time_t Signatures[] = { +static FILE *saveFile; + +static const uint64_t Signatures[] = { 0x6C72696774616301, // no heat, unread, unreadWarm 0x6C72696774616302, // no self.pos 0x6C72696774616303, // no buffer line heat @@ -1144,68 +651,33 @@ static const time_t Signatures[] = { 0x6C72696774616308, }; -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 file signature %" PRIX64, signature); } -static int writeTime(FILE *file, time_t time) { - return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1); +static int writeUint64(FILE *file, uint64_t u) { + return (fwrite(&u, sizeof(u), 1, file) ? 0 : -1); } -static int writeString(FILE *file, const char *str) { - return (fwrite(str, strlen(str) + 1, 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[7]) + || writeUint64(saveFile, self.pos) + || windowSave(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; + return u; } void uiLoad(const char *name) { @@ -1235,31 +707,8 @@ 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); urlLoad(saveFile, version); - - free(buf); } diff --git a/window.c b/window.c new file mode 100644 index 0000000..ce3ec4d --- /dev/null +++ b/window.c @@ -0,0 +1,656 @@ +/* Copyright (C) 2020 June McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(); + completeAdd(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); + 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) { + 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%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(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(); + uiWait(); + + 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(); + uiUpdate(); +} + +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); + completeClear(window->id); + 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(); + uiUpdate(); +} + +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 eof"); + return time; +} + +static ssize_t readString(FILE *file, char **buf, size_t *cap) { + ssize_t len = getdelim(buf, cap, '\0', file); + if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim"); + return len; +} + +void 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); +} -- cgit 1.4.1 From 073cebec7a5a07ab2b829e40ce47ec3b1d774bd9 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 20:20:19 -0500 Subject: Factor out input handling to input.c --- Makefile | 5 +- README.7 | 4 +- chat.c | 9 +- chat.h | 14 ++- handle.c | 2 +- input.c | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ui.c | 369 ++--------------------------------------------------------- window.c | 6 +- 8 files changed, 424 insertions(+), 375 deletions(-) create mode 100644 input.c diff --git a/Makefile b/Makefile index dffe4e8..3abba03 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ OBJS += config.o OBJS += edit.o OBJS += filter.o OBJS += handle.o +OBJS += input.o OBJS += irc.o OBJS += log.o OBJS += ui.o @@ -36,7 +37,9 @@ all: catgirl catgirl: ${OBJS} ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ -${OBJS} ${TESTS}: chat.h edit.h +${OBJS}: chat.h + +edit.o edit.t input.o: edit.h check: ${TESTS} diff --git a/README.7 b/README.7 index b98b589..dd0020d 100644 --- a/README.7 +++ b/README.7 @@ -183,10 +183,12 @@ IRC connection and parsing 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 diff --git a/chat.c b/chat.c index 57cae6d..c4b256c 100644 --- a/chat.c +++ b/chat.c @@ -375,7 +375,7 @@ int main(int argc, char *argv[]) { ircConfig(insecure, trust, cert, priv); - uiInitEarly(); + uiInit(); sig_t cursesWinch = signal(SIGWINCH, signalHandler); if (save) { uiLoad(save); @@ -407,7 +407,8 @@ int main(int argc, char *argv[]) { ircFormat("NICK :%s\r\n", nick); 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); @@ -436,7 +437,7 @@ int main(int argc, char *argv[]) { int nfds = poll(fds, (pipes ? ARRAY_LEN(fds) : 2), -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(); @@ -488,7 +489,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 e9a2926..1bf1edd 100644 --- a/chat.h +++ b/chat.h @@ -311,17 +311,16 @@ enum { 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 uiUpdate(void); void uiShow(void); void uiHide(void); -void uiWait(void); void uiDraw(void); -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, ... @@ -329,6 +328,11 @@ void uiFormat( void uiLoad(const char *name); int uiSave(void); +void inputInit(void); +void inputWait(void); +void inputUpdate(void); +void inputRead(void); + enum Scroll { ScrollOne, ScrollPage, diff --git a/handle.c b/handle.c index f4cf68e..9f051c7 100644 --- a/handle.c +++ b/handle.c @@ -425,7 +425,7 @@ 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));) { if (!strcmp(idNames[id], msg->nick)) { diff --git a/input.c b/input.c new file mode 100644 index 0000000..963bd4e --- /dev/null +++ b/input.c @@ -0,0 +1,390 @@ +/* Copyright (C) 2020 June McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 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 +#include +#include +#include +#include +#include +#include + +#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 edit; + +void inputInit(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(); + +#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; +} + +void inputUpdate(void) { + uint id = windowID(); + char *buf = editString(&edit); + + 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[edit.mbs.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 pos; + struct Style style = styleInput; + inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]); + getyx(uiInput, y, pos); + wmove(uiInput, y, x); + + style = styleInput; + const char *ptr = skip; + if (split) { + ptr = inputStop(styleInput, &style, ptr, &buf[split]); + style = styleInput; + style.bg = Red; + } + inputAdd(styleInput, &style, ptr); + wclrtoeol(uiInput); + wmove(uiInput, y, pos); +} + +static void inputEnter(void) { + command(windowID(), editString(&edit)); + editFn(&edit, EditClear); +} + +static void keyCode(int code) { + switch (code) { + break; case KEY_RESIZE: uiResize(); + break; case KeyFocusIn: windowUnmark(); + break; case KeyFocusOut: windowMark(); + + break; case KeyMetaEnter: 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: editFn(&edit, EditPrevWord); + break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); + break; case KeyMetaF: 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: 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: editFn(&edit, EditPrevWord); + break; case KeyCtrlRight: editFn(&edit, EditNextWord); + + break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); + break; case KEY_DC: editFn(&edit, EditDeleteNext); + break; case KEY_DOWN: windowScroll(ScrollOne, -1); + break; case KEY_END: editFn(&edit, EditTail); + break; case KEY_ENTER: inputEnter(); + break; case KEY_HOME: editFn(&edit, EditHead); + break; case KEY_LEFT: editFn(&edit, EditPrev); + break; case KEY_NPAGE: windowScroll(ScrollPage, -1); + break; case KEY_PPAGE: windowScroll(ScrollPage, +1); + break; case KEY_RIGHT: editFn(&edit, EditNext); + break; case KEY_SEND: windowScroll(ScrollAll, -1); + break; case KEY_SHOME: windowScroll(ScrollAll, +1); + break; case KEY_UP: windowScroll(ScrollOne, +1); + } +} + +static void keyCtrl(wchar_t ch) { + switch (ch ^ L'@') { + break; case L'?': editFn(&edit, EditDeletePrev); + break; case L'A': editFn(&edit, EditHead); + break; case L'B': editFn(&edit, EditPrev); + break; case L'C': raise(SIGINT); + break; case L'D': editFn(&edit, EditDeleteNext); + break; case L'E': editFn(&edit, EditTail); + break; case L'F': editFn(&edit, EditNext); + break; case L'H': editFn(&edit, EditDeletePrev); + break; case L'J': inputEnter(); + break; case L'K': editFn(&edit, EditDeleteTail); + break; case L'L': clearok(curscr, true); + break; case L'N': windowShow(windowNum() + 1); + break; case L'P': windowShow(windowNum() - 1); + break; case L'R': windowSearch(editString(&edit), -1); + break; case L'S': windowSearch(editString(&edit), +1); + break; case L'T': editFn(&edit, EditTranspose); + break; case L'U': editFn(&edit, EditDeleteHead); + break; case L'V': windowScroll(ScrollPage, -1); + break; case L'W': editFn(&edit, EditDeletePrevWord); + break; case L'Y': editFn(&edit, EditPaste); + } +} + +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); + } + for (char *ch = buf; *ch; ++ch) { + editInsert(&edit, *ch); + } +} + +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 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) { + editInsert(&edit, 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 { + editInsert(&edit, ch); + } + style = false; + literal = false; + if (spr) { + uiSpoilerReveal = false; + windowUpdate(); + } + } + inputUpdate(); +} diff --git a/ui.c b/ui.c index 85b5a72..d24f7a7 100644 --- a/ui.c +++ b/ui.c @@ -54,7 +54,6 @@ #endif #include "chat.h" -#include "edit.h" // Annoying stuff from : #undef lines @@ -66,7 +65,7 @@ WINDOW *uiStatus; WINDOW *uiMain; -static WINDOW *input; +WINDOW *uiInput; static short colorPairs; @@ -101,52 +100,6 @@ 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(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 -}; - // 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" }; @@ -158,7 +111,7 @@ static void errExit(void) { reset_shell_mode(); } -void uiInitEarly(void) { +void uiInit(void) { initscr(); cbreak(); noecho(); @@ -177,49 +130,20 @@ 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 - uiStatus = newwin(StatusLines, COLS, 0, 0); if (!uiStatus) err(EX_OSERR, "newwin"); uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); if (!uiMain) err(EX_OSERR, "newwin"); - input = newpad(InputLines, InputCols); - if (!input) err(EX_OSERR, "newpad"); - keypad(input, true); - nodelay(input, true); + uiInput = newpad(InputLines, InputCols); + if (!uiInput) err(EX_OSERR, "newpad"); 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; char uiTitle[TitleCap]; static char prevTitle[TitleCap]; @@ -229,9 +153,9 @@ void uiDraw(void) { 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 @@ -284,10 +208,10 @@ uint uiAttr(struct Style style) { return attr | colorAttr(Colors[style.fg]); } -static bool spoilerReveal; +bool uiSpoilerReveal; short uiPair(struct Style style) { - if (spoilerReveal && style.fg == style.bg) { + if (uiSpoilerReveal && style.fg == style.bg) { return colorPair(Colors[Default], Colors[style.bg]); } return colorPair(Colors[style.fg], Colors[style.bg]); @@ -312,10 +236,6 @@ void uiHide(void) { endwin(); } -void uiWait(void) { - waiting = true; -} - struct Util uiNotifyUtil; static void notify(uint id, const char *str) { if (self.restricted) return; @@ -361,283 +281,12 @@ void uiFormat( uiWrite(id, heat, time, buf); } -static void resize(void) { +void uiResize(void) { wclear(uiMain); wresize(uiMain, MAIN_LINES, COLS); windowResize(); } -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, uiAttr(*style), uiPair(*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 struct Edit edit; - -void uiUpdate(void) { - char *buf = editString(&edit); - uint id = windowID(); - - 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[edit.mbs.pos]) { - prefix = prompt = suffix = ""; - skip = buf; - } - - int y, x; - wmove(input, 0, 0); - if (windowTimeEnable() && id != Network) { - whline(input, ' ', windowTime.width); - wmove(input, 0, windowTime.width); - } - wattr_set(input, uiAttr(stylePrompt), uiPair(stylePrompt), NULL); - waddstr(input, prefix); - waddstr(input, prompt); - waddstr(input, suffix); - getyx(input, y, x); - - int pos; - struct Style style = styleInput; - inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]); - getyx(input, y, pos); - wmove(input, y, x); - - style = styleInput; - const char *ptr = skip; - if (split) { - ptr = inputStop(styleInput, &style, ptr, &buf[split]); - style = styleInput; - style.bg = Red; - } - inputAdd(styleInput, &style, ptr); - wclrtoeol(input); - wmove(input, y, pos); -} - -static void inputEnter(void) { - command(windowID(), editString(&edit)); - editFn(&edit, EditClear); -} - -static void keyCode(int code) { - switch (code) { - break; case KEY_RESIZE: resize(); - break; case KeyFocusIn: windowUnmark(); - break; case KeyFocusOut: windowMark(); - - break; case KeyMetaEnter: 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: editFn(&edit, EditPrevWord); - break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); - break; case KeyMetaF: 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: editFn(&edit, EditCollapse); - break; case KeyMetaS: spoilerReveal ^= true; windowUpdate(); - break; case KeyMetaT: windowToggleTime(); - break; case KeyMetaU: windowScroll(ScrollUnread, 0); - break; case KeyMetaV: windowScroll(ScrollPage, +1); - - break; case KeyCtrlLeft: editFn(&edit, EditPrevWord); - break; case KeyCtrlRight: editFn(&edit, EditNextWord); - - break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); - break; case KEY_DC: editFn(&edit, EditDeleteNext); - break; case KEY_DOWN: windowScroll(ScrollOne, -1); - break; case KEY_END: editFn(&edit, EditTail); - break; case KEY_ENTER: inputEnter(); - break; case KEY_HOME: editFn(&edit, EditHead); - break; case KEY_LEFT: editFn(&edit, EditPrev); - break; case KEY_NPAGE: windowScroll(ScrollPage, -1); - break; case KEY_PPAGE: windowScroll(ScrollPage, +1); - break; case KEY_RIGHT: editFn(&edit, EditNext); - break; case KEY_SEND: windowScroll(ScrollAll, -1); - break; case KEY_SHOME: windowScroll(ScrollAll, +1); - break; case KEY_UP: windowScroll(ScrollOne, +1); - } -} - -static void keyCtrl(wchar_t ch) { - switch (ch ^ L'@') { - break; case L'?': editFn(&edit, EditDeletePrev); - break; case L'A': editFn(&edit, EditHead); - break; case L'B': editFn(&edit, EditPrev); - break; case L'C': raise(SIGINT); - break; case L'D': editFn(&edit, EditDeleteNext); - break; case L'E': editFn(&edit, EditTail); - break; case L'F': editFn(&edit, EditNext); - break; case L'H': editFn(&edit, EditDeletePrev); - break; case L'J': inputEnter(); - break; case L'K': editFn(&edit, EditDeleteTail); - break; case L'L': clearok(curscr, true); - break; case L'N': windowShow(windowNum() + 1); - break; case L'P': windowShow(windowNum() - 1); - break; case L'R': windowSearch(editString(&edit), -1); - break; case L'S': windowSearch(editString(&edit), +1); - break; case L'T': editFn(&edit, EditTranspose); - break; case L'U': editFn(&edit, EditDeleteHead); - break; case L'V': windowScroll(ScrollPage, -1); - break; case L'W': editFn(&edit, EditDeletePrevWord); - break; case L'Y': editFn(&edit, EditPaste); - } -} - -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); - } - for (char *ch = buf; *ch; ++ch) { - editInsert(&edit, *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));) { - bool spr = spoilerReveal; - 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) { - editInsert(&edit, 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 { - editInsert(&edit, ch); - } - style = false; - literal = false; - if (spr) { - spoilerReveal = false; - windowUpdate(); - } - } - uiUpdate(); -} - static FILE *saveFile; static const uint64_t Signatures[] = { diff --git a/window.c b/window.c index ce3ec4d..0395542 100644 --- a/window.c +++ b/window.c @@ -273,7 +273,7 @@ void windowUpdate(void) { void windowBare(void) { uiHide(); - uiWait(); + inputWait(); const struct Window *window = windows[show]; const struct Line *line = bufferHard(window->buffer, windowBottom(window)); @@ -426,7 +426,7 @@ void windowShow(uint num) { user = num; unmark(windows[show]); mainUpdate(); - uiUpdate(); + inputUpdate(); } void windowAuto(void) { @@ -515,7 +515,7 @@ void windowToggleTime(void) { windows[show]->time ^= true; reflow(windows[show]); windowUpdate(); - uiUpdate(); + inputUpdate(); } void windowToggleThresh(int n) { -- cgit 1.4.1 From 143fa1b50965645dc0ebafecdd5db192f4454695 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 20:37:48 -0500 Subject: Reimplement text macros --- README.7 | 2 +- chat.c | 1 + chat.h | 1 + input.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.7 b/README.7 index dd0020d..32ee50f 100644 --- a/README.7 +++ b/README.7 @@ -132,7 +132,7 @@ $ make all .Pp Packagers are encouraged to patch in their own text macros in -.Pa edit.c . +.Pa input.c . . .Pp If installing diff --git a/chat.c b/chat.c index c4b256c..b085165 100644 --- a/chat.c +++ b/chat.c @@ -372,6 +372,7 @@ int main(int argc, char *argv[]) { set(&self.nick, "*"); commandCompleteAdd(); + inputCompleteAdd(); ircConfig(insecure, trust, cert, priv); diff --git a/chat.h b/chat.h index 1bf1edd..0de930d 100644 --- a/chat.h +++ b/chat.h @@ -332,6 +332,7 @@ void inputInit(void); void inputWait(void); void inputUpdate(void); void inputRead(void); +void inputCompleteAdd(void); enum Scroll { ScrollOne, diff --git a/input.c b/input.c index 963bd4e..45913cc 100644 --- a/input.c +++ b/input.c @@ -27,6 +27,7 @@ #define _XOPEN_SOURCE_EXTENDED +#include #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include "chat.h" #include "edit.h" @@ -221,6 +223,54 @@ void inputUpdate(void) { wmove(uiInput, y, pos); } +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 inputCompleteAdd(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); + } +} + +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 void inputEnter(void) { command(windowID(), editString(&edit)); editFn(&edit, EditClear); @@ -296,6 +346,7 @@ static void keyCtrl(wchar_t ch) { break; case L'U': editFn(&edit, EditDeleteHead); break; case L'V': windowScroll(ScrollPage, -1); break; case L'W': editFn(&edit, EditDeletePrevWord); + break; case L'X': macroExpand(&edit); break; case L'Y': editFn(&edit, EditPaste); } } -- cgit 1.4.1 From da8deff7679431267dbb6aee91305f1ea62de91f Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 20:44:36 -0500 Subject: Handle errors from editFn, etc. --- input.c | 71 +++++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/input.c b/input.c index 45913cc..2d79c7b 100644 --- a/input.c +++ b/input.c @@ -158,6 +158,7 @@ static char *inputStop( void inputUpdate(void) { uint id = windowID(); char *buf = editString(&edit); + if (!buf) err(EX_OSERR, "editString"); const char *prefix = ""; const char *prompt = self.nick; @@ -272,17 +273,21 @@ static int macroExpand(struct Edit *e) { } static void inputEnter(void) { - command(windowID(), editString(&edit)); + char *cmd = editString(&edit); + if (!cmd) err(EX_OSERR, "editString"); + + command(windowID(), cmd); editFn(&edit, EditClear); } static void keyCode(int code) { + int error = 0; switch (code) { break; case KEY_RESIZE: uiResize(); break; case KeyFocusIn: windowUnmark(); break; case KeyFocusOut: windowMark(); - break; case KeyMetaEnter: editInsert(&edit, L'\n'); + break; case KeyMetaEnter: error = editInsert(&edit, L'\n'); break; case KeyMetaEqual: windowToggleMute(); break; case KeyMetaMinus: windowToggleThresh(-1); break; case KeyMetaPlus: windowToggleThresh(+1); @@ -293,62 +298,65 @@ static void keyCode(int code) { break; case KeyMeta0 ... KeyMeta9: windowShow(code - KeyMeta0); break; case KeyMetaA: windowAuto(); - break; case KeyMetaB: editFn(&edit, EditPrevWord); - break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); - break; case KeyMetaF: editFn(&edit, EditNextWord); + 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: editFn(&edit, EditCollapse); + 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: editFn(&edit, EditPrevWord); - break; case KeyCtrlRight: editFn(&edit, EditNextWord); + break; case KeyCtrlLeft: error = editFn(&edit, EditPrevWord); + break; case KeyCtrlRight: error = editFn(&edit, EditNextWord); - break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); - break; case KEY_DC: editFn(&edit, EditDeleteNext); + 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: editFn(&edit, EditTail); + break; case KEY_END: error = editFn(&edit, EditTail); break; case KEY_ENTER: inputEnter(); - break; case KEY_HOME: editFn(&edit, EditHead); - break; case KEY_LEFT: editFn(&edit, EditPrev); + 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: editFn(&edit, EditNext); + 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; switch (ch ^ L'@') { - break; case L'?': editFn(&edit, EditDeletePrev); - break; case L'A': editFn(&edit, EditHead); - break; case L'B': editFn(&edit, EditPrev); + 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': editFn(&edit, EditDeleteNext); - break; case L'E': editFn(&edit, EditTail); - break; case L'F': editFn(&edit, EditNext); - break; case L'H': editFn(&edit, EditDeletePrev); + 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'J': inputEnter(); - break; case L'K': editFn(&edit, EditDeleteTail); + break; case L'K': error = editFn(&edit, EditDeleteTail); break; case L'L': clearok(curscr, true); break; case L'N': windowShow(windowNum() + 1); break; case L'P': windowShow(windowNum() - 1); break; case L'R': windowSearch(editString(&edit), -1); break; case L'S': windowSearch(editString(&edit), +1); - break; case L'T': editFn(&edit, EditTranspose); - break; case L'U': editFn(&edit, EditDeleteHead); + 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': editFn(&edit, EditDeletePrevWord); - break; case L'X': macroExpand(&edit); - break; case L'Y': editFn(&edit, EditPaste); + break; case L'W': error = editFn(&edit, EditDeletePrevWord); + break; case L'X': error = macroExpand(&edit); + break; case L'Y': error = editFn(&edit, EditPaste); } + if (error) err(EX_OSERR, "editFn"); } static void keyStyle(wchar_t ch) { @@ -382,7 +390,8 @@ static void keyStyle(wchar_t ch) { snprintf(buf, sizeof(buf), "%c%02d", C, color); } for (char *ch = buf; *ch; ++ch) { - editInsert(&edit, *ch); + int error = editInsert(&edit, *ch); + if (error) err(EX_OSERR, "editInsert"); } } @@ -414,7 +423,8 @@ void inputRead(void) { } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { paste ^= true; } else if (paste || literal) { - editInsert(&edit, ch); + int error = editInsert(&edit, ch); + if (error) err(EX_OSERR, "editInsert"); } else if (ret == KEY_CODE_YES) { keyCode(ch); } else if (ch == (L'Z' ^ L'@')) { @@ -428,7 +438,8 @@ void inputRead(void) { } else if (iswcntrl(ch)) { keyCtrl(ch); } else { - editInsert(&edit, ch); + int error = editInsert(&edit, ch); + if (error) err(EX_OSERR, "editInsert"); } style = false; literal = false; -- cgit 1.4.1 From 5c4ecb5a0f8a8a532192d066f792c1f2bfbc414c Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 22:02:49 -0500 Subject: Reimplement tab complete --- input.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index 2d79c7b..ba83762 100644 --- a/input.c +++ b/input.c @@ -31,11 +31,14 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include "chat.h" #include "edit.h" @@ -272,10 +275,105 @@ static int macroExpand(struct Edit *e) { return 0; } +static struct { + char *pre; + size_t pos; + size_t len; + bool suffix; +} tab; + +static int tabComplete(struct Edit *e, uint id) { + if (!tab.len) { + 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 = complete(id, tab.pre); + if (!comp) { + comp = complete(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 tabAccept(void) { + completeAccept(); + tab.len = 0; +} + +static void tabReject(void) { + completeReject(); + tab.len = 0; +} + static void inputEnter(void) { char *cmd = editString(&edit); if (!cmd) err(EX_OSERR, "editString"); + tabAccept(); command(windowID(), cmd); editFn(&edit, EditClear); } @@ -342,6 +440,7 @@ static void keyCtrl(wchar_t ch) { 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); @@ -353,7 +452,7 @@ static void keyCtrl(wchar_t ch) { 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); + break; case L'X': error = macroExpand(&edit); tabAccept(); break; case L'Y': error = editFn(&edit, EditPaste); } if (error) err(EX_OSERR, "editFn"); @@ -415,7 +514,10 @@ void inputRead(void) { wint_t ch; static bool paste, style, literal; for (int ret; ERR != (ret = wget_wch(uiInput, &ch));) { + bool tab = false; + size_t pos = edit.pos; bool spr = uiSpoilerReveal; + if (ret == KEY_CODE_YES && ch == KeyPasteOn) { paste = true; } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) { @@ -436,6 +538,7 @@ void inputRead(void) { } else if (style) { keyStyle(ch); } else if (iswcntrl(ch)) { + tab = (ch == (L'I' ^ L'@')); keyCtrl(ch); } else { int error = editInsert(&edit, ch); @@ -443,6 +546,15 @@ void inputRead(void) { } style = false; literal = false; + + if (!tab) { + if (edit.pos > pos) { + tabAccept(); + } else if (edit.pos < pos) { + tabReject(); + } + } + if (spr) { uiSpoilerReveal = false; windowUpdate(); -- cgit 1.4.1 From 3b4ad30d9789e43d5f639f97fd4613b3edbf83fa Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 19 Feb 2022 23:23:14 -0500 Subject: Remove unneeded includes in ui.c --- ui.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ui.c b/ui.c index d24f7a7..67c6349 100644 --- a/ui.c +++ b/ui.c @@ -28,13 +28,11 @@ #define _XOPEN_SOURCE_EXTENDED #include -#include #include #include #include #include #include -#include #include #include #include @@ -43,11 +41,8 @@ #include #include #include -#include #include #include -#include -#include #ifdef __FreeBSD__ #include @@ -55,10 +50,6 @@ #include "chat.h" -// Annoying stuff from : -#undef lines -#undef tab - #define BOTTOM (LINES - 1) #define RIGHT (COLS - 1) #define MAIN_LINES (LINES - StatusLines - InputLines) -- cgit 1.4.1 From 157be8a8d72f17b4c56fc64c1659ac7b1cac22df Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 11:54:06 -0500 Subject: Remove unused mbs.len field from struct Edit --- edit.c | 15 +++++++-------- edit.h | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/edit.c b/edit.c index 5766b29..b0bdb23 100644 --- a/edit.c +++ b/edit.c @@ -44,7 +44,7 @@ void editFree(struct Edit *e) { free(e->mbs.buf); e->pos = e->len = e->cap = 0; e->cut.len = 0; - e->mbs.pos = e->mbs.len = 0; + e->mbs.pos = 0; } char *editString(struct Edit *e) { @@ -54,19 +54,18 @@ char *editString(struct Edit *e) { e->mbs.buf = buf; const wchar_t *ptr = e->buf; - e->mbs.len = wcsnrtombs(e->mbs.buf, &ptr, e->pos, cap-1, NULL); - if (e->mbs.len == (size_t)-1) return NULL; - e->mbs.pos = e->mbs.len; + size_t len = wcsnrtombs(e->mbs.buf, &ptr, e->pos, cap-1, NULL); + if (len == (size_t)-1) return NULL; + e->mbs.pos = len; ptr = &e->buf[e->pos]; size_t n = wcsnrtombs( - &e->mbs.buf[e->mbs.len], &ptr, e->len - e->pos, - cap-1 - e->mbs.len, NULL + &e->mbs.buf[len], &ptr, e->len - e->pos, cap-1 - len, NULL ); if (n == (size_t)-1) return NULL; - e->mbs.len += n; + len += n; - e->mbs.buf[e->mbs.len] = '\0'; + e->mbs.buf[len] = '\0'; return e->mbs.buf; } diff --git a/edit.h b/edit.h index d15e5fb..49adb7f 100644 --- a/edit.h +++ b/edit.h @@ -45,7 +45,6 @@ struct Edit { struct { char *buf; size_t pos; - size_t len; } mbs; }; -- cgit 1.4.1 From 8065fcabc35006c55b42a8771fa7cdedf4f31a8d Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 12:24:54 -0500 Subject: Make sure new cap is actually larger than new length --- edit.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edit.c b/edit.c index b0bdb23..a8f5d4a 100644 --- a/edit.c +++ b/edit.c @@ -75,7 +75,8 @@ int editReserve(struct Edit *e, size_t index, size_t count) { return -1; } if (e->len + count > e->cap) { - size_t cap = (e->cap ? e->cap * 2 : 256); + 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; -- cgit 1.4.1 From 012fd67539cdf1c13edbb6047253e168d1814cd3 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 14:37:51 -0500 Subject: Use separate edit buffers for each ID --- input.c | 120 +++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/input.c b/input.c index ba83762..b578d17 100644 --- a/input.c +++ b/input.c @@ -89,7 +89,7 @@ enum { #undef X }; -static struct Edit edit; +static struct Edit edits[IDCap]; void inputInit(void) { struct termios term; @@ -160,7 +160,7 @@ static char *inputStop( void inputUpdate(void) { uint id = windowID(); - char *buf = editString(&edit); + char *buf = editString(&edits[id]); if (!buf) err(EX_OSERR, "editString"); const char *prefix = ""; @@ -192,7 +192,7 @@ void inputUpdate(void) { } else { prompt = ""; } - if (skip > &buf[edit.mbs.pos]) { + if (skip > &buf[edits[id].mbs.pos]) { prefix = prompt = suffix = ""; skip = buf; } @@ -211,7 +211,7 @@ void inputUpdate(void) { int pos; struct Style style = styleInput; - inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]); + inputStop(styleInput, &style, skip, &buf[edits[id].mbs.pos]); getyx(uiInput, y, pos); wmove(uiInput, y, x); @@ -276,14 +276,30 @@ static int macroExpand(struct Edit *e) { } static struct { + uint id; char *pre; size_t pos; size_t len; bool suffix; } tab; +static void tabAccept(void) { + completeAccept(); + tab.len = 0; +} + +static void tabReject(void) { + completeReject(); + 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; @@ -359,33 +375,25 @@ fail: return -1; } -static void tabAccept(void) { - completeAccept(); - tab.len = 0; -} - -static void tabReject(void) { - completeReject(); - tab.len = 0; -} - static void inputEnter(void) { - char *cmd = editString(&edit); + uint id = windowID(); + char *cmd = editString(&edits[id]); if (!cmd) err(EX_OSERR, "editString"); tabAccept(); - command(windowID(), cmd); - editFn(&edit, EditClear); + command(id, cmd); + editFn(&edits[id], EditClear); } 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 KeyMetaEnter: error = editInsert(edit, L'\n'); break; case KeyMetaEqual: windowToggleMute(); break; case KeyMetaMinus: windowToggleThresh(-1); break; case KeyMetaPlus: windowToggleThresh(+1); @@ -396,32 +404,32 @@ static void keyCode(int code) { 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 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 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 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_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_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_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_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); @@ -431,29 +439,30 @@ static void keyCode(int code) { 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'?': 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'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'K': error = editFn(edit, EditDeleteTail); break; case L'L': clearok(curscr, true); break; case L'N': windowShow(windowNum() + 1); break; case L'P': windowShow(windowNum() - 1); - break; case L'R': windowSearch(editString(&edit), -1); - break; case L'S': windowSearch(editString(&edit), +1); - break; case L'T': error = editFn(&edit, EditTranspose); - break; case L'U': error = editFn(&edit, EditDeleteHead); + break; case L'R': windowSearch(editString(edit), -1); + break; case L'S': windowSearch(editString(edit), +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); + 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"); } @@ -488,8 +497,9 @@ static void keyStyle(wchar_t ch) { 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); + int error = editInsert(edit, *ch); if (error) err(EX_OSERR, "editInsert"); } } @@ -514,8 +524,8 @@ void inputRead(void) { wint_t ch; static bool paste, style, literal; for (int ret; ERR != (ret = wget_wch(uiInput, &ch));) { - bool tab = false; - size_t pos = edit.pos; + bool tabbing = false; + size_t pos = edits[tab.id].pos; bool spr = uiSpoilerReveal; if (ret == KEY_CODE_YES && ch == KeyPasteOn) { @@ -525,7 +535,7 @@ void inputRead(void) { } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { paste ^= true; } else if (paste || literal) { - int error = editInsert(&edit, ch); + int error = editInsert(&edits[windowID()], ch); if (error) err(EX_OSERR, "editInsert"); } else if (ret == KEY_CODE_YES) { keyCode(ch); @@ -538,19 +548,19 @@ void inputRead(void) { } else if (style) { keyStyle(ch); } else if (iswcntrl(ch)) { - tab = (ch == (L'I' ^ L'@')); + tabbing = (ch == (L'I' ^ L'@')); keyCtrl(ch); } else { - int error = editInsert(&edit, ch); + int error = editInsert(&edits[windowID()], ch); if (error) err(EX_OSERR, "editInsert"); } style = false; literal = false; - if (!tab) { - if (edit.pos > pos) { + if (!tabbing) { + if (edits[tab.id].pos > pos) { tabAccept(); - } else if (edit.pos < pos) { + } else if (edits[tab.id].pos < pos) { tabReject(); } } -- cgit 1.4.1 From a281bdc5e1700e25022536a5482b1fb41ece4219 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 14:49:17 -0500 Subject: Show indicator in status when window has pending input --- chat.h | 1 + input.c | 4 ++++ window.c | 10 ++++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/chat.h b/chat.h index 0de930d..17334fa 100644 --- a/chat.h +++ b/chat.h @@ -331,6 +331,7 @@ int uiSave(void); void inputInit(void); void inputWait(void); void inputUpdate(void); +bool inputPending(uint id); void inputRead(void); void inputCompleteAdd(void); diff --git a/input.c b/input.c index b578d17..ad6d533 100644 --- a/input.c +++ b/input.c @@ -227,6 +227,10 @@ void inputUpdate(void) { wmove(uiInput, y, pos); } +bool inputPending(uint id) { + return edits[id].len; +} + static const struct { const wchar_t *name; const wchar_t *string; diff --git a/window.c b/window.c index 0395542..ee0911f 100644 --- a/window.c +++ b/window.c @@ -164,7 +164,7 @@ static void statusUpdate(void) { wmove(uiStatus, 0, 0); for (uint num = 0; num < count; ++num) { const struct Window *window = windows[num]; - if (num != show && !window->scroll) { + if (num != show && !window->scroll && !inputPending(window->id)) { if (window->heat < Warm) continue; if (window->mute && window->heat < Hot) continue; } @@ -181,15 +181,17 @@ static void statusUpdate(void) { ); if (window->mark && window->unreadWarm) { ptr = seprintf( - ptr, end, "\3%d+%d\3%d%s", + ptr, end, "\3%d+%d\3%d ", (window->heat > Warm ? White : idColors[window->id]), - window->unreadWarm, idColors[window->id], - (window->scroll ? "" : " ") + 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); -- cgit 1.4.1 From cca9688cca171dc3ee6ac24822bdd8c315997759 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 14:58:33 -0500 Subject: Clear edit buffer before running command Otherwise a command that switches windows will update the status line while the edit buffer still has input "pending", showing an indicator. --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index ad6d533..28349f4 100644 --- a/input.c +++ b/input.c @@ -385,8 +385,8 @@ static void inputEnter(void) { if (!cmd) err(EX_OSERR, "editString"); tabAccept(); - command(id, cmd); editFn(&edits[id], EditClear); + command(id, cmd); } static void keyCode(int code) { -- cgit 1.4.1 From e39bba1a8a2fda74bcfd06f728b7e1fddadef161 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 15:26:23 -0500 Subject: Move mbs out of struct Edit, use a global buffer This saves 4K in the edit buffers, not to mention all the heap allocations for the separate mbs buffers! There might be a way to be more clever about capacities, but I don't think it's worth it. --- edit.c | 36 ++++++++++++++++++++---------------- edit.h | 8 ++------ input.c | 27 ++++++++++++++++----------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/edit.c b/edit.c index a8f5d4a..7c24865 100644 --- a/edit.c +++ b/edit.c @@ -41,32 +41,33 @@ static bool isword(wchar_t ch) { void editFree(struct Edit *e) { free(e->buf); free(e->cut.buf); - free(e->mbs.buf); e->pos = e->len = e->cap = 0; e->cut.len = 0; - e->mbs.pos = 0; } -char *editString(struct Edit *e) { - size_t cap = e->len * MB_CUR_MAX + 1; - char *buf = realloc(e->mbs.buf, cap); - if (!buf) return NULL; - e->mbs.buf = buf; +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 = e->buf; - size_t len = wcsnrtombs(e->mbs.buf, &ptr, e->pos, cap-1, NULL); + size_t len = wcsnrtombs(*buf, &ptr, e->pos, *cap-1, NULL); if (len == (size_t)-1) return NULL; - e->mbs.pos = len; + if (pos) *pos = len; ptr = &e->buf[e->pos]; size_t n = wcsnrtombs( - &e->mbs.buf[len], &ptr, e->len - e->pos, cap-1 - len, NULL + *buf + len, &ptr, e->len - e->pos, *cap-1 - len, NULL ); if (n == (size_t)-1) return NULL; len += n; - e->mbs.buf[len] = '\0'; - return e->mbs.buf; + (*buf)[len] = '\0'; + return *buf; } int editReserve(struct Edit *e, size_t index, size_t count) { @@ -212,11 +213,14 @@ static void fix(struct Edit *e, const char *str) { } static bool eq(struct Edit *e, const char *str1) { + size_t pos; + static size_t cap; + static char *buf; + editString(e, &buf, &cap, &pos); const char *str2 = &str1[strlen(str1) + 1]; - const char *buf = editString(e); - return e->mbs.pos == strlen(str1) - && !strncmp(buf, str1, e->mbs.pos) - && !strcmp(&buf[e->mbs.pos], str2); + return pos == strlen(str1) + && !strncmp(buf, str1, pos) + && !strcmp(&buf[pos], str2); } int main(void) { diff --git a/edit.h b/edit.h index 49adb7f..9cf814b 100644 --- a/edit.h +++ b/edit.h @@ -42,10 +42,6 @@ struct Edit { wchar_t *buf; size_t len; } cut; - struct { - char *buf; - size_t pos; - } mbs; }; enum EditFn { @@ -76,8 +72,8 @@ int editVi(struct Edit *e, wchar_t ch); // Insert a character at the cursor. int editInsert(struct Edit *e, wchar_t ch); -// Convert the buffer to a multi-byte string stored in e->mbs. -char *editString(struct Edit *e); +// Convert the buffer to a multi-byte string. +char *editString(const struct Edit *e, char **buf, size_t *cap, size_t *pos); // Free all buffers. void editFree(struct Edit *e); diff --git a/input.c b/input.c index 28349f4..a74335e 100644 --- a/input.c +++ b/input.c @@ -158,10 +158,15 @@ static char *inputStop( return stop; } +static size_t cap; +static char *buf; + void inputUpdate(void) { uint id = windowID(); - char *buf = editString(&edits[id]); - if (!buf) err(EX_OSERR, "editString"); + + 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; @@ -192,7 +197,7 @@ void inputUpdate(void) { } else { prompt = ""; } - if (skip > &buf[edits[id].mbs.pos]) { + if (skip > &buf[pos]) { prefix = prompt = suffix = ""; skip = buf; } @@ -209,14 +214,14 @@ void inputUpdate(void) { waddstr(uiInput, suffix); getyx(uiInput, y, x); - int pos; + int posx; struct Style style = styleInput; - inputStop(styleInput, &style, skip, &buf[edits[id].mbs.pos]); - getyx(uiInput, y, pos); + inputStop(styleInput, &style, skip, &buf[pos]); + getyx(uiInput, y, posx); wmove(uiInput, y, x); + ptr = skip; style = styleInput; - const char *ptr = skip; if (split) { ptr = inputStop(styleInput, &style, ptr, &buf[split]); style = styleInput; @@ -224,7 +229,7 @@ void inputUpdate(void) { } inputAdd(styleInput, &style, ptr); wclrtoeol(uiInput); - wmove(uiInput, y, pos); + wmove(uiInput, y, posx); } bool inputPending(uint id) { @@ -381,7 +386,7 @@ fail: static void inputEnter(void) { uint id = windowID(); - char *cmd = editString(&edits[id]); + char *cmd = editString(&edits[id], &buf, &cap, NULL); if (!cmd) err(EX_OSERR, "editString"); tabAccept(); @@ -459,8 +464,8 @@ static void keyCtrl(wchar_t ch) { break; case L'L': clearok(curscr, true); break; case L'N': windowShow(windowNum() + 1); break; case L'P': windowShow(windowNum() - 1); - break; case L'R': windowSearch(editString(edit), -1); - break; case L'S': windowSearch(editString(edit), +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); -- cgit 1.4.1 From c8b6e331de95419de72546964f6b255dccddcd93 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 16:05:24 -0500 Subject: Assert return values in edit tests --- edit.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/edit.c b/edit.c index 7c24865..eb3d28d 100644 --- a/edit.c +++ b/edit.c @@ -206,9 +206,9 @@ int editInsert(struct Edit *e, wchar_t ch) { #include static void fix(struct Edit *e, const char *str) { - editFn(e, EditClear); + assert(0 == editFn(e, EditClear)); for (const char *ch = str; *ch; ++ch) { - editInsert(e, (wchar_t)*ch); + assert(0 == editInsert(e, (wchar_t)*ch)); } } @@ -216,13 +216,15 @@ static bool eq(struct Edit *e, const char *str1) { size_t pos; static size_t cap; static char *buf; - editString(e, &buf, &cap, &pos); + 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 e = { .mode = EditEmacs }; -- cgit 1.4.1 From da1b943fccc426bc43caa7e7e51208833b9fce66 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 16:20:33 -0500 Subject: Share a cut buffer between all edit buffers --- edit.c | 26 ++++++++++---------------- edit.h | 8 +------- input.c | 5 +++++ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/edit.c b/edit.c index eb3d28d..bb92edf 100644 --- a/edit.c +++ b/edit.c @@ -38,13 +38,6 @@ static bool isword(wchar_t ch) { return !iswspace(ch) && !iswpunct(ch); } -void editFree(struct Edit *e) { - free(e->buf); - free(e->cut.buf); - e->pos = e->len = e->cap = 0; - e->cut.len = 0; -} - 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) { @@ -93,11 +86,10 @@ int editCopy(struct Edit *e, size_t index, size_t count) { errno = EINVAL; return -1; } - wchar_t *buf = realloc(e->cut.buf, sizeof(*buf) * count); - if (!buf) return -1; - e->cut.buf = buf; - wmemcpy(e->cut.buf, &e->buf[index], count); - e->cut.len = count; + 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; } @@ -159,10 +151,11 @@ int editFn(struct Edit *e, enum EditFn fn) { } break; case EditPaste: { - ret = editReserve(e, e->pos, e->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; + wmemcpy(&e->buf[e->pos], e->cut->buf, e->cut->len); + e->pos += e->cut->len; } } break; case EditTranspose: { @@ -226,7 +219,8 @@ static bool eq(struct Edit *e, const char *str1) { #define editFn(...) assert(0 == editFn(__VA_ARGS__)) int main(void) { - struct Edit e = { .mode = EditEmacs }; + struct Edit cut = {0}; + struct Edit e = { .cut = &cut }; fix(&e, "foo bar"); editFn(&e, EditHead); diff --git a/edit.h b/edit.h index 9cf814b..957d3e3 100644 --- a/edit.h +++ b/edit.h @@ -38,10 +38,7 @@ struct Edit { size_t pos; size_t len; size_t cap; - struct { - wchar_t *buf; - size_t len; - } cut; + struct Edit *cut; }; enum EditFn { @@ -75,9 +72,6 @@ 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); -// Free all buffers. -void editFree(struct Edit *e); - // Reserve a range in the buffer. int editReserve(struct Edit *e, size_t index, size_t count); diff --git a/input.c b/input.c index a74335e..b6c51a2 100644 --- a/input.c +++ b/input.c @@ -89,9 +89,14 @@ enum { #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"); -- cgit 1.4.1 From 78ff548b934bb265437e3afeea78942c601691e6 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 16:57:11 -0500 Subject: Save input buffer contents --- chat.h | 2 ++ input.c | 39 +++++++++++++++++++++++++++++++++++++++ ui.c | 7 +++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/chat.h b/chat.h index 17334fa..1c46f00 100644 --- a/chat.h +++ b/chat.h @@ -334,6 +334,8 @@ void inputUpdate(void); bool inputPending(uint id); void inputRead(void); void inputCompleteAdd(void); +int inputSave(FILE *file); +void inputLoad(FILE *file, size_t version); enum Scroll { ScrollOne, diff --git a/input.c b/input.c index b6c51a2..de8cecf 100644 --- a/input.c +++ b/input.c @@ -586,3 +586,42 @@ void inputRead(void) { } 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/ui.c b/ui.c index 67c6349..64bd6ce 100644 --- a/ui.c +++ b/ui.c @@ -288,7 +288,8 @@ static const uint64_t Signatures[] = { 0x6C72696774616305, // no URLs 0x6C72696774616306, // no thresh 0x6C72696774616307, // no window time - 0x6C72696774616308, + 0x6C72696774616308, // no input + 0x6C72696774616309, }; static size_t signatureVersion(uint64_t signature) { @@ -305,9 +306,10 @@ static int writeUint64(FILE *file, uint64_t u) { int uiSave(void) { return 0 || ftruncate(fileno(saveFile), 0) - || writeUint64(saveFile, Signatures[7]) + || writeUint64(saveFile, Signatures[8]) || writeUint64(saveFile, self.pos) || windowSave(saveFile) + || inputSave(saveFile) || urlSave(saveFile) || fclose(saveFile); } @@ -350,5 +352,6 @@ void uiLoad(const char *name) { self.pos = readUint64(saveFile); } windowLoad(saveFile, version); + inputLoad(saveFile, version); urlLoad(saveFile, version); } -- cgit 1.4.1 From c929a696eb8a1902a740a19f8f22c745df0cf953 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sun, 20 Feb 2022 17:16:21 -0500 Subject: Clean up unimplemented editing mode stuff --- edit.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/edit.h b/edit.h index 957d3e3..db0d416 100644 --- a/edit.h +++ b/edit.h @@ -29,7 +29,7 @@ #include enum EditMode { - EditEmacs, + EditInsert, }; struct Edit { @@ -63,9 +63,6 @@ enum EditFn { // Perform an editing function. int editFn(struct Edit *e, enum EditFn fn); -// Perform a vi-mode editing function. -int editVi(struct Edit *e, wchar_t ch); - // Insert a character at the cursor. int editInsert(struct Edit *e, wchar_t ch); -- cgit 1.4.1 From 5b3b798c499971abbd243e47ad0df908679c8cc5 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Tue, 22 Feb 2022 20:04:42 -0500 Subject: Document the interface --- catgirl.1 | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/catgirl.1 b/catgirl.1 index 065002b..21eb82e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 12, 2022 +.Dd February 22, 2022 .Dt CATGIRL 1 .Os . @@ -429,6 +429,104 @@ trust = 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, -- cgit 1.4.1 From 4e8ba236b606d53f5cf4688d977ac3c4196c690b Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Wed, 23 Feb 2022 12:50:31 -0500 Subject: Add missing unistd.h include in input.c --- input.c | 1 + 1 file changed, 1 insertion(+) diff --git a/input.c b/input.c index de8cecf..f3813c4 100644 --- a/input.c +++ b/input.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include -- cgit 1.4.1 From f51e7b4d2cc601bef8f22d5061ff8fafb012d671 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Wed, 23 Feb 2022 18:05:10 -0500 Subject: Give examples of "general events" --- catgirl.1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/catgirl.1 b/catgirl.1 index 21eb82e..e9b124b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -326,7 +326,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 @@ -765,7 +766,8 @@ 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, -- cgit 1.4.1 From 628e064056a02cbc3fb3b758df51559d62c37a10 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 26 Feb 2022 15:40:11 -0500 Subject: Factor out commandAvailable --- command.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/command.c b/command.c index a127af3..511bc62 100644 --- a/command.c +++ b/command.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe +/* Copyright (C) 2020 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -639,6 +639,12 @@ 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->flags & Kiosk && self.kiosk) return false; + return true; +} + void command(uint id, char *input) { if (id == Debug && input[0] != '/' && !self.restricted) { commandQuote(id, input); @@ -667,10 +673,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; } -- cgit 1.4.1 From b7fe705c9143a8bae6b0e87e62c9568265291484 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 26 Feb 2022 15:41:50 -0500 Subject: Only add available commands to complete --- command.c | 1 + 1 file changed, 1 insertion(+) diff --git a/command.c b/command.c index 511bc62..60281d9 100644 --- a/command.c +++ b/command.c @@ -692,6 +692,7 @@ void command(uint id, char *input) { void commandCompleteAdd(void) { for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { + if (!commandAvailable(&Commands[i])) continue; completeAdd(None, Commands[i].cmd, Default); } } -- cgit 1.4.1 From b6c72806498e95cb08606a7ec46742e5439d3348 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Sat, 26 Feb 2022 15:51:42 -0500 Subject: Specify commands which depend on caps Currently only /setname. --- chat.c | 3 +- command.c | 96 +++++++++++++++++++++++++++++++++------------------------------ handle.c | 3 +- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/chat.c b/chat.c index b085165..b74fdc0 100644 --- a/chat.c +++ b/chat.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe +/* Copyright (C) 2020 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -371,7 +371,6 @@ int main(int argc, char *argv[]) { set(&network.name, host); set(&self.nick, "*"); - commandCompleteAdd(); inputCompleteAdd(); ircConfig(insecure, trust, cert, priv); diff --git a/command.c b/command.c index 60281d9..f404498 100644 --- a/command.c +++ b/command.c @@ -537,53 +537,54 @@ 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 | Kiosk, 0 }, + { "/cs", commandCS, 0, 0 }, + { "/debug", commandDebug, Kiosk, 0 }, + { "/deop", commandDeop, 0, 0 }, + { "/devoice", commandDevoice, 0, 0 }, + { "/except", commandExcept, 0, 0 }, + { "/exec", commandExec, Multiline | Restrict | Kiosk, 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, Kiosk, 0 }, + { "/kick", commandKick, 0, 0 }, + { "/list", commandList, Kiosk, 0 }, + { "/me", commandMe, Multiline, 0 }, + { "/mode", commandMode, 0, 0 }, + { "/move", commandMove, 0, 0 }, + { "/msg", commandMsg, Multiline | Kiosk, 0 }, + { "/names", commandNames, 0, 0 }, + { "/nick", commandNick, 0, 0 }, + { "/notice", commandNotice, Multiline, 0 }, + { "/ns", commandNS, 0, 0 }, + { "/o", commandOpen, Restrict | Kiosk, 0 }, + { "/op", commandOp, 0, 0 }, + { "/open", commandOpen, Restrict | Kiosk, 0 }, + { "/ops", commandOps, 0, 0 }, + { "/part", commandPart, Kiosk, 0 }, + { "/query", commandQuery, Kiosk, 0 }, + { "/quit", commandQuit, 0, 0 }, + { "/quote", commandQuote, Multiline | Kiosk, 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) { @@ -642,6 +643,9 @@ size_t commandWillSplit(uint id, const char *input) { static bool commandAvailable(const struct Handler *handler) { if (handler->flags & Restrict && self.restricted) return false; if (handler->flags & Kiosk && self.kiosk) return false; + if (handler->caps && (handler->caps & self.caps) != handler->caps) { + return false; + } return true; } diff --git a/handle.c b/handle.c index 9f051c7..388a122 100644 --- a/handle.c +++ b/handle.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 C. McEnroe +/* Copyright (C) 2020 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -254,6 +254,7 @@ static void handleReplyWelcome(struct Message *msg) { replies[ReplyTopicAuto] += count; replies[ReplyNamesAuto] += count; } + commandCompleteAdd(); } static void handleReplyISupport(struct Message *msg) { -- cgit 1.4.1