2 * Copyright (C) 2012-2013 Andy Spencer <andy753421@gmail.com>
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #define _XOPEN_SOURCE_EXTENDED
27 #include <sys/signalfd.h>
30 #include <ncursesw/ncurses.h>
41 #define COLOR_BROWN 130
44 #define KEY_CTRL_D '\4'
45 #define KEY_CTRL_E '\5'
46 #define KEY_CTRL_F '\6'
47 #define KEY_CTRL_G '\7'
48 #define KEY_CTRL_L '\14'
49 #define KEY_CTRL_N '\16'
50 #define KEY_CTRL_P '\20'
51 #define KEY_CTRL_T '\24'
52 #define KEY_CTRL_U '\25'
53 #define KEY_CTRL_X '\30'
54 #define KEY_CTRL_Y '\31'
56 #define KEY_RETURN '\12'
57 #define KEY_ESCAPE '\33'
58 #define KEY_DELETE '\177'
61 typedef struct window_t window_t;
62 typedef struct watch_t watch_t;
64 typedef struct window_t {
81 typedef struct watch_t {
129 static poll_t poll_in;
130 static poll_t poll_sig;
142 static char cmd_buf[4096];
148 static buf_t item_buf;
151 static int item_scroll;
152 static char save_buf[4096];
156 static int hdr_lines;
157 static int sts_lines;
158 static int cmd_lines;
160 static server_t sys_srv;
161 static channel_t sys_chan;
162 static window_t sys_win;
164 static window_t *focus;
165 static window_t *windows;
167 static watch_t *watches;
169 static int ncolors = 1;
170 static short colors[256+1][256+1];
171 static short color_title;
172 static short color_status;
173 static short color_date;
174 static short color_spell;
176 static const char *themes[][2] = {
177 [THEME_NORMAL] { "normal", "Default theme" },
178 [THEME_DARK] { "dark", "Dark background" },
179 [THEME_LIGHT] { "light", "Light background" },
182 static const char *abbrevs[][2] = {
183 [ABBREV_NONE] { "none", "Do not abbreviate" },
184 [ABBREV_FIRST] { "first", "First name only" },
185 [ABBREV_LAST] { "last", "Last name only" },
186 [ABBREV_FLAST] { "flast", "First initial last name" },
187 [ABBREV_INITIALS] { "initials", "Initials only" },
190 /* Helper functions */
191 static short color(int fg, int bg)
193 if (!colors[fg+1][bg+1]) {
194 init_pair(ncolors, fg, bg);
195 colors[fg+1][bg+1] = ncolors;
198 return COLOR_PAIR(colors[fg+1][bg+1]);
201 static void abbreviate(char **dst, const char *name)
208 int max = 32, fi = 0, fli = 0, li = 0, ii = 0;
209 int word = 0, space = 1;
214 for (int i = 0; name[i]; i++) {
215 if (name[i] == ' ' || name[i] == '-') {
220 if (word == 0 && fi < max)
221 first[fi++] = tolower(name[i]);
222 if (word > 0 && li < max)
223 last[li++] = tolower(name[i]);
224 if (((space && word == 0) || word > 0) && fli < max)
225 flast[fli++] = tolower(name[i]);
226 if (space && ii < max)
227 initials[ii++] = tolower(name[i]);
237 case ABBREV_NONE: strset(dst, name); break;
238 case ABBREV_FIRST: strset(dst, first); break;
239 case ABBREV_LAST: strset(dst, li?last:first); break;
240 case ABBREV_FLAST: strset(dst, li?flast:first); break;
241 case ABBREV_INITIALS: strset(dst, initials); break;
245 static int msg_compare(const void *_a, const void *_b)
247 const message_t *a = _a;
248 const message_t *b = _b;
249 return a->when < b->when ? -1 :
250 a->when > b->when ? 1 : 0;
253 static unsigned str_hash(const char *str)
255 unsigned long hash = 0;
256 for (int i = 0; str[i]; i++)
257 hash = str[i] + (hash<<6) + (hash<<16) - hash;
258 for (int i = 0; str[i]; i++)
259 hash = str[i] + (hash<<6) + (hash<<16) - hash;
263 static int rgb_color(int r, int g, int b)
265 return MIN(r*6/0x100, 5)*6*6 +
266 MIN(g*6/0x100, 5)*6 +
267 MIN(b*6/0x100, 5) + 16;
270 static int str_color(const char *str)
272 int h,s,l, t,c,x,m, r,g,b;
274 unsigned int n = str_hash(str);
276 h = (n / 0x1) % 0x100; // 0..256
277 s = (n / 0x100) % 0x100; // 0..256
278 l = (n / 0x10000) % 0x100; // 0..256
280 h = h * 360 / 0x100; // 0..360
281 s = s / 0x4 + 0xC0; // 0..256
284 case THEME_DARK: l = l / 0x4 + 0x80; break;
285 case THEME_LIGHT: l = l / 0x4 + 0x30; break;
286 case THEME_NORMAL: l = l / 0x4 + 0x60; break;
289 t = h / 60; // ,1,2,3,4,5
290 c = ((0x80-abs(l-0x80))*s)/0x80; // ..256
291 x = (c*(60-abs((h%120)-60)))/60; // ..256
292 m = l - c/2; // ..128
294 r = t==0 || t==5 ? c+m :
295 t==1 || t==4 ? x+m : m;
296 g = t==1 || t==2 ? c+m :
297 t==0 || t==3 ? x+m : m;
298 b = t==3 || t==4 ? c+m :
299 t==2 || t==5 ? x+m : m;
301 x = rgb_color(r, g, b);
303 //debug("%12s %3d,%02x,%02x -> "
304 // "tcxm=%d,%02x,%02x,%02x -> "
305 // "rgb=%02x,%02x,%02x -> %d",
306 // str, h,s,l, t,c,x,m, r,g,b, x);
311 /* Window functions */
312 static int in_window(message_t *msg, window_t *win)
314 if (win->server && win->server != msg->channel->server)
316 if (win->channel && win->channel != msg->channel)
318 if (!reg_match(&win->filter, msg->channel->name))
323 static window_t *add_window(channel_t *dest)
325 window_t *win = new0(window_t);
329 abbreviate(&win->name, dest->name);
331 window_t **last = &windows;
333 last = &(*last)->next;
338 static window_t *find_window(const char *name)
340 for (window_t *win = windows; win; win = win->next)
341 if (match(win->name, name))
346 static window_t *next_window(window_t *win)
348 for (window_t *cur = win->next; cur; cur = cur->next)
354 static window_t *prev_window(window_t *win)
356 window_t *prev = NULL;
357 for (window_t *cur = windows; cur != win; cur = cur->next)
363 static channel_t *cycle_channel(window_t *win)
365 /* Clear seen flags */
366 for (int i = 0; i < history; i++)
367 messages[i].channel->seen = 0;
369 /* Find current channel */
370 channel_t *first = NULL, *cur = NULL, *next = NULL;
371 for (int i = 0; i < history && !next; i++) {
372 message_t *msg = &messages[i];
373 if (msg->channel->seen)
375 msg->channel->seen = 1;
376 if (!in_window(msg, win))
380 if (msg->channel == win->dest)
383 first = msg->channel;
385 return next ?: first ?: &sys_chan;
388 static channel_t *last_channel(window_t *win)
390 if (win->dest == &sys_chan &&
391 win->saved != &sys_chan)
393 for (int i = history-1; i>=0; i--)
394 if (in_window(&messages[i], win))
395 return messages[i].channel;
399 static void update_windows(void)
405 focus = next_window(focus) ?:
406 prev_window(focus) ?: &sys_win;
411 for (window_t *cur = windows; cur; cur = cur->next)
412 if (cur->channel && !cur->custom)
413 abbreviate(&cur->name, cur->channel->name);
415 /* Process new messages */
416 for (; seen < history; seen++) {
417 message_t *msg = &messages[seen];
418 channel_t *chan = msg->channel;
419 window_t *prime = NULL;
422 /* Skip focused messages */
423 if (in_window(msg, focus))
426 /* Flag existing windows */
427 for (window_t *cur = windows; cur; cur = cur->next) {
428 if (!in_window(msg, cur))
430 if (cur->channel == chan)
437 /* No window, create a new one */
443 prime = add_window(chan);
444 prime->channel = chan;
451 static server_t *find_server(const char *name)
453 for (server_t *cur = servers; cur; cur = cur->next)
454 if (match(cur->name, name))
459 static channel_t *find_channel(const char *name)
461 for (channel_t *cur = channels; cur; cur = cur->next)
462 if (match(cur->name, name))
467 /* Watch functions */
468 static watch_t *add_watch(const char *name)
470 watch_t *watch = new0(watch_t);
471 strset(&watch->name, name);
473 watch_t **last = &watches;
475 last = &(*last)->next;
480 static watch_t *find_watch(const char *name)
482 for (watch_t *watch = watches; watch; watch = watch->next)
483 if (match(watch->name, name))
488 static void set_watch(const char *text)
490 static char name[64];
491 static char regex[512];
493 sscanf(text, "%s %d %[^\n]", name, &color, regex);
495 watch_t *watch = find_watch(name);
497 watch = add_watch(name);
498 reg_set(&watch->regex, regex, MATCHES);
499 watch->color = color;
502 static void watch_run(const char *text)
504 for (watch_t *watch = watches; watch; watch = watch->next)
505 reg_match(&watch->regex, text);
508 static void watch_end(void)
510 for (watch_t *watch = watches; watch; watch = watch->next)
511 watch->regex.count = 0;
514 static void watch_on(int pos)
516 for (watch_t *watch = watches; watch; watch = watch->next)
517 if (reg_check(&watch->regex, pos) & MATCH_START)
518 attron(color(watch->color, -1) | A_BOLD);
521 static void watch_off(int pos)
523 for (watch_t *watch = watches; watch; watch = watch->next)
524 if (reg_check(&watch->regex, pos) & MATCH_STOP)
525 attroff(color(watch->color, -1) | A_BOLD);
528 /* Print functions */
529 static int word_wrap(printer_t *pr, const char *txt,
530 const char **l, const char **r)
532 /* In previous word? */
533 if (txt > *l && txt < *r)
536 /* Find left and right ends of the word */
538 while (*txt != '\0' &&
539 *txt != ' ' && *txt != '\t' &&
540 *txt != '\r' && *txt != '\n')
544 /* End goes past the margin? */
546 int edge = COLS - pr->margin;
547 if (pr->col + len > edge) {
548 if (pr->indent + len > edge)
549 *r = *l + (edge - pr->indent);
550 return pr->col != pr->indent;
556 static int irc_number(const char *txt, int *i)
561 char b = a ? txt[(*i)+1] : 0;
563 n = ('0' <= b && b <= '9') ? 2 : 1;
565 n = ('0' <= b && b <= '5') ? 2 : 1;
566 if ('2' <= a && a <= '9')
570 for (int j = 0; j < n; j++)
571 r = r*10 + txt[(*i)++]-'0';
575 static int irc_color(const char *txt, int *i)
577 static int color_map[16][3] = {
578 [0x0] { 0xFF, 0xFF, 0xFF }, // White
579 [0x1] { 0x00, 0x00, 0x00 }, // Black
580 [0x2] { 0x00, 0x00, 0x80 }, // Navy Blue
581 [0x3] { 0x00, 0x80, 0x00 }, // Green
582 [0x4] { 0xFF, 0x00, 0x00 }, // Red
583 [0x5] { 0x80, 0x40, 0x40 }, // Brown
584 [0x6] { 0x80, 0x00, 0xFF }, // Purple
585 [0x7] { 0x80, 0x80, 0x00 }, // Olive
586 [0x8] { 0xFF, 0xFF, 0x00 }, // Yellow
587 [0x9] { 0x00, 0xFF, 0x00 }, // Lime Green
588 [0xA] { 0x00, 0x80, 0x80 }, // Teal
589 [0xB] { 0x00, 0xFF, 0xFF }, // Aqua Light
590 [0xC] { 0x00, 0x00, 0xFF }, // Royal Blue
591 [0xD] { 0xFF, 0x00, 0xFF }, // Hot Pink
592 [0xE] { 0x80, 0x80, 0x80 }, // Dark Gray
593 [0xF] { 0xC0, 0xC0, 0xC0 }, // Light Gray
603 case '\002': attr = A_BOLD; break; // bold
604 case '\011': attr = A_ITALIC; break; // italic
605 case '\023': attr = A_UNDERLINE; break; // strike
606 case '\025': attr = A_UNDERLINE; break; // underline
607 case '\037': attr = A_UNDERLINE; break; // underline
608 case '\026': attr = A_REVERSE; break; // reverse
611 case '\017': reset = 1; break; // reset
617 fg = irc_number(txt, i);
618 if (txt[*i] == ',') {
620 bg = irc_number(txt, i);
638 attr_get(&tmp, &pair, 0);
639 attr_set(tmp^attr, pair, 0);
641 else if (fg != -1 || bg != -1) {
642 if (0 <= fg && fg <= 15)
643 fg = rgb_color(color_map[fg][0],
646 if (0 <= bg && bg <= 15)
647 bg = rgb_color(color_map[bg][0],
650 attron(color(fg, bg));
655 static void print_string(printer_t *pr, const char *txt)
660 const char *l = 0, *r = 0;
662 for (i = 0; txt[i]; i++) {
663 if (word_wrap(pr, &txt[i], &l, &r)) {
665 pr->col = pr->indent;
672 if (irc_color(txt, &i))
674 if (pr->spell && (i == 0 || txt[i-1] == ' '))
675 if (spell_check(&txt[i], NULL))
682 pr->col -= pr->indent;
686 pr->col += pr->indent;
691 pr->col = pr->indent;
692 if (pr->limit && li++ > pr->limit)
693 txt = "\n\011[continued]\017" - i;
696 if ((n = mbtowc(&wc, &txt[i], 1)) == -1)
700 pr->row >= pr->top &&
702 mvaddnwstr(pr->row, pr->col, &wc, 1);
706 if (pr->spell && (txt[i] == ' ' || !txt[i+1]))
707 attroff(color_spell);
716 static void print_format(printer_t *pr, const char *fmt, ...)
718 static char buf[4096];
721 vsnprintf(buf, sizeof(buf), fmt, ap);
724 print_string(pr, buf);
727 static void print_message(printer_t *pr, message_t *msg)
729 if (!in_window(msg, focus))
732 time_t timep = msg->when;
733 struct tm *tm = localtime(&timep);
736 pr->limit = full ? 0 : 5;
745 print_format(pr, "%04d-%02d-%02d %02d:%02d ",
746 tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
747 tm->tm_hour, tm->tm_min);
749 print_format(pr, "%02d:%02d ", tm->tm_hour, tm->tm_min);
753 if (!focus->channel) {
754 attron(str_color(msg->channel->name) | A_BOLD);
755 print_format(pr, "[%s] ", msg->channel->name);
756 attroff(str_color(msg->channel->name) | A_BOLD);
761 attron(str_color(msg->from->name));
762 print_format(pr, "%s: ", msg->from->name);
763 attroff(str_color(msg->from->name));
765 print_format(pr, "*** ");
769 watch_run(msg->text);
772 pr->indent = pr->col;
773 print_string(pr, msg->text);
780 static void print_status(printer_t *pr)
783 print_format(pr, " Windows: ");
784 pr->indent = pr->col;
786 for (window_t *cur = windows; cur; cur = cur->next) {
794 if (!cur->channel && cur->flag)
795 print_format(pr, "[%s:%d]", cur->name, cur->flag);
796 else if (!cur->channel)
797 print_format(pr, "[%s]", cur->name);
799 print_format(pr, "%s:%d", cur->name, cur->flag);
801 print_format(pr, "%s", cur->name);
809 static void print_clear(printer_t *pr)
811 for (int i = pr->top; i < pr->end; i++)
812 mvhline(i, 0, ' ', COLS);
816 static int get_lines(int pos)
818 printer_t pr = {.print=0};
819 print_message(&pr, &messages[pos]);
823 static int next_message(int pos)
828 if (in_window(&messages[pos], focus))
833 static int prev_message(int pos)
836 if (++pos >= history)
838 if (in_window(&messages[pos], focus))
844 static void add_item(const char *prefix, const char *value, const char *desc)
850 append(&item_buf, (void*)&item, sizeof(item));
852 item_pos = prefix - cmd_buf;
855 static int num_items(void)
857 return item_buf.len / sizeof(item_t);
860 static item_t *get_item(int i)
864 if (i >= item_buf.len/sizeof(item_t))
866 return (item_t*)&item_buf.data[i*sizeof(item_t)];
869 static int compare_items(const void *_a, const void *_b)
871 const item_t *a = _a;
872 const item_t *b = _b;
873 return compare(a->value, b->value) ?:
874 compare(a->desc, b->desc);
877 static void sort_items(void)
881 qsort(item_buf.data, num_items(), sizeof(item_t), compare_items);
882 item_t *dst = (item_t*)item_buf.data;
883 item_t *src = (item_t*)item_buf.data;
884 for (int i = 0; i < num_items(); i++, src++)
885 if (compare_items(src, dst))
887 item_buf.len = (char*)(dst+1) - item_buf.data;
891 static void send_complete(const char *text)
894 if (prefix(text, "/theme ", &arg)) {
895 complete_array(arg, themes, N_ELEMENTS(themes));
897 else if (prefix(text, "/abbrev ", &arg)) {
898 complete_array(arg, abbrevs, N_ELEMENTS(abbrevs));
900 else if (prefix(text, "/server ", &arg)) {
901 complete_server(arg);
903 else if (prefix(text, "/channel ", &arg)) {
904 complete_channel(arg);
906 else if (prefix(text, "/watch ", &arg)) {
911 "/quit", "Quit the program",
912 "/sort", "Sort chat history",
913 "/theme ", "Change the theme",
914 "/abbrev ", "Abbreviate names",
915 "/watch ", "Watch for regex",
916 "/open", "Open window",
917 "/close", "Close window",
918 "/name ", "Set window name",
919 "/server ", "Set window server",
920 "/channel ", "Set window channel",
921 "/filter ", "Set window filter",
925 chat_complete(last_channel(focus), text);
928 static int send_command(const char *text)
931 window_t *win = focus;
933 if (match(text, "/quit")) {
936 else if (match(text, "/sort")) {
937 qsort(messages, history, sizeof(message_t), msg_compare);
939 else if (prefix(text, "/theme", &arg)) {
940 for (int i = 0; i < N_ELEMENTS(themes); i++)
941 if (match(themes[i][0], arg))
944 else if (prefix(text, "/abbrev", &arg)) {
945 for (int i = 0; i < N_ELEMENTS(abbrevs); i++)
946 if (match(abbrevs[i][0], arg))
949 else if (prefix(text, "/watch", &arg) && arg) {
952 else if (prefix(text, "/open", &arg)) {
953 win = find_window(arg ?: focus->dest->name);
954 if (!win || win == focus) {
955 win = add_window(focus->dest);
956 win->channel = focus->dest;
961 else if (match(text, "/close")) {
964 else if (prefix(text, "/name", &arg) && arg) {
966 if (match(focus->name, arg) &&
968 focus->dest != &sys_chan)
969 strset(&focus->dest->name, arg);
971 strset(&focus->name, arg);
973 else if (prefix(text, "/server", &arg)) {
974 win->server = find_server(arg);
976 else if (prefix(text, "/channel", &arg)) {
977 win->channel = find_channel(arg);
978 win->dest = win->channel ?: win->dest;
980 else if (prefix(text, "/filter", &arg)) {
981 reg_set(&focus->filter, arg, 0);
983 else if (focus->dest != &sys_chan) {
984 chat_send(focus->dest, text);
992 /* Drawing functions */
993 static void draw_header(void)
1003 const char *topic = focus->dest->topic ?: "No Topic";
1004 attron(color_title);
1006 print_format(&pr, "%s", topic);
1007 attroff(color_title);
1008 hdr_lines = pr.row + 1;
1011 static void draw_chat(void)
1013 /* Clear pinned if scrolling */
1014 if (focus->scroll && focus->pinned < 0)
1015 focus->pinned = history-1;
1017 /* Scroll up to previous messages */
1018 while (focus->scroll < 0) {
1019 int lines = get_lines(focus->pinned);
1021 /* Scrolling to a warpped line in this message */
1022 if (lines + focus->scroll > 0)
1025 /* Find the previous message */
1026 int prev = next_message(focus->pinned);
1028 /* At the top message already,
1029 * just scroll to the top line. */
1031 focus->scroll = -lines + 1;
1035 /* Scroll back to the previous message */
1036 focus->pinned = prev;
1037 focus->scroll += lines;
1040 /* Scroll down to next messages */
1041 while (focus->scroll > 0) {
1042 /* Find the next message */
1043 int next = prev_message(focus->pinned);
1045 /* At the bottom already,
1046 * remove pin and scroll to last line */
1053 /* Scroll to the next message */
1054 int lines = get_lines(next);
1055 focus->pinned = next;
1056 focus->scroll -= lines;
1059 /* Find pinned message */
1060 int log = history-1;
1063 if (focus->pinned >= 0) {
1064 log = focus->pinned;
1065 skip = focus->scroll;
1071 .end = LINES-cmd_lines-sts_lines,
1077 pr.row = pr.top + skip;
1078 while (pr.row < pr.end && log >= 0)
1079 print_message(&pr, &messages[log--]);
1081 /* Compute skip lines */
1082 skip = pr.end - pr.row;
1087 pr.row = pr.top + skip;
1088 while (pr.row < pr.end && log < history)
1089 print_message(&pr, &messages[log++]);
1092 static void draw_menu(void)
1094 int top = hdr_lines;
1095 int end = LINES-cmd_lines-sts_lines;
1096 int num = end - top;
1098 /* Scroll up / down */
1099 int select = top + (item_idx - item_scroll);
1100 int scroll = select < top ? select - top :
1101 select >= end ? select - (end-1) : 0;
1102 item_scroll += scroll;
1105 /* Compute column size */
1107 for (int i = 0; i < num_items(); i++)
1108 width0 = MAX(width0, strlen(get_item(i)->value));
1109 width0 = MIN(width0, 32);
1110 int width1 = COLS - (3+width0+3);
1113 for (int i = 0; i < num; i++) {
1115 item_t *item = get_item(i + item_scroll);
1116 if (item && line == select) {
1118 mvprintw(line, 0, " > %-*.*s | %-*.*s",
1119 width0, width0, item->value,
1120 width1, width1, item->desc);
1123 mvprintw(line, 0, " %-*.*s | %-*.*s",
1124 width0, width0, item->value,
1125 width1, width1, item->desc);
1127 mvhline(line, 0, ' ', COLS);
1133 static void draw_status(void)
1138 sts_lines = pr.row + 1;
1141 attron(color_status);
1143 pr.row = LINES-cmd_lines-sts_lines;
1144 pr.top = LINES-cmd_lines-sts_lines;
1145 pr.end = LINES-cmd_lines;
1148 attroff(color_status);
1151 static void draw_cmdline(void)
1153 const char *name = focus->dest->name;
1155 .indent = 1 + strlen(name) + 2,
1159 /* Terminate buffer */
1160 cmd_buf[cmd_len] = '\0';
1164 print_string(&pr, cmd_buf);
1165 cmd_lines = pr.row + 1;
1170 pr.row = LINES-cmd_lines;
1171 pr.top = LINES-cmd_lines;
1176 mvprintw(pr.top, 0, "[%s]", name);
1179 print_string(&pr, cmd_buf);
1184 /* Command line editing */
1185 static void insert(const char *buf, int len)
1187 if (cmd_len+len+1 < sizeof(cmd_buf)) {
1188 memmove(&cmd_buf[cmd_pos+len],
1191 memcpy(&cmd_buf[cmd_pos], buf, len);
1195 debug("form: out of space");
1199 static void insert_mb(wint_t chr)
1201 char buf[MB_CUR_MAX];
1202 int n = wctomb(buf, chr);
1206 static void delete(int before, int after)
1208 if (before > 0 && (cmd_pos-before) >= 0) {
1209 memmove(&cmd_buf[cmd_pos-before],
1211 (cmd_len-cmd_pos)+before);
1215 if (after > 0 && (cmd_pos+after) <= cmd_len) {
1216 memmove(&cmd_buf[cmd_pos],
1217 &cmd_buf[cmd_pos+after],
1218 (cmd_len-cmd_pos)+after);
1223 /* Tab completion */
1224 static void show_completion(void)
1226 debug("complete: show");
1228 /* Apply completion */
1229 const char *pick = get_item(item_idx)->value;
1230 const char *save = &save_buf[save_pos];
1233 insert(pick, strlen(pick));
1234 insert(save, save_len-save_pos);
1235 cmd_pos -= save_len-save_pos;
1238 static void start_completion(void)
1240 debug("complete: start");
1242 /* Reset item buffers */
1248 /* Run completion */
1249 char tmp = cmd_buf[cmd_pos];
1250 cmd_buf[cmd_pos] = '\0';
1251 send_complete(cmd_buf);
1252 cmd_buf[cmd_pos] = tmp;
1257 /* Search for matches */
1258 int num = num_items();
1262 memcpy(save_buf, cmd_buf, cmd_len);
1268 /* Switch to menu */
1270 debug("complete: one");
1271 if (cmd_buf[cmd_pos-1] != ' ')
1273 } else if (num > 1) {
1274 debug("complete: menu");
1279 /* Input handling */
1280 static void input_chat(wint_t chr)
1282 /* Window management */
1283 if (chr == KEY_RESIZE) {
1286 else if (chr == KEY_CTRL_L) {
1289 else if (chr == KEY_CTRL_G) {
1293 /* View management */
1294 else if (chr == KEY_CTRL_N) {
1295 focus = next_window(focus) ?: focus;
1297 else if (chr == KEY_CTRL_P) {
1298 focus = prev_window(focus) ?: focus;
1300 else if (chr == KEY_CTRL_X) {
1301 focus->dest = cycle_channel(focus);
1303 else if (chr == KEY_CTRL_Y) {
1306 else if (chr == KEY_CTRL_E) {
1309 else if (chr == KEY_CTRL_U) {
1310 focus->scroll -= (LINES-3)/2;
1312 else if (chr == KEY_CTRL_D) {
1313 focus->scroll += (LINES-3)/2;
1315 else if (chr == KEY_PPAGE) {
1316 focus->scroll -= (LINES-3)-1;
1318 else if (chr == KEY_NPAGE) {
1319 focus->scroll += (LINES-3)-1;
1321 else if (chr == KEY_CTRL_F) {
1324 else if (chr == KEY_CTRL_T) {
1329 else if (chr == KEY_RETURN) {
1330 cmd_buf[cmd_len] = '\0';
1331 if (send_command(cmd_buf)) {
1336 else if (chr == KEY_TAB) {
1338 focus->dest = last_channel(focus);
1342 else if (chr == KEY_ESCAPE) {
1346 else if (chr == KEY_LEFT) {
1350 else if (chr == KEY_RIGHT) {
1351 if (cmd_pos < cmd_len)
1354 else if (chr == KEY_UP) {
1357 else if (chr == KEY_DOWN) {
1360 else if (chr == KEY_HOME) {
1363 else if (chr == KEY_END) {
1366 else if (chr == KEY_BACKSPACE ||
1367 chr == KEY_DELETE) {
1370 else if (chr == KEY_DC) {
1373 else if (iswprint(chr)) {
1377 /* Unknown control character */
1379 debug("main: Unhandled key - Dec %3d, Hex %02x, Oct %03o, Chr <%c>",
1380 chr, chr, chr, chr);
1384 static void input_menu(wint_t chr)
1386 if (chr == KEY_CTRL_N ||
1388 if (item_idx+1 < num_items())
1392 else if (chr == KEY_CTRL_P ||
1398 else if (chr == KEY_TAB) {
1401 else if (chr == KEY_ESCAPE) {
1402 debug("complete: escape");
1403 memcpy(cmd_buf, save_buf, save_len);
1409 debug("complete: accept");
1416 void view_init(void)
1418 /* System windows */
1419 sys_srv.name = strcopy("system");
1420 sys_srv.protocol = -1;
1422 sys_chan.name = strcopy("system");
1423 sys_chan.server = &sys_srv;
1425 sys_win.name = strcopy("system");
1426 sys_win.dest = &sys_chan;
1427 sys_win.channel = &sys_chan;
1428 sys_win.pinned = -1;
1429 sys_win.next = windows;
1431 windows = focus = &sys_win;
1433 /* Print welcome message */
1434 notice("Welcome to lamechat!");
1441 /* Set default escape timeout */
1442 if (!getenv("ESCDELAY"))
1443 putenv("ESCDELAY=25");
1446 setlocale(LC_ALL, "en_US.utf8");
1450 keypad(stdscr, TRUE);
1453 use_default_colors();
1455 color_title = color(COLOR_WHITE, COLOR_BLUE);
1456 color_status = color(COLOR_WHITE, COLOR_BLUE);
1457 color_date = color(COLOR_BROWN, -1);
1458 color_spell = color(-1, COLOR_RED);
1460 /* Create signal FD */
1463 sigaddset(&mask, SIGWINCH);
1464 if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0)
1465 error("creating signal fd");
1467 /* Register callback */
1468 poll_add(&poll_in, 0, (cb_t)view_sync, NULL);
1469 poll_ctl(&poll_in, 1, 0, 1);
1471 poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL);
1472 poll_ctl(&poll_sig, 1, 0, 1);
1477 /* Draw initial view */
1483 void view_config(const char *group, const char *name, const char *key, const char *value)
1488 if (match(group, "general")) {
1489 if (match(key, "theme"))
1490 theme = get_mapv(value, themes);
1491 else if (match(key, "abbrev"))
1492 abbrev = get_mapv(value, abbrevs);
1493 else if (match(key, "defocus"))
1494 defocus = get_number(value);
1497 if (match(group, "window")) {
1498 win = find_window(name);
1499 if (match(key, "")) {
1500 win = add_window(&sys_chan);
1502 strset(&win->name, get_name(name));
1504 else if (match(key, "server")) {
1505 win->server = find_server(get_string(value));
1507 else if (match(key, "channel")) {
1508 win->channel = find_channel(get_string(value));
1509 win->dest = win->channel ?: win->dest;
1511 else if (match(key, "filter")) {
1512 reg_set(&win->filter, get_string(value), 0);
1516 if (match(group, "watch")) {
1517 watch = find_watch(name);
1518 if (match(key, "")) {
1519 watch = add_watch(get_name(name));
1521 else if (match(key, "regex")) {
1522 reg_set(&watch->regex, get_string(value), MATCHES);
1524 else if (match(key, "color")) {
1525 watch->color = get_number(value);
1530 void view_message(const char *prefix, const char *msg)
1532 if (match(prefix, "notice"))
1533 chat_recv(&sys_chan, NULL, msg);
1537 void view_sync(void)
1540 while (get_wch(&wch) != ERR) {
1541 if (view == VIEW_CHAT)
1543 else if (view == VIEW_MENU)
1545 deadline = time(NULL) + defocus;
1549 if (defocus && time(NULL) > deadline &&
1550 focus->dest != &sys_chan) {
1551 focus->saved = focus->dest;
1552 focus->dest = &sys_chan;
1557 debug("view: flush draw");
1564 if (view == VIEW_CHAT)
1566 else if (view == VIEW_MENU)
1569 move(cmd_row, cmd_col);
1577 void view_draw(void)
1579 debug("view: queue draw");
1583 void view_exit(void)
1591 void complete_item(const char *prefix, const char *value, const char *desc)
1593 add_item(prefix, value, desc ?: "");
1596 void complete_user(const char *prefix)
1598 for (user_t *cur = users; cur; cur = cur->next) {
1599 user_t *alias = cur;
1600 while (alias->alias)
1601 alias = alias->alias;
1602 if (starts(prefix, cur->name) ||
1603 starts(prefix, alias->name))
1604 add_item(prefix, alias->name, cur->name);
1608 void complete_channel(const char *prefix)
1610 for (channel_t *cur = channels; cur; cur = cur->next)
1611 if (starts(prefix, cur->name))
1612 add_item(prefix, cur->name, cur->topic);
1615 void complete_server(const char *prefix)
1617 for (server_t *cur = servers; cur; cur = cur->next)
1618 if (starts(prefix, cur->name))
1619 add_item(prefix, cur->name, NULL);
1622 void complete_watch(const char *prefix)
1624 for (watch_t *cur = watches; cur; cur = cur->next) {
1625 if (starts(prefix, cur->name)) {
1626 snprintf(cur->desc, sizeof(cur->desc),
1630 cur->regex.pattern);
1631 add_item(prefix, cur->desc, NULL);
1636 void complete_array(const char *prefix, const char *list[][2], int n)
1638 for (int i = 0; i < n; i++)
1639 if (starts(prefix, list[i][0]))
1640 add_item(prefix, list[i][0], list[i][1]);
1643 void complete_args(const char *prefix, ...)
1645 const char *value, *desc;
1647 va_start(ap, prefix);
1649 value = va_arg(ap, const char *);
1652 desc = va_arg(ap, const char *);
1653 if (!starts(prefix, value))
1655 add_item(prefix, value, desc);