From 4cce893eab7403821ff211f64a7df05051fd6f52 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 00:20:39 -0500 Subject: Add extremely basic editing and message sending --- command.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 command.c (limited to 'command.c') diff --git a/command.c b/command.c new file mode 100644 index 0000000..ab05587 --- /dev/null +++ b/command.c @@ -0,0 +1,32 @@ +/* Copyright (C) 2020 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "chat.h" + +void command(size_t id, char *input) { + ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); + struct Message msg = { + .nick = self.nick, + // TODO: .user, + .cmd = "PRIVMSG", + .params[0] = idNames[id], + .params[1] = input, + }; + handle(msg); +} -- cgit 1.4.0 From 7414a8a11cd8d16fea47e30513b3a5eaeb232ba1 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 00:40:24 -0500 Subject: Save own username for message echoing --- chat.h | 1 + command.c | 2 +- handle.c | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'command.c') diff --git a/chat.h b/chat.h index c8b31c2..90c7da8 100644 --- a/chat.h +++ b/chat.h @@ -73,6 +73,7 @@ extern struct Self { char *chanTypes; char *prefixes; char *nick; + char *user; enum Color color; } self; diff --git a/command.c b/command.c index ab05587..76d7d7b 100644 --- a/command.c +++ b/command.c @@ -23,7 +23,7 @@ void command(size_t id, char *input) { ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); struct Message msg = { .nick = self.nick, - // TODO: .user, + .user = self.user, .cmd = "PRIVMSG", .params[0] = idNames[id], .params[1] = input, diff --git a/handle.c b/handle.c index b5585ba..85783d7 100644 --- a/handle.c +++ b/handle.c @@ -187,7 +187,10 @@ static void handleJoin(struct Message *msg) { require(msg, true, 1); size_t id = idFor(msg->params[0]); if (self.nick && !strcmp(msg->nick, self.nick)) { - self.color = hash(msg->user); + if (!self.user) { + set(&self.user, msg->user); + self.color = hash(msg->user); + } idColors[id] = hash(msg->params[0]); uiShowID(id); } -- cgit 1.4.0 From 1cf6e29fc465dc9e3633950ede758d4fd1eb644d Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 03:42:04 -0500 Subject: Send input as raw IRC in --- command.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'command.c') diff --git a/command.c b/command.c index 76d7d7b..ef27888 100644 --- a/command.c +++ b/command.c @@ -20,6 +20,10 @@ #include "chat.h" void command(size_t id, char *input) { + if (id == Debug) { + ircFormat("%s\r\n", input); + return; + } ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); struct Message msg = { .nick = self.nick, -- cgit 1.4.0 From b2d35edcb22a9a41235229b41b180a50b51b5908 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 19:00:54 -0500 Subject: Change prompt depending on command --- chat.h | 3 +++ command.c | 21 +++++++++++++++++++++ ui.c | 23 ++++++++++++++++++----- 3 files changed, 42 insertions(+), 5 deletions(-) (limited to 'command.c') diff --git a/chat.h b/chat.h index 90c7da8..b73cf40 100644 --- a/chat.h +++ b/chat.h @@ -112,6 +112,9 @@ void ircFormat(const char *format, ...) void handle(struct Message msg); void command(size_t id, char *input); +const char *commandIsPrivmsg(size_t id, const char *input); +const char *commandIsNotice(size_t id, const char *input); +const char *commandIsAction(size_t id, const char *input); enum Heat { Cold, Warm, Hot }; void uiInit(void); diff --git a/command.c b/command.c index ef27888..928f470 100644 --- a/command.c +++ b/command.c @@ -19,6 +19,27 @@ #include "chat.h" +const char *commandIsPrivmsg(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (input[0] != '/') return input; + const char *space = strchr(&input[1], ' '); + const char *slash = strchr(&input[1], '/'); + if (slash && (!space || slash < space)) return input; + return NULL; +} + +const char *commandIsNotice(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/notice ", 8)) return NULL; + return &input[8]; +} + +const char *commandIsAction(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/me ", 4)) return NULL; + return &input[4]; +} + void command(size_t id, char *input) { if (id == Debug) { ircFormat("%s\r\n", input); diff --git a/ui.c b/ui.c index 12c8541..daa6dec 100644 --- a/ui.c +++ b/ui.c @@ -479,16 +479,29 @@ static void inputUpdate(void) { colorPair(mapColor(self.color), -1), NULL ); + const char *head = editHead(); + const char *skip = NULL; if (self.nick) { - // TODO: Check if input is command or action. - waddch(input, '<'); - waddstr(input, self.nick); - waddstr(input, "> "); + size_t id = windows.active->id; + if (NULL != (skip = commandIsPrivmsg(id, head))) { + waddch(input, '<'); + waddstr(input, self.nick); + waddstr(input, "> "); + } else if (NULL != (skip = commandIsNotice(id, head))) { + waddch(input, '-'); + waddstr(input, self.nick); + waddstr(input, "- "); + } else if (NULL != (skip = commandIsAction(id, head))) { + waddstr(input, "* "); + waddstr(input, self.nick); + waddch(input, ' '); + } } + if (skip) head = skip; int y, x; struct Style style = Reset; - inputAdd(&style, editHead()); + inputAdd(&style, head); getyx(input, y, x); inputAdd(&style, editTail()); wclrtoeol(input); -- cgit 1.4.0 From 7c0b60221bf22a8042b584c453bda0e3e87cd0ea Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 19:19:01 -0500 Subject: Add /me, /notice, /quote commands --- command.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 10 deletions(-) (limited to 'command.c') diff --git a/command.c b/command.c index 928f470..0843bd3 100644 --- a/command.c +++ b/command.c @@ -19,6 +19,57 @@ #include "chat.h" +typedef void Command(size_t id, char *params); + +static void commandQuote(size_t id, char *params) { + (void)id; + ircFormat("%s\r\n", params); +} + +static void commandPrivmsg(size_t id, char *params) { + ircFormat("PRIVMSG %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "PRIVMSG", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandNotice(size_t id, char *params) { + ircFormat("NOTICE %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "NOTICE", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandMe(size_t id, char *params) { + char buf[512]; + snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + commandPrivmsg(id, buf); +} + +static const struct Handler { + const char *cmd; + Command *fn; +} Commands[] = { + { "/me", commandMe }, + { "/notice", commandNotice }, + { "/quote", commandQuote }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + const char *commandIsPrivmsg(size_t id, const char *input) { if (id == Network || id == Debug) return NULL; if (input[0] != '/') return input; @@ -42,16 +93,20 @@ const char *commandIsAction(size_t id, const char *input) { void command(size_t id, char *input) { if (id == Debug) { - ircFormat("%s\r\n", input); + commandQuote(id, input); return; } - ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input); - struct Message msg = { - .nick = self.nick, - .user = self.user, - .cmd = "PRIVMSG", - .params[0] = idNames[id], - .params[1] = input, - }; - handle(msg); + if (commandIsPrivmsg(id, input)) { + commandPrivmsg(id, input); + return; + } + char *cmd = strsep(&input, " "); + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (handler) { + handler->fn(id, input); + } else { + uiFormat(id, Hot, NULL, "No such command %s", cmd); + } } -- cgit 1.4.0 From 7c0e9cf3d2e83814fab3bb4bb09f7b955c2afaca Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 21:57:23 -0500 Subject: Add /quit --- chat.c | 13 ++++++++----- chat.h | 1 + command.c | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) (limited to 'command.c') diff --git a/chat.c b/chat.c index 2d58b1e..1ad2833 100644 --- a/chat.c +++ b/chat.c @@ -115,13 +115,12 @@ int main(int argc, char *argv[]) { { .events = POLLIN, .fd = STDIN_FILENO }, { .events = POLLIN, .fd = irc }, }; - for (;;) { + while (!self.quit) { int nfds = poll(fds, 2, -1); if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); - if (signals[SIGHUP] || signals[SIGINT] || signals[SIGTERM]) { - break; - } + if (signals[SIGHUP]) self.quit = "zzz"; + if (signals[SIGINT] || signals[SIGTERM]) break; if (signals[SIGWINCH]) { signals[SIGWINCH] = 0; cursesWinch(SIGWINCH); @@ -136,6 +135,10 @@ int main(int argc, char *argv[]) { uiDraw(); } - ircFormat("QUIT\r\n"); + if (self.quit) { + ircFormat("QUIT :%s\r\n", self.quit); + } else { + ircFormat("QUIT\r\n"); + } uiHide(); } diff --git a/chat.h b/chat.h index b73cf40..5b3c01c 100644 --- a/chat.h +++ b/chat.h @@ -75,6 +75,7 @@ extern struct Self { char *nick; char *user; enum Color color; + char *quit; } self; static inline void set(char **field, const char *value) { diff --git a/command.c b/command.c index 0843bd3..7fb98af 100644 --- a/command.c +++ b/command.c @@ -56,12 +56,17 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandQuit(size_t id, char *params) { + set(&self.quit, (params ? params : "Goodbye")); +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { { "/me", commandMe }, { "/notice", commandNotice }, + { "/quit", commandQuit }, { "/quote", commandQuote }, }; -- cgit 1.4.0 From b2cf8733048029354aeb794b14dd71d1bbb0b72d Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 22:09:29 -0500 Subject: Add /window --- chat.h | 1 + command.c | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) (limited to 'command.c') diff --git a/chat.h b/chat.h index 5b3c01c..9317843 100644 --- a/chat.h +++ b/chat.h @@ -123,6 +123,7 @@ void uiShow(void); void uiHide(void); void uiDraw(void); void uiShowID(size_t id); +void uiShowNum(size_t num); void uiRead(void); void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); void uiFormat( diff --git a/command.c b/command.c index 7fb98af..8471214 100644 --- a/command.c +++ b/command.c @@ -57,9 +57,16 @@ static void commandMe(size_t id, char *params) { } static void commandQuit(size_t id, char *params) { + (void)id; set(&self.quit, (params ? params : "Goodbye")); } +static void commandWindow(size_t id, char *params) { + (void)id; + if (!params) return; + uiShowNum(strtoul(params, NULL, 10)); +} + static const struct Handler { const char *cmd; Command *fn; @@ -68,6 +75,7 @@ static const struct Handler { { "/notice", commandNotice }, { "/quit", commandQuit }, { "/quote", commandQuote }, + { "/window", commandWindow }, }; static int compar(const void *cmd, const void *_handler) { @@ -97,21 +105,21 @@ const char *commandIsAction(size_t id, const char *input) { } void command(size_t id, char *input) { - if (id == Debug) { + if (id == Debug && input[0] != '/') { commandQuote(id, input); - return; - } - if (commandIsPrivmsg(id, input)) { + } else if (commandIsPrivmsg(id, input)) { commandPrivmsg(id, input); - return; - } - char *cmd = strsep(&input, " "); - const struct Handler *handler = bsearch( - cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar - ); - if (handler) { - handler->fn(id, input); + } else if (input[0] == '/' && isdigit(input[1])) { + commandWindow(id, &input[1]); } else { - uiFormat(id, Hot, NULL, "No such command %s", cmd); + char *cmd = strsep(&input, " "); + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (handler) { + handler->fn(id, input); + } else { + uiFormat(id, Hot, NULL, "No such command %s", cmd); + } } } -- cgit 1.4.0 From 7cc64927bd223ed0a0197cf3285dbc85691fd32b Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 22:15:08 -0500 Subject: Handle empty messages on privmsg, notice, action --- command.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/command.c b/command.c index 8471214..2a3df9b 100644 --- a/command.c +++ b/command.c @@ -27,6 +27,7 @@ static void commandQuote(size_t id, char *params) { } static void commandPrivmsg(size_t id, char *params) { + if (!params || !params[0]) return; ircFormat("PRIVMSG %s :%s\r\n", idNames[id], params); struct Message msg = { .nick = self.nick, @@ -39,6 +40,7 @@ static void commandPrivmsg(size_t id, char *params) { } static void commandNotice(size_t id, char *params) { + if (!params || !params[0]) return; ircFormat("NOTICE %s :%s\r\n", idNames[id], params); struct Message msg = { .nick = self.nick, @@ -52,7 +54,7 @@ static void commandNotice(size_t id, char *params) { static void commandMe(size_t id, char *params) { char buf[512]; - snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + snprintf(buf, sizeof(buf), "\1ACTION %s\1", (params ? params : "")); commandPrivmsg(id, buf); } -- cgit 1.4.0 From 63b92672fe9ecfd400d9439343a068a3ae5224df Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 22:18:11 -0500 Subject: Handle empty params in /quote --- command.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'command.c') diff --git a/command.c b/command.c index 2a3df9b..f9362dc 100644 --- a/command.c +++ b/command.c @@ -23,7 +23,7 @@ typedef void Command(size_t id, char *params); static void commandQuote(size_t id, char *params) { (void)id; - ircFormat("%s\r\n", params); + if (params) ircFormat("%s\r\n", params); } static void commandPrivmsg(size_t id, char *params) { @@ -65,8 +65,7 @@ static void commandQuit(size_t id, char *params) { static void commandWindow(size_t id, char *params) { (void)id; - if (!params) return; - uiShowNum(strtoul(params, NULL, 10)); + if (params) uiShowNum(strtoul(params, NULL, 10)); } static const struct Handler { -- cgit 1.4.0 From 6ca54617ce1fe0ac4dbd8094e13b38a0aa375200 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 5 Feb 2020 22:25:34 -0500 Subject: Add /window name variant --- catgirl.1 | 2 ++ command.c | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index e6d8efa..fb031c2 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -115,6 +115,8 @@ Send a raw IRC command. . .Ss UI Commands .Bl -tag -width Ds +.It Ic /window Ar name +Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num Switch to window by number. .El diff --git a/command.c b/command.c index f9362dc..e4f035f 100644 --- a/command.c +++ b/command.c @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +#include #include #include @@ -64,8 +65,13 @@ static void commandQuit(size_t id, char *params) { } static void commandWindow(size_t id, char *params) { - (void)id; - if (params) uiShowNum(strtoul(params, NULL, 10)); + if (!params) return; + if (isdigit(params[0])) { + uiShowNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiShowID(id); + } } static const struct Handler { -- cgit 1.4.0 From 9a585188c546ab65633707c3a3e17dbef1d8e3dc Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Thu, 6 Feb 2020 01:05:09 -0500 Subject: Add /join command --- catgirl.1 | 4 +++- command.c | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 91c4ff4..bf6ccc7 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 5, 2020 +.Dd February 6, 2020 .Dt CATGIRL 1 .Os . @@ -120,6 +120,8 @@ Log in with the server password .Sh COMMANDS .Ss Chat Commands .Bl -tag -width Ds +.It Ic /join Ar channel +Join a channel. .It Ic /me Op Ar action Send an action message. .It Ic /notice Ar message diff --git a/command.c b/command.c index e4f035f..3215322 100644 --- a/command.c +++ b/command.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "chat.h" @@ -59,6 +60,10 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandJoin(size_t id, char *params) { + ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); +} + static void commandQuit(size_t id, char *params) { (void)id; set(&self.quit, (params ? params : "Goodbye")); @@ -78,6 +83,7 @@ static const struct Handler { const char *cmd; Command *fn; } Commands[] = { + { "/join", commandJoin }, { "/me", commandMe }, { "/notice", commandNotice }, { "/quit", commandQuit }, -- cgit 1.4.0 From fe5fd897052abd1909d1536056936a0417666459 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Fri, 7 Feb 2020 21:30:25 -0500 Subject: Populate completion with commands --- Makefile | 1 + chat.c | 1 + chat.h | 8 +++++ command.c | 6 ++++ complete.c | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ edit.c | 3 ++ ui.c | 1 + 7 files changed, 130 insertions(+) create mode 100644 complete.c (limited to 'command.c') diff --git a/Makefile b/Makefile index 5380d20..48aba7b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ LDLIBS = -lcrypto -ltls -lncursesw OBJS += chat.o OBJS += command.o +OBJS += complete.o OBJS += config.o OBJS += edit.o OBJS += handle.o diff --git a/chat.c b/chat.c index c487722..91da6e3 100644 --- a/chat.c +++ b/chat.c @@ -110,6 +110,7 @@ int main(int argc, char *argv[]) { set(&self.network, host); set(&self.chanTypes, "#&"); set(&self.prefixes, "@+"); + commandComplete(); FILE *certFile = NULL; FILE *privFile = NULL; diff --git a/chat.h b/chat.h index a327620..f164e7a 100644 --- a/chat.h +++ b/chat.h @@ -118,6 +118,7 @@ void command(size_t id, char *input); const char *commandIsPrivmsg(size_t id, const char *input); const char *commandIsNotice(size_t id, const char *input); const char *commandIsAction(size_t id, const char *input); +void commandComplete(void); enum Heat { Cold, Warm, Hot }; void uiInit(void); @@ -140,12 +141,19 @@ enum Edit { EditKill, EditErase, EditInsert, + EditComplete, EditEnter, }; void edit(size_t id, enum Edit op, wchar_t ch); char *editHead(void); char *editTail(void); +const char *complete(size_t id, const char *prefix); +void completeAccept(void); +void completeReject(void); +void completeAdd(size_t id, const char *str, enum Color color); +void completeTouch(size_t id, const char *str, enum Color color); + FILE *configOpen(const char *path, const char *mode); int getopt_config( int argc, char *const *argv, diff --git a/command.c b/command.c index 3215322..41aacc9 100644 --- a/command.c +++ b/command.c @@ -136,3 +136,9 @@ void command(size_t id, char *input) { } } } + +void commandComplete(void) { + for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { + completeAdd(None, Commands[i].cmd, Default); + } +} diff --git a/complete.c b/complete.c new file mode 100644 index 0000000..b8f2dfc --- /dev/null +++ b/complete.c @@ -0,0 +1,110 @@ +/* Copyright (C) 2020 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "chat.h" + +struct Node { + size_t id; + char *str; + enum Color color; + struct Node *prev; + struct Node *next; +}; + +static struct Node *alloc(size_t id, const char *str, enum Color color) { + struct Node *node = malloc(sizeof(*node)); + if (!node) err(EX_OSERR, "malloc"); + node->id = id; + node->str = strdup(str); + if (!node->str) err(EX_OSERR, "strdup"); + node->color = color; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *head; +static struct Node *tail; + +static struct Node *detach(struct Node *node) { + if (node->prev) node->prev->next = node->next; + if (node->next) node->next->prev = node->prev; + if (head == node) head = node->next; + if (tail == node) tail = node->prev; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *prepend(struct Node *node) { + node->prev = NULL; + node->next = head; + if (head) head->prev = node; + head = node; + if (!tail) tail = node; + return node; +} + +static struct Node *append(struct Node *node) { + node->next = NULL; + node->prev = tail; + if (tail) tail->next = node; + tail = node; + if (!head) head = node; + return node; +} + +static struct Node *find(size_t id, const char *str) { + for (struct Node *node = head; node; node = node->next) { + if (node->id == id && !strcmp(node->str, str)) return node; + } + return NULL; +} + +void completeAdd(size_t id, const char *str, enum Color color) { + if (!find(id, str)) append(alloc(id, str, color)); +} + +void completeTouch(size_t id, const char *str, enum Color color) { + struct Node *node = find(id, str); + prepend(node ? detach(node) : alloc(id, str, color)); +} + +static struct Node *match; + +const char *complete(size_t id, const char *prefix) { + for (match = (match ? match->next : head); match; match = match->next) { + if (match->id && match->id != id) continue; + if (strncasecmp(match->str, prefix, strlen(prefix))) continue; + return match->str; + } + return NULL; +} + +void completeAccept(void) { + if (match) prepend(detach(match)); + match = NULL; +} + +void completeReject(void) { + match = NULL; +} diff --git a/edit.c b/edit.c index b6edb98..0c50f33 100644 --- a/edit.c +++ b/edit.c @@ -73,6 +73,9 @@ void edit(size_t id, enum Edit op, wchar_t ch) { reserve(pos, 1); if (pos < Cap) buf[pos++] = ch; } + break; case EditComplete: { + // TODO + } break; case EditEnter: { pos = 0; command(id, editTail()); diff --git a/ui.c b/ui.c index 147381e..5a8f155 100644 --- a/ui.c +++ b/ui.c @@ -596,6 +596,7 @@ static void keyCtrl(wchar_t ch) { break; case L'A': edit(id, EditHome, 0); break; case L'E': edit(id, EditEnd, 0); break; case L'H': edit(id, EditErase, 0); + break; case L'I': edit(id, EditComplete, 0); break; case L'J': edit(id, EditEnter, 0); break; case L'L': clearok(curscr, true); break; case L'U': edit(id, EditKill, 0); -- cgit 1.4.0 From b200194206a943bf89dde619288eb7fbe3fee1a2 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Fri, 7 Feb 2020 21:53:50 -0500 Subject: Use complete to abbreviate commands --- catgirl.1 | 6 ++++++ command.c | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index e746150..76f527e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -118,6 +118,12 @@ Log in with the server password .El . .Sh COMMANDS +Any unique prefix can be used to abbreviate a command. +For example, +.Ic /join +can be typed +.Ic /j . +. .Ss Chat Commands .Bl -tag -width Ds .It Ic /join Ar channel diff --git a/command.c b/command.c index 41aacc9..8bd8b28 100644 --- a/command.c +++ b/command.c @@ -125,7 +125,12 @@ void command(size_t id, char *input) { } else if (input[0] == '/' && isdigit(input[1])) { commandWindow(id, &input[1]); } else { - char *cmd = strsep(&input, " "); + const char *cmd = strsep(&input, " "); + const char *unique = complete(None, cmd); + if (unique && !complete(None, cmd)) { + cmd = unique; + completeReject(); + } const struct Handler *handler = bsearch( cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); -- cgit 1.4.0 From 55173ef29777959b0b761097499e3eef397de609 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 00:02:10 -0500 Subject: Add /nick --- catgirl.1 | 2 ++ command.c | 7 +++++++ 2 files changed, 9 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 76f527e..5b9b1a5 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -130,6 +130,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /nick Ar nick +Change nicknames. .It Ic /notice Ar message Send a notice. .It Ic /quit Op Ar message diff --git a/command.c b/command.c index 8bd8b28..7416f81 100644 --- a/command.c +++ b/command.c @@ -69,6 +69,12 @@ static void commandQuit(size_t id, char *params) { set(&self.quit, (params ? params : "Goodbye")); } +static void commandNick(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("NICK :%s\r\n", params); +} + static void commandWindow(size_t id, char *params) { if (!params) return; if (isdigit(params[0])) { @@ -85,6 +91,7 @@ static const struct Handler { } Commands[] = { { "/join", commandJoin }, { "/me", commandMe }, + { "/nick", commandNick }, { "/notice", commandNotice }, { "/quit", commandQuit }, { "/quote", commandQuote }, -- cgit 1.4.0 From f5783d15c6a640d553e4e356c4ba10895ad602a3 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 01:25:07 -0500 Subject: Add /part --- catgirl.1 | 4 +++- command.c | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 5b9b1a5..4dc002e 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd February 7, 2020 +.Dd February 8, 2020 .Dt CATGIRL 1 .Os . @@ -134,6 +134,8 @@ Send an action message. Change nicknames. .It Ic /notice Ar message Send a notice. +.It Ic /part Op Ar message +Leave the channel. .It Ic /quit Op Ar message Quit IRC. .It Ic /quote Ar command diff --git a/command.c b/command.c index 7416f81..dfe4850 100644 --- a/command.c +++ b/command.c @@ -64,6 +64,14 @@ static void commandJoin(size_t id, char *params) { ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); } +static void commandPart(size_t id, char *params) { + if (params) { + ircFormat("PART %s :%s\r\n", idNames[id], params); + } else { + ircFormat("PART %s\r\n", idNames[id]); + } +} + static void commandQuit(size_t id, char *params) { (void)id; set(&self.quit, (params ? params : "Goodbye")); @@ -93,6 +101,7 @@ static const struct Handler { { "/me", commandMe }, { "/nick", commandNick }, { "/notice", commandNotice }, + { "/part", commandPart }, { "/quit", commandQuit }, { "/quote", commandQuote }, { "/window", commandWindow }, -- cgit 1.4.0 From 5c10fe0d414b655ae2cbf14c3db9216b438c5193 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 01:34:55 -0500 Subject: Add /query --- catgirl.1 | 2 ++ command.c | 9 +++++++++ 2 files changed, 11 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 4dc002e..0702f58 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -136,6 +136,8 @@ Change nicknames. Send a notice. .It Ic /part Op Ar message Leave the channel. +.It Ic /query Ar nick +Start a private conversation. .It Ic /quit Op Ar message Quit IRC. .It Ic /quote Ar command diff --git a/command.c b/command.c index dfe4850..9047e95 100644 --- a/command.c +++ b/command.c @@ -83,6 +83,13 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandQuery(size_t id, char *params) { + if (!params) return; + size_t query = idFor(params); + idColors[query] = completeColor(id, params); + uiShowID(query); +} + static void commandWindow(size_t id, char *params) { if (!params) return; if (isdigit(params[0])) { @@ -102,6 +109,7 @@ static const struct Handler { { "/nick", commandNick }, { "/notice", commandNotice }, { "/part", commandPart }, + { "/query", commandQuery }, { "/quit", commandQuit }, { "/quote", commandQuote }, { "/window", commandWindow }, @@ -151,6 +159,7 @@ void command(size_t id, char *input) { cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); if (handler) { + if (input && !input[0]) input = NULL; handler->fn(id, input); } else { uiFormat(id, Hot, NULL, "No such command %s", cmd); -- cgit 1.4.0 From 943502ea82b3965b4f652146ca03262ac6390f83 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 02:26:00 -0500 Subject: Add /close --- catgirl.1 | 2 ++ chat.h | 2 ++ command.c | 12 ++++++++++++ ui.c | 28 ++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 0702f58..9314e7a 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -146,6 +146,8 @@ Send a raw IRC command. . .Ss UI Commands .Bl -tag -width Ds +.It Ic /close Op Ar name | num +Close the named, numbered or current window. .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/chat.h b/chat.h index 081370e..9daa38c 100644 --- a/chat.h +++ b/chat.h @@ -128,6 +128,8 @@ void uiHide(void); void uiDraw(void); void uiShowID(size_t id); void uiShowNum(size_t num); +void uiCloseID(size_t id); +void uiCloseNum(size_t id); void uiRead(void); void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); void uiFormat( diff --git a/command.c b/command.c index 9047e95..e33c57e 100644 --- a/command.c +++ b/command.c @@ -100,10 +100,22 @@ static void commandWindow(size_t id, char *params) { } } +static void commandClose(size_t id, char *params) { + if (!params) { + uiCloseID(id); + } else if (isdigit(params[0])) { + uiCloseNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiCloseID(id); + } +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { + { "/close", commandClose }, { "/join", commandJoin }, { "/me", commandMe }, { "/nick", commandNick }, diff --git a/ui.c b/ui.c index 6d1338b..c738617 100644 --- a/ui.c +++ b/ui.c @@ -573,6 +573,34 @@ void uiShowNum(size_t num) { windowShow(window); } +static void windowClose(struct Window *window) { + if (window->id == Network) return; + if (windows.active == window) { + windowShow(window->prev ? window->prev : window->next); + } + if (windows.other == window) windows.other = NULL; + windowRemove(window); + for (size_t i = 0; i < BufferCap; ++i) { + free(window->buffer.lines[i]); + } + delwin(window->pad); + free(window); + statusUpdate(); +} + +void uiCloseID(size_t id) { + windowClose(windowFor(id)); +} + +void uiCloseNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowClose(window); +} + static void keyCode(int code) { size_t id = windows.active->id; switch (code) { -- cgit 1.4.0 From 2cacf15314be31b33a61007ba6e063ced96c3d41 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 02:33:41 -0500 Subject: Add /debug --- catgirl.1 | 4 ++++ command.c | 11 +++++++++++ 2 files changed, 15 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 9314e7a..3f8131f 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -148,6 +148,10 @@ Send a raw IRC command. .Bl -tag -width Ds .It Ic /close Op Ar name | num Close the named, numbered or current window. +.It Ic /debug +Toggle logging in the +.Sy +window. .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/command.c b/command.c index e33c57e..1d1c756 100644 --- a/command.c +++ b/command.c @@ -23,6 +23,16 @@ typedef void Command(size_t id, char *params); +static void commandDebug(size_t id, char *params) { + (void)id; + (void)params; + self.debug ^= true; + uiFormat( + Debug, Warm, NULL, + "\3%dDebug is %s", Gray, (self.debug ? "on" : "off") + ); +} + static void commandQuote(size_t id, char *params) { (void)id; if (params) ircFormat("%s\r\n", params); @@ -116,6 +126,7 @@ static const struct Handler { Command *fn; } Commands[] = { { "/close", commandClose }, + { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, { "/nick", commandNick }, -- cgit 1.4.0 From b6bf6d62b0bb6d203ce41e4b375c415ca8fde719 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 03:15:17 -0500 Subject: Only show expected topic/names replies --- chat.h | 5 +++++ command.c | 8 ++++++++ handle.c | 23 ++++++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/chat.h b/chat.h index 9daa38c..34e1812 100644 --- a/chat.h +++ b/chat.h @@ -114,6 +114,11 @@ void ircSend(const char *ptr, size_t len); void ircFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); +extern struct Replies { + size_t topic; + size_t names; +} replies; + void handle(struct Message msg); void command(size_t id, char *input); const char *commandIsPrivmsg(size_t id, const char *input); diff --git a/command.c b/command.c index 1d1c756..9879dbe 100644 --- a/command.c +++ b/command.c @@ -71,7 +71,15 @@ static void commandMe(size_t id, char *params) { } static void commandJoin(size_t id, char *params) { + size_t count = 1; + if (params) { + for (char *ch = params; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + } ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); + replies.topic += count; + replies.names += count; } static void commandPart(size_t id, char *params) { diff --git a/handle.c b/handle.c index 8ebc3b1..0780767 100644 --- a/handle.c +++ b/handle.c @@ -25,6 +25,8 @@ #include "chat.h" +struct Replies replies; + static const char *CapNames[] = { #define X(name, id) [id##Bit] = name, ENUM_CAP @@ -156,7 +158,15 @@ static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); completeTouch(None, self.nick, Default); - if (self.join) ircFormat("JOIN %s\r\n", self.join); + if (self.join) { + size_t count = 1; + for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", self.join); + replies.topic += count; + replies.names += count; + } } static void handleReplyISupport(struct Message *msg) { @@ -278,6 +288,7 @@ static void handleQuit(struct Message *msg) { static void handleReplyNames(struct Message *msg) { require(msg, false, 4); + if (!replies.names) return; size_t id = idFor(msg->params[2]); char buf[1024]; size_t len = 0; @@ -302,8 +313,15 @@ static void handleReplyNames(struct Message *msg) { ); } +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies.names) replies.names--; +} + static void handleReplyNoTopic(struct Message *msg) { require(msg, false, 2); + if (!replies.topic) return; + replies.topic--; uiFormat( idFor(msg->params[1]), Cold, tagTime(msg), "There is no sign in \3%02d%s\3", @@ -313,6 +331,8 @@ static void handleReplyNoTopic(struct Message *msg) { static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); + if (!replies.topic) return; + replies.topic--; uiFormat( idFor(msg->params[1]), Cold, tagTime(msg), "The sign in \3%02d%s\3 reads: %s", @@ -421,6 +441,7 @@ static const struct Handler { { "331", handleReplyNoTopic }, { "332", handleReplyTopic }, { "353", handleReplyNames }, + { "366", handleReplyEndOfNames }, { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, -- cgit 1.4.0 From ff6424a87ce22586c3e2fe1ab57ed3407bef18ca Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 03:19:56 -0500 Subject: Add /names --- catgirl.1 | 2 ++ command.c | 7 +++++++ 2 files changed, 9 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 3f8131f..ccf981b 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -130,6 +130,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /names +List users in the channel. .It Ic /nick Ar nick Change nicknames. .It Ic /notice Ar message diff --git a/command.c b/command.c index 9879dbe..a6434bf 100644 --- a/command.c +++ b/command.c @@ -101,6 +101,12 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandNames(size_t id, char *params) { + (void)params; + ircFormat("NAMES :%s\r\n", idNames[id]); + replies.names++; +} + static void commandQuery(size_t id, char *params) { if (!params) return; size_t query = idFor(params); @@ -137,6 +143,7 @@ static const struct Handler { { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, + { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, { "/part", commandPart }, -- cgit 1.4.0 From b98c7d68630a7af37f61a52a555e1aaed1c2e7af Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 03:25:50 -0500 Subject: Add /topic --- catgirl.1 | 2 ++ command.c | 10 ++++++++++ 2 files changed, 12 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index ccf981b..5394d33 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -144,6 +144,8 @@ Start a private conversation. Quit IRC. .It Ic /quote Ar command Send a raw IRC command. +.It Ic /topic Op Ar topic +Show or set the topic of the channel. .El . .Ss UI Commands diff --git a/command.c b/command.c index a6434bf..eaabc9c 100644 --- a/command.c +++ b/command.c @@ -101,6 +101,15 @@ static void commandNick(size_t id, char *params) { ircFormat("NICK :%s\r\n", params); } +static void commandTopic(size_t id, char *params) { + if (params) { + ircFormat("TOPIC %s :%s\r\n", idNames[id], params); + } else { + ircFormat("TOPIC %s\r\n", idNames[id]); + replies.topic++; + } +} + static void commandNames(size_t id, char *params) { (void)params; ircFormat("NAMES :%s\r\n", idNames[id]); @@ -150,6 +159,7 @@ static const struct Handler { { "/query", commandQuery }, { "/quit", commandQuit }, { "/quote", commandQuote }, + { "/topic", commandTopic }, { "/window", commandWindow }, }; -- cgit 1.4.0 From f502260dd0aa73b09bfbb7289b50a67592866166 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 18:29:01 -0500 Subject: Scan messages for URLs --- Makefile | 1 + catgirl.1 | 9 ++++++ chat.h | 4 +++ command.c | 11 ++++++++ handle.c | 15 ++++++++-- url.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 url.c (limited to 'command.c') diff --git a/Makefile b/Makefile index 48aba7b..bcbb0d8 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ OBJS += edit.o OBJS += handle.o OBJS += irc.o OBJS += ui.o +OBJS += url.o dev: tags all diff --git a/catgirl.1 b/catgirl.1 index 5394d33..f489d07 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -156,6 +156,15 @@ Close the named, numbered or current window. Toggle logging in the .Sy window. +.It Ic /open Op Ar count +Open each of +.Ar count +most recent URLs. +.It Ic /open Ar nick | substring +Open the most recent URL from +.Ar nick +or matching +.Ar substring . .It Ic /window Ar name Switch to window by name. .It Ic /window Ar num , Ic / Ns Ar num diff --git a/chat.h b/chat.h index 909527e..583107a 100644 --- a/chat.h +++ b/chat.h @@ -169,6 +169,10 @@ void completeClear(size_t id); size_t completeID(const char *str); enum Color completeColor(size_t id, const char *str); +void urlScan(size_t id, const char *nick, const char *mesg); +void urlOpenCount(size_t id, size_t count); +void urlOpenMatch(size_t id, const char *str); + FILE *configOpen(const char *path, const char *mode); int getopt_config( int argc, char *const *argv, diff --git a/command.c b/command.c index eaabc9c..4100928 100644 --- a/command.c +++ b/command.c @@ -144,6 +144,16 @@ static void commandClose(size_t id, char *params) { } } +static void commandOpen(size_t id, char *params) { + if (!params) { + urlOpenCount(id, 1); + } else if (isdigit(params[0])) { + urlOpenCount(id, strtoul(params, NULL, 10)); + } else { + urlOpenMatch(id, params); + } +} + static const struct Handler { const char *cmd; Command *fn; @@ -155,6 +165,7 @@ static const struct Handler { { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, + { "/open", commandOpen }, { "/part", commandPart }, { "/query", commandQuery }, { "/quit", commandQuit }, diff --git a/handle.c b/handle.c index 0780767..f919fcb 100644 --- a/handle.c +++ b/handle.c @@ -193,6 +193,7 @@ static void handleReplyISupport(struct Message *msg) { static void handleReplyMOTD(struct Message *msg) { require(msg, false, 2); char *line = msg->params[1]; + urlScan(Network, msg->nick, line); if (!strncmp(line, "- ", 2)) { uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); } else { @@ -227,6 +228,7 @@ static void handlePart(struct Message *msg) { completeClear(id); } completeRemove(id, msg->nick); + urlScan(id, msg->nick, msg->params[1]); uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tleaves \3%02d%s\3%s%s", @@ -241,6 +243,7 @@ static void handleKick(struct Message *msg) { size_t id = idFor(msg->params[0]); bool kicked = self.nick && !strcmp(msg->params[1], self.nick); completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[2]); uiFormat( id, (kicked ? Hot : Cold), tagTime(msg), "%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s", @@ -275,6 +278,7 @@ static void handleQuit(struct Message *msg) { require(msg, true, 0); size_t id; while (None != (id = completeID(msg->nick))) { + urlScan(id, msg->nick, msg->params[0]); uiFormat( id, Cold, tagTime(msg), "\3%02d%s\3\tleaves%s%s", @@ -333,8 +337,10 @@ static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); if (!replies.topic) return; replies.topic--; + size_t id = idFor(msg->params[1]); + urlScan(id, NULL, msg->params[2]); uiFormat( - idFor(msg->params[1]), Cold, tagTime(msg), + id, Cold, tagTime(msg), "The sign in \3%02d%s\3 reads: %s", hash(msg->params[1]), msg->params[1], msg->params[2] ); @@ -342,16 +348,18 @@ static void handleReplyTopic(struct Message *msg) { static void handleTopic(struct Message *msg) { require(msg, true, 2); + size_t id = idFor(msg->params[0]); if (msg->params[1][0]) { + urlScan(id, msg->nick, msg->params[1]); uiFormat( - idFor(msg->params[0]), Warm, tagTime(msg), + id, Warm, tagTime(msg), "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s", hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], msg->params[1] ); } else { uiFormat( - idFor(msg->params[0]), Warm, tagTime(msg), + id, Warm, tagTime(msg), "\3%02d%s\3\tremoves the sign in \3%02d%s\3", hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] ); @@ -400,6 +408,7 @@ static void handlePrivmsg(struct Message *msg) { bool action = isAction(msg); bool mention = !mine && isMention(msg); if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[1]); if (notice) { uiFormat( id, Warm, tagTime(msg), diff --git a/url.c b/url.c new file mode 100644 index 0000000..7790461 --- /dev/null +++ b/url.c @@ -0,0 +1,96 @@ +/* Copyright (C) 2020 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "chat.h" + +static const char *Pattern = { + "(" + "cvs|" + "ftp|" + "git|" + "gopher|" + "http|" + "https|" + "irc|" + "ircs|" + "magnet|" + "sftp|" + "ssh|" + "svn|" + "telnet|" + "vnc" + ")" + ":[^[:space:]>\"]+" +}; +static regex_t Regex; + +static void compile(void) { + static bool compiled; + if (compiled) return; + compiled = true; + int error = regcomp(&Regex, Pattern, REG_EXTENDED); + if (!error) return; + char buf[256]; + regerror(error, &Regex, buf, sizeof(buf)); + errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern); +} + +enum { Cap = 32 }; +static struct { + size_t ids[Cap]; + char *nicks[Cap]; + char *urls[Cap]; + size_t len; +} ring; + +static void push(size_t id, const char *nick, const char *url, size_t len) { + size_t i = ring.len++ % Cap; + free(ring.nicks[i]); + free(ring.urls[i]); + ring.ids[i] = id; + ring.nicks[i] = NULL; + if (nick) { + ring.nicks[i] = strdup(nick); + if (!ring.nicks[i]) err(EX_OSERR, "strdup"); + } + ring.urls[i] = strndup(url, len); + if (!ring.urls[i]) err(EX_OSERR, "strndup"); +} + +void urlScan(size_t id, const char *nick, const char *mesg) { + if (!mesg) return; + compile(); + regmatch_t match = {0}; + for (const char *ptr = mesg; *ptr; ptr += match.rm_eo) { + if (regexec(&Regex, ptr, 1, &match, 0)) break; + push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so); + } +} + +void urlOpenCount(size_t id, size_t count) { + // TODO +} + +void urlOpenMatch(size_t id, const char *str) { + // TODO +} -- cgit 1.4.0 From 3e6868414811be8902e6973c78ef2010b26a9e08 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 21:44:50 -0500 Subject: Add /copy --- catgirl.1 | 17 ++++++++++++++++- chat.c | 4 +++- chat.h | 2 ++ command.c | 5 +++++ url.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 6129b71..4dabb4f 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -9,6 +9,7 @@ .Sh SYNOPSIS .Nm .Op Fl ev +.Op Fl C Ar copy .Op Fl O Ar open .Op Fl a Ar auth .Op Fl c Ar cert @@ -47,8 +48,17 @@ following their corresponding flags. .Pp The arguments are as follows: .Bl -tag -width Ds +.It Fl C Ar util , Cm copy = Ar util +Set the utility used by +.Ic /copy . +The default is the first available of +.Xr pbcopy 1 , +.Xr wl-copy 1 , +.Xr xclip 1 , +.Xr xsel 1 . +. .It Fl O Ar util , Cm open = Ar util -Set the command used by +Set the utility used by .Ic /open . The default is the first available of .Xr open 1 , @@ -160,6 +170,11 @@ Show or set the topic of the channel. .Bl -tag -width Ds .It Ic /close Op Ar name | num Close the named, numbered or current window. +.It Ic /copy Op Ar nick | substring +Copy the most recent URL from +.Ar nick +or matching +.Ar substring . .It Ic /debug Toggle logging in the .Sy diff --git a/chat.c b/chat.c index 77aa61d..dbad242 100644 --- a/chat.c +++ b/chat.c @@ -81,9 +81,10 @@ int main(int argc, char *argv[]) { const char *user = NULL; const char *real = NULL; - const char *Opts = "!O:a:c:eh:j:k:n:p:r:u:vw:"; + const char *Opts = "!C:O:a:c:eh:j:k:n:p:r:u:vw:"; const struct option LongOpts[] = { { "insecure", no_argument, NULL, '!' }, + { "copy", required_argument, NULL, 'C' }, { "open", required_argument, NULL, 'O' }, { "sasl-plain", required_argument, NULL, 'a' }, { "cert", required_argument, NULL, 'c' }, @@ -104,6 +105,7 @@ int main(int argc, char *argv[]) { while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) { switch (opt) { break; case '!': insecure = true; + break; case 'C': urlCopyUtil = optarg; break; case 'O': urlOpenUtil = optarg; break; case 'a': sasl = true; self.plain = optarg; break; case 'c': cert = optarg; diff --git a/chat.h b/chat.h index 3084359..8bc8e81 100644 --- a/chat.h +++ b/chat.h @@ -170,9 +170,11 @@ size_t completeID(const char *str); enum Color completeColor(size_t id, const char *str); extern const char *urlOpenUtil; +extern const char *urlCopyUtil; void urlScan(size_t id, const char *nick, const char *mesg); void urlOpenCount(size_t id, size_t count); void urlOpenMatch(size_t id, const char *str); +void urlCopyMatch(size_t id, const char *str); FILE *configOpen(const char *path, const char *mode); int getopt_config( diff --git a/command.c b/command.c index 4100928..feb52b7 100644 --- a/command.c +++ b/command.c @@ -154,11 +154,16 @@ static void commandOpen(size_t id, char *params) { } } +static void commandCopy(size_t id, char *params) { + urlCopyMatch(id, params); +} + static const struct Handler { const char *cmd; Command *fn; } Commands[] = { { "/close", commandClose }, + { "/copy", commandCopy }, { "/debug", commandDebug }, { "/join", commandJoin }, { "/me", commandMe }, diff --git a/url.c b/url.c index c9c4d5c..7ab1e53 100644 --- a/url.c +++ b/url.c @@ -122,6 +122,47 @@ static void urlOpen(const char *url) { _exit(EX_CONFIG); } +const char *urlCopyUtil; +static const char *CopyUtils[] = { "pbcopy", "wl-copy", "xclip", "xsel" }; + +static void urlCopy(const char *url) { + int rw[2]; + int error = pipe(rw); + if (error) err(EX_OSERR, "pipe"); + + ssize_t len = write(rw[1], url, strlen(url)); + if (len < 0) err(EX_IOERR, "write"); + + error = close(rw[1]); + if (error) err(EX_IOERR, "close"); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) { + close(rw[0]); + return; + } + + dup2(rw[0], STDIN_FILENO); + dup2(procPipe[1], STDOUT_FILENO); + dup2(procPipe[1], STDERR_FILENO); + close(rw[0]); + if (urlCopyUtil) { + execlp(urlCopyUtil, urlCopyUtil, NULL); + warn("%s", urlCopyUtil); + _exit(EX_CONFIG); + } + for (size_t i = 0; i < ARRAY_LEN(CopyUtils); ++i) { + execlp(CopyUtils[i], CopyUtils[i], NULL); + if (errno != ENOENT) { + warn("%s", CopyUtils[i]); + _exit(EX_CONFIG); + } + } + warnx("no copy utility found"); + _exit(EX_CONFIG); +} + void urlOpenCount(size_t id, size_t count) { for (size_t i = 1; i <= Cap; ++i) { const struct URL *url = &ring.urls[(ring.len - i) % Cap]; @@ -143,3 +184,19 @@ void urlOpenMatch(size_t id, const char *str) { } } } + +void urlCopyMatch(size_t id, const char *str) { + for (size_t i = 1; i <= Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len - i) % Cap]; + if (!url->url) break; + if (url->id != id) continue; + if ( + !str + || (url->nick && !strcmp(url->nick, str)) + || strstr(url->url, str) + ) { + urlCopy(url->url); + break; + } + } +} -- cgit 1.4.0 From af14947103775fa0251a1a1d96a9e8cae73141c9 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 8 Feb 2020 21:50:29 -0500 Subject: Trim whitespace from both ends of command params --- command.c | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'command.c') diff --git a/command.c b/command.c index feb52b7..f88a6d5 100644 --- a/command.c +++ b/command.c @@ -223,6 +223,12 @@ void command(size_t id, char *input) { cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar ); if (handler) { + if (input) { + input += strspn(input, " "); + size_t len = strlen(input); + while (input[len - 1] == ' ') input[--len] = '\0'; + if (!input[0]) input = NULL; + } if (input && !input[0]) input = NULL; handler->fn(id, input); } else { -- cgit 1.4.0 From 5254e1035c5945407ee354276f839426fc17e432 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 9 Feb 2020 14:09:27 -0500 Subject: Add /help Now with automatic search! Also had to fix the SIGCHLD handling... --- catgirl.1 | 6 ++++++ chat.c | 2 ++ command.c | 19 +++++++++++++++++++ ui.c | 7 +++++++ 4 files changed, 34 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index eb7310d..5772db3 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -179,6 +179,12 @@ or matching Toggle logging in the .Sy window. +.It Ic /help Op Ar search +View this manual. +Type +.Ic q +to return to +.Nm . .It Ic /open Op Ar count Open each of .Ar count diff --git a/chat.c b/chat.c index dbad242..ff74485 100644 --- a/chat.c +++ b/chat.c @@ -191,6 +191,7 @@ int main(int argc, char *argv[]) { if (signals[SIGINT] || signals[SIGTERM]) break; if (signals[SIGCHLD]) { + signals[SIGCHLD] = 0; int status; while (0 < waitpid(-1, &status, WNOHANG)) { if (WIFEXITED(status) && WEXITSTATUS(status)) { @@ -206,6 +207,7 @@ int main(int argc, char *argv[]) { ); } } + uiShow(); } if (signals[SIGWINCH]) { diff --git a/command.c b/command.c index f88a6d5..44d0d54 100644 --- a/command.c +++ b/command.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "chat.h" @@ -158,6 +159,23 @@ static void commandCopy(size_t id, char *params) { urlCopyMatch(id, params); } +static void commandHelp(size_t id, char *params) { + (void)id; + uiHide(); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + char buf[256]; + snprintf(buf, sizeof(buf), "ip%s$", (params ? params : "COMMANDS")); + setenv("LESS", buf, 1); + execlp("man", "man", "1", "catgirl", NULL); + dup2(procPipe[1], STDERR_FILENO); + warn("man"); + _exit(EX_UNAVAILABLE); +} + static const struct Handler { const char *cmd; Command *fn; @@ -165,6 +183,7 @@ static const struct Handler { { "/close", commandClose }, { "/copy", commandCopy }, { "/debug", commandDebug }, + { "/help", commandHelp }, { "/join", commandJoin }, { "/me", commandMe }, { "/names", commandNames }, diff --git a/ui.c b/ui.c index 9abfffc..66a9c59 100644 --- a/ui.c +++ b/ui.c @@ -156,13 +156,18 @@ static const char *ExitFocusMode = "\33[?1004l"; static const char *EnterPasteMode = "\33[?2004h"; static const char *ExitPasteMode = "\33[?2004l"; +static bool hidden; + void uiShow(void) { putp(EnterFocusMode); putp(EnterPasteMode); fflush(stdout); + hidden = false; + uiDraw(); } void uiHide(void) { + hidden = true; putp(ExitFocusMode); putp(ExitPasteMode); endwin(); @@ -250,6 +255,7 @@ void uiInit(void) { } void uiDraw(void) { + if (hidden) return; wnoutrefresh(status); struct Window *window = windows.active; pnoutrefresh( @@ -755,6 +761,7 @@ static void keyStyle(wchar_t ch) { } void uiRead(void) { + if (hidden) return; int ret; wint_t ch; static bool style; -- cgit 1.4.0 From 2bb3590de9eb7f9195c32fb94491515ac395f1db Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 9 Feb 2020 15:35:02 -0500 Subject: Add /msg Services tend to tell you to use /msg so it definitely needs to exist. --- catgirl.1 | 2 ++ command.c | 8 ++++++++ 2 files changed, 10 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 5772db3..8679f22 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -148,6 +148,8 @@ can be typed Join a channel. .It Ic /me Op Ar action Send an action message. +.It Ic /msg Ar nick message +Send a private message. .It Ic /names List users in the channel. .It Ic /nick Ar nick diff --git a/command.c b/command.c index 44d0d54..6d9ef9b 100644 --- a/command.c +++ b/command.c @@ -71,6 +71,13 @@ static void commandMe(size_t id, char *params) { commandPrivmsg(id, buf); } +static void commandMsg(size_t id, char *params) { + (void)id; + char *nick = strsep(¶ms, " "); + if (!params) return; + ircFormat("PRIVMSG %s :%s\r\n", nick, params); +} + static void commandJoin(size_t id, char *params) { size_t count = 1; if (params) { @@ -186,6 +193,7 @@ static const struct Handler { { "/help", commandHelp }, { "/join", commandJoin }, { "/me", commandMe }, + { "/msg", commandMsg }, { "/names", commandNames }, { "/nick", commandNick }, { "/notice", commandNotice }, -- cgit 1.4.0 From 3436cd1068ca37cf4043bc8dc83e3b8890edcb2b Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 9 Feb 2020 16:45:49 -0500 Subject: Add /whois --- catgirl.1 | 2 ++ chat.h | 1 + command.c | 8 +++++ handle.c | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) (limited to 'command.c') diff --git a/catgirl.1 b/catgirl.1 index 8679f22..ac558a9 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -166,6 +166,8 @@ Quit IRC. Send a raw IRC command. .It Ic /topic Op Ar topic Show or set the topic of the channel. +.It Ic /whois Ar nick +Query information about a user. .El . .Ss UI Commands diff --git a/chat.h b/chat.h index 24360f0..f79cc70 100644 --- a/chat.h +++ b/chat.h @@ -120,6 +120,7 @@ void ircFormat(const char *format, ...) extern struct Replies { size_t topic; size_t names; + size_t whois; } replies; void handle(struct Message msg); diff --git a/command.c b/command.c index 6d9ef9b..3e201cc 100644 --- a/command.c +++ b/command.c @@ -124,6 +124,13 @@ static void commandNames(size_t id, char *params) { replies.names++; } +static void commandWhois(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("WHOIS :%s\r\n", params); + replies.whois++; +} + static void commandQuery(size_t id, char *params) { if (!params) return; size_t query = idFor(params); @@ -203,6 +210,7 @@ static const struct Handler { { "/quit", commandQuit }, { "/quote", commandQuote }, { "/topic", commandTopic }, + { "/whois", commandWhois }, { "/window", commandWindow }, }; diff --git a/handle.c b/handle.c index 2cc7a25..2ef2477 100644 --- a/handle.c +++ b/handle.c @@ -374,6 +374,97 @@ static void handleTopic(struct Message *msg) { } } +static void handleReplyWhoisUser(struct Message *msg) { + require(msg, false, 6); + if (!replies.whois) return; + completeTouch(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %s!%s@%s (%s)", + hash(msg->params[2]), msg->params[1], + msg->params[1], msg->params[2], msg->params[3], msg->params[5] + ); +} + +static void handleReplyWhoisServer(struct Message *msg) { + require(msg, false, 4); + if (!replies.whois) return; + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis connected to %s (%s)", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], msg->params[3] + ); +} + +static void handleReplyWhoisIdle(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + unsigned long idle = strtoul(msg->params[2], NULL, 10); + const char *unit = "second"; + if (idle / 60) { idle /= 60; unit = "minute"; } + if (idle / 60) { idle /= 60; unit = "hour"; } + if (idle / 24) { idle /= 24; unit = "day"; } + time_t signon = (msg->params[3] ? strtoul(msg->params[3], NULL, 10) : 0); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis idle for %lu %s%s%s%.*s", + completeColor(Network, msg->params[1]), msg->params[1], + idle, unit, (idle != 1 ? "s" : ""), + (signon ? ", signed on " : ""), + 24, (signon ? ctime(&signon) : "") + ); +} + +static void handleReplyWhoisChannels(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + char buf[1024]; + size_t len = 0; + while (msg->params[2]) { + char *channel = strsep(&msg->params[2], " "); + channel += strspn(channel, self.prefixes); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), hash(channel), channel + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis in %s", + completeColor(Network, msg->params[1]), msg->params[1], buf + ); +} + +static void handleReplyWhoisGeneric(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + if (msg->params[3]) { + msg->params[0] = msg->params[2]; + msg->params[2] = msg->params[3]; + msg->params[3] = msg->params[0]; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%s%s%s", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], + (msg->params[3] ? " " : ""), + (msg->params[3] ? msg->params[3] : "") + ); +} + +static void handleReplyEndOfWhois(struct Message *msg) { + require(msg, false, 2); + if (!replies.whois) return; + if (!self.nick || strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } + replies.whois--; +} + static bool isAction(struct Message *msg) { if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; msg->params[1] += 8; @@ -495,6 +586,15 @@ static const struct Handler { } Handlers[] = { { "001", handleReplyWelcome }, { "005", handleReplyISupport }, + { "276", handleReplyWhoisGeneric }, + { "307", handleReplyWhoisGeneric }, + { "311", handleReplyWhoisUser }, + { "312", handleReplyWhoisServer }, + { "313", handleReplyWhoisGeneric }, + { "317", handleReplyWhoisIdle }, + { "318", handleReplyEndOfWhois }, + { "319", handleReplyWhoisChannels }, + { "330", handleReplyWhoisGeneric }, { "331", handleReplyNoTopic }, { "332", handleReplyTopic }, { "353", handleReplyNames }, @@ -502,6 +602,7 @@ static const struct Handler { { "372", handleReplyMOTD }, { "432", handleErrorErroneousNickname }, { "433", handleErrorNicknameInUse }, + { "671", handleReplyWhoisGeneric }, { "900", handleReplyLoggedIn }, { "904", handleErrorSASLFail }, { "905", handleErrorSASLFail }, -- cgit 1.4.0 From 00f0f94fc80ebecff531388e38d0fb121e3f4e74 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Mon, 10 Feb 2020 20:17:21 -0500 Subject: Delegate to commandPrivmsg from commandMsg --- command.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'command.c') diff --git a/command.c b/command.c index 3e201cc..cab1d26 100644 --- a/command.c +++ b/command.c @@ -75,7 +75,7 @@ static void commandMsg(size_t id, char *params) { (void)id; char *nick = strsep(¶ms, " "); if (!params) return; - ircFormat("PRIVMSG %s :%s\r\n", nick, params); + commandPrivmsg(idFor(nick), params); } static void commandJoin(size_t id, char *params) { -- cgit 1.4.0 From 80a79467efca8f17e440cb63009c60dd8e78cc63 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Mon, 10 Feb 2020 20:24:07 -0500 Subject: Only automatically switch to expected joins --- chat.h | 1 + command.c | 1 + handle.c | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) (limited to 'command.c') diff --git a/chat.h b/chat.h index 03a0a50..f47b244 100644 --- a/chat.h +++ b/chat.h @@ -120,6 +120,7 @@ void ircFormat(const char *format, ...) __attribute__((format(printf, 1, 2))); extern struct Replies { + size_t join; size_t topic; size_t names; size_t whois; diff --git a/command.c b/command.c index cab1d26..5cb43cf 100644 --- a/command.c +++ b/command.c @@ -86,6 +86,7 @@ static void commandJoin(size_t id, char *params) { } } ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); + replies.join += count; replies.topic += count; replies.names += count; } diff --git a/handle.c b/handle.c index fd2a67f..0db7fd9 100644 --- a/handle.c +++ b/handle.c @@ -164,6 +164,7 @@ static void handleReplyWelcome(struct Message *msg) { if (*ch == ',') count++; } ircFormat("JOIN %s\r\n", self.join); + replies.join += count; replies.topic += count; replies.names += count; } @@ -211,7 +212,10 @@ static void handleJoin(struct Message *msg) { } idColors[id] = hash(msg->params[0]); completeTouch(None, msg->params[0], idColors[id]); - uiShowID(id); + if (replies.join) { + uiShowID(id); + replies.join--; + } } completeTouch(id, msg->nick, hash(msg->user)); if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { -- cgit 1.4.0