]> Pileus Git - ~andy/lamechat/blob - view.c
Make logfile optional.
[~andy/lamechat] / view.c
1 /*
2  * Copyright (C) 2012-2013 Andy Spencer <andy753421@gmail.com>
3  *
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.
8  *
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.
13  *
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/>.
16  */
17
18 #define _XOPEN_SOURCE
19 #define _XOPEN_SOURCE_EXTENDED
20
21 #include <stdlib.h>
22 #include <string.h>
23 #include <time.h>
24 #include <ctype.h>
25 #include <locale.h>
26 #include <signal.h>
27 #include <sys/signalfd.h>
28 #include <unistd.h>
29
30 #include <ncursesw/ncurses.h>
31
32 #include "util.h"
33 #include "conf.h"
34 #include "chat.h"
35 #include "view.h"
36
37 /* View constants */
38 #define MATCHES    16
39
40 /* Extra colors */
41 #define COLOR_BROWN 130
42
43 /* Extra keys */
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'
55 #define KEY_TAB    '\11'
56 #define KEY_RETURN '\12'
57 #define KEY_ESCAPE '\33'
58 #define KEY_DELETE '\177'
59
60 /* View types */
61 typedef struct window_t window_t;
62 typedef struct watch_t  watch_t;
63
64 typedef struct window_t {
65         char      *name;
66         int        hide;
67         int        flag;
68         int        scroll;
69         int        pinned;
70         int        custom;
71
72         reg_t      filter;
73         server_t  *server;
74         channel_t *channel;
75
76         channel_t *dest;
77         channel_t *saved;
78         window_t  *next;
79 } window_t;
80
81 typedef struct watch_t {
82         char    *name;
83         char     desc[512];
84         reg_t    regex;
85         short    color;
86         watch_t *next;
87 } watch_t;
88
89 typedef struct {
90         const char *value;
91         const char *desc;
92 } item_t;
93
94 typedef struct {
95         int top;
96         int end;
97         int row;
98         int col;
99         int indent;
100         int margin;
101         int limit;
102         int print;
103         int spell;
104         int pick;
105         int prow;
106         int pcol;
107 } printer_t;
108
109 typedef enum {
110         VIEW_CHAT,
111         VIEW_MENU,
112 } view_t;
113
114 typedef enum {
115         THEME_NORMAL,
116         THEME_DARK,
117         THEME_LIGHT,
118 } theme_t;
119
120 typedef enum {
121         ABBREV_NONE,
122         ABBREV_FIRST,
123         ABBREV_LAST,
124         ABBREV_FLAST,
125         ABBREV_INITIALS,
126 } abbrev_t;
127
128 /* Local data */
129 static poll_t poll_in;
130 static poll_t poll_sig;
131 static int    sig_fd;
132 static int    running;
133 static int    defocus;
134 static int    deadline;
135 static int    theme;
136 static int    abbrev;
137 static int    full;
138 static int    date;
139 static int    draw;
140 static int    view;
141
142 static char   cmd_buf[4096];
143 static int    cmd_pos;
144 static int    cmd_len;
145 static int    cmd_row;
146 static int    cmd_col;
147
148 static buf_t  item_buf;
149 static int    item_idx;
150 static int    item_pos;
151 static int    item_scroll;
152 static char   save_buf[4096];
153 static int    save_pos;
154 static int    save_len;
155
156 static int    hdr_lines;
157 static int    sts_lines;
158 static int    cmd_lines;
159
160 static server_t   sys_srv;
161 static channel_t  sys_chan;
162 static window_t   sys_win;
163
164 static window_t  *focus;
165 static window_t  *windows;
166
167 static watch_t   *watches;
168
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;
175
176 static const char *themes[][2] = {
177         [THEME_NORMAL]    { "normal",   "Default theme"    },
178         [THEME_DARK]      { "dark",     "Dark background"  },
179         [THEME_LIGHT]     { "light",    "Light background" },
180 };
181
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"           },
188 };
189
190 /* Helper functions */
191 static short color(int fg, int bg)
192 {
193         if (!colors[fg+1][bg+1]) {
194                 init_pair(ncolors, fg, bg);
195                 colors[fg+1][bg+1] = ncolors;
196                 ncolors++;
197         }
198         return COLOR_PAIR(colors[fg+1][bg+1]);
199 }
200
201 static void abbreviate(char **dst, const char *name)
202 {
203         char first[32];
204         char last[32];
205         char flast[32];
206         char initials[32];
207
208         int max = 32, fi = 0, fli = 0, li = 0, ii = 0;
209         int word = 0, space = 1;
210
211         if (!name)
212                 return;
213
214         for (int i = 0; name[i]; i++) {
215                 if (name[i] == ' ' || name[i] == '-') {
216                         space = 1;
217                         word++;
218                         continue;
219                 }
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]);
228                 if (space)
229                         space = 0;
230         }
231         first[fi] = '\0';
232         last[li] = '\0';
233         flast[fli] = '\0';
234         initials[ii] = '\0';
235
236         switch (abbrev) {
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;
242         }
243 }
244
245 static int msg_compare(const void *_a, const void *_b)
246 {
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;
251 }
252
253 static unsigned str_hash(const char *str)
254 {
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;
260         return hash;
261 }
262
263 static int rgb_color(int r, int g, int b)
264 {
265         return MIN(r*6/0x100, 5)*6*6 +
266                MIN(g*6/0x100, 5)*6   +
267                MIN(b*6/0x100, 5)     + 16;
268 }
269
270 static int str_color(const char *str)
271 {
272         int h,s,l, t,c,x,m, r,g,b;
273
274         unsigned int n = str_hash(str);
275
276         h = (n /     0x1) % 0x100;            // 0..256
277         s = (n /   0x100) % 0x100;            // 0..256
278         l = (n / 0x10000) % 0x100;            // 0..256
279
280         h = h * 360 / 0x100;                  // 0..360
281         s = s / 0x4 + 0xC0;                   // 0..256
282
283         switch (theme) {
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;
287         }
288
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
293
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;
300
301         x = rgb_color(r, g, b);
302
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);
307
308         return color(x, -1);
309 }
310
311 /* Window functions */
312 static int in_window(message_t *msg, window_t *win)
313 {
314         if (win->server && win->server != msg->channel->server)
315                 return 0;
316         if (win->channel && win->channel != msg->channel)
317                 return 0;
318         if (!reg_match(&win->filter, msg->channel->name))
319                 return 0;
320         return 1;
321 }
322
323 static window_t *add_window(channel_t *dest)
324 {
325         window_t *win = new0(window_t);
326         win->dest = dest;
327         win->saved = dest;
328         win->pinned = -1;
329         abbreviate(&win->name, dest->name);
330
331         window_t **last = &windows;
332         while (*last)
333                 last = &(*last)->next;
334         *last = win;
335         return win;
336 }
337
338 static window_t *find_window(const char *name)
339 {
340         for (window_t *win = windows; win; win = win->next)
341                 if (match(win->name, name))
342                         return win;
343         return NULL;
344 }
345
346 static window_t *next_window(window_t *win)
347 {
348         for (window_t *cur = win->next; cur; cur = cur->next)
349                 if (!cur->hide)
350                         return cur;
351         return NULL;
352 }
353
354 static window_t *prev_window(window_t *win)
355 {
356         window_t *prev = NULL;
357         for (window_t *cur = windows; cur != win; cur = cur->next)
358                 if (!cur->hide)
359                         prev = cur;
360         return prev;
361 }
362
363 static channel_t *cycle_channel(window_t *win)
364 {
365         /* Clear seen flags */
366         for (int i = 0; i < history; i++)
367                 messages[i].channel->seen = 0;
368
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)
374                         continue;
375                 msg->channel->seen = 1;
376                 if (!in_window(msg, win))
377                         continue;
378                 if (cur)
379                         next = msg->channel;
380                 if (msg->channel == win->dest)
381                         cur = msg->channel;
382                 if (!first)
383                         first = msg->channel;
384         }
385         return next ?: first ?: &sys_chan;
386 }
387
388 static channel_t *last_channel(window_t *win)
389 {
390         if (win->dest  == &sys_chan &&
391             win->saved != &sys_chan)
392                 return win->saved;
393         for (int i = history-1; i>=0; i--)
394                 if (in_window(&messages[i], win))
395                         return messages[i].channel;
396         return win->dest;
397 }
398
399 static void update_windows(void)
400 {
401         static int seen = 0;
402
403         /* Update focus */
404         if (focus->hide) {
405                 focus = next_window(focus) ?:
406                         prev_window(focus) ?: &sys_win;
407                 focus->hide = 0;
408         }
409
410         /* Update names */
411         for (window_t *cur = windows; cur; cur = cur->next)
412                 if (cur->channel && !cur->custom)
413                         abbreviate(&cur->name, cur->channel->name);
414
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;
420                 int        found = 0;
421
422                 /* Skip focused messages */
423                 if (in_window(msg, focus))
424                         continue;
425
426                 /* Flag existing windows */
427                 for (window_t *cur = windows; cur; cur = cur->next) {
428                         if (!in_window(msg, cur))
429                                 continue;
430                         if (cur->channel == chan)
431                                 prime = cur;
432                         if (!cur->hide)
433                                 found = 1;
434                         cur->flag += 1;
435                 }
436
437                 /* No window, create a new one */
438                 if (!found) {
439                         if (prime) {
440                                 prime->hide = 0;
441                                 prime->flag = 1;
442                         } else {
443                                 prime = add_window(chan);
444                                 prime->channel = chan;
445                                 prime->flag = 1;
446                         }
447                 }
448         }
449 }
450
451 static server_t *find_server(const char *name)
452 {
453         for (server_t *cur = servers; cur; cur = cur->next)
454                 if (match(cur->name, name))
455                         return cur;
456         return NULL;
457 }
458
459 static channel_t *find_channel(const char *name)
460 {
461         for (channel_t *cur = channels; cur; cur = cur->next)
462                 if (match(cur->name, name))
463                         return cur;
464         return NULL;
465 }
466
467 /* Watch functions */
468 static watch_t *add_watch(const char *name)
469 {
470         watch_t *watch = new0(watch_t);
471         strset(&watch->name, name);
472
473         watch_t **last = &watches;
474         while (*last)
475                 last = &(*last)->next;
476         *last = watch;
477         return watch;
478 }
479
480 static watch_t *find_watch(const char *name)
481 {
482         for (watch_t *watch = watches; watch; watch = watch->next)
483                 if (match(watch->name, name))
484                         return watch;
485         return NULL;
486 }
487
488 static void set_watch(const char *text)
489 {
490         static char name[64];
491         static char regex[512];
492         static int  color;
493         sscanf(text, "%s %d %[^\n]", name, &color, regex);
494
495         watch_t *watch = find_watch(name);
496         if (!watch)
497                 watch = add_watch(name);
498         reg_set(&watch->regex, regex, MATCHES);
499         watch->color = color;
500 }
501
502 static void watch_run(const char *text)
503 {
504         for (watch_t *watch = watches; watch; watch = watch->next)
505                 reg_match(&watch->regex, text);
506 }
507
508 static void watch_end(void)
509 {
510         for (watch_t *watch = watches; watch; watch = watch->next)
511                 watch->regex.count = 0;
512 }
513
514 static void watch_on(int pos)
515 {
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);
519 }
520
521 static void watch_off(int pos)
522 {
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);
526 }
527
528 /* Print functions */
529 static int word_wrap(printer_t *pr, const char *txt,
530                       const char **l, const char **r)
531 {
532         /* In previous word? */
533         if (txt > *l && txt < *r)
534                 return 0;
535
536         /* Find left and right ends of the word */
537         *l = txt;
538         while (*txt != '\0' &&
539                *txt != ' '  && *txt != '\t' &&
540                *txt != '\r' && *txt != '\n')
541                 txt++;
542         *r = txt;
543
544         /* End goes past the margin? */
545         int len = *r - *l;
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;
551         }
552
553         return 0;
554 }
555
556 static int irc_number(const char *txt, int *i)
557 {
558         int  n = 0;
559         int  r = 0;
560         char a = txt[*i];
561         char b = a ? txt[(*i)+1] : 0;
562         if ('0' == a)
563                 n = ('0' <= b && b <= '9') ? 2 : 1;
564         if ('1' == a)
565                 n = ('0' <= b && b <= '5') ? 2 : 1;
566         if ('2' <= a && a <= '9')
567                 n = 1;
568         if (n == 0)
569                 return -1;
570         for (int j = 0; j < n; j++)
571                 r = r*10 + txt[(*i)++]-'0';
572         return r;
573 }
574
575 static int irc_color(const char *txt, int *i)
576 {
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
594         };
595
596         int attr  = 0;
597         int reset = 0;
598         int fg = -1;
599         int bg = -1;
600
601         switch (txt[*i]) {
602                 // Format attributes
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
609
610                 // Reset
611                 case '\017': reset = 1;          break; // reset
612
613                 // Colors
614                 case '\003':
615                 case '\013':
616                         (*i)++;
617                         fg = irc_number(txt, i);
618                         if (txt[*i] == ',') {
619                                 (*i)++;
620                                 bg = irc_number(txt, i);
621                                 if (bg == -1)
622                                         (*i)--;
623                         }
624                         (*i)--;
625                         break;
626
627                 // Not IRC Colors
628                 default:
629                         return 0;
630         }
631
632         if (reset) {
633                 attrset(0);
634         }
635         else if (attr){
636                 attr_t tmp = 0;
637                 short pair = 0;
638                 attr_get(&tmp, &pair, 0);
639                 attr_set(tmp^attr, pair, 0);
640         }
641         else if (fg != -1 || bg != -1) {
642                 if (0 <= fg && fg <= 15)
643                         fg = rgb_color(color_map[fg][0],
644                                        color_map[fg][1],
645                                        color_map[fg][2]);
646                 if (0 <= bg && bg <= 15)
647                         bg = rgb_color(color_map[bg][0],
648                                        color_map[bg][1],
649                                        color_map[bg][2]);
650                 attron(color(fg, bg));
651         }
652         return 1;
653 }
654
655 static void print_string(printer_t *pr, const char *txt)
656 {
657         int i, n;
658         int li = 0;
659         wchar_t wc;
660         const char *l = 0, *r = 0;
661
662         for (i = 0; txt[i]; i++) {
663                 if (word_wrap(pr, &txt[i], &l, &r)) {
664                         pr->row += 1;
665                         pr->col  = pr->indent;
666                 }
667                 if (i == pr->pick) {
668                         pr->prow = pr->row;
669                         pr->pcol = pr->col;
670                 }
671                 watch_on(i);
672                 if (irc_color(txt, &i))
673                         continue;
674                 if (pr->spell && (i == 0 || txt[i-1] == ' '))
675                         if (spell_check(&txt[i], NULL))
676                                 attron(color_spell);
677                 switch (txt[i]) {
678                         case ' ':
679                                 pr->col += 1;
680                                 break;
681                         case '\t':
682                                 pr->col -= pr->indent;
683                                 pr->col /= 8;
684                                 pr->col += 1;
685                                 pr->col *= 8;
686                                 pr->col += pr->indent;
687                                 break;
688                         case '\r':
689                         case '\n':
690                                 pr->row += 1;
691                                 pr->col  = pr->indent;
692                                 if (pr->limit && li++ > pr->limit)
693                                         txt = "\n\011[continued]\017" - i;
694                                 break;
695                         default:
696                                 if ((n = mbtowc(&wc, &txt[i], 1)) == -1)
697                                         break;
698                                 i += n-1;
699                                 if (pr->print &&
700                                     pr->row >= pr->top &&
701                                     pr->row <  pr->end)
702                                         mvaddnwstr(pr->row, pr->col, &wc, 1);
703                                 pr->col += 1;
704                                 break;
705                 }
706                 if (pr->spell && (txt[i] == ' ' || !txt[i+1]))
707                         attroff(color_spell);
708                 watch_off(i);
709         }
710         if (i <= pr->pick) {
711                 pr->prow = pr->row;
712                 pr->pcol = pr->col;
713         }
714 }
715
716 static void print_format(printer_t *pr, const char *fmt, ...)
717 {
718         static char buf[4096];
719         va_list ap;
720         va_start(ap, fmt);
721         vsnprintf(buf, sizeof(buf), fmt, ap);
722         va_end(ap);
723
724         print_string(pr, buf);
725 }
726
727 static void print_message(printer_t *pr, message_t *msg)
728 {
729         if (!in_window(msg, focus))
730                 return;
731
732         time_t timep = msg->when;
733         struct tm *tm = localtime(&timep);
734
735         /* Set line limit */
736         pr->limit = full ? 0 : 5;
737
738         /* Start new line */
739         pr->col = 0;
740         pr->indent = 0;
741
742         /* Print time */
743         attron(color_date);
744         if (date)
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);
748         else
749                 print_format(pr, "%02d:%02d ", tm->tm_hour, tm->tm_min);
750         attroff(color_date);
751
752         /* Print channel */
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);
757         }
758
759         /* Print from */
760         if (msg->from) {
761                 attron(str_color(msg->from->name));
762                 print_format(pr, "%s: ", msg->from->name);
763                 attroff(str_color(msg->from->name));
764         } else {
765                 print_format(pr, "*** ");
766         }
767
768         /* Set matches */
769         watch_run(msg->text);
770
771         /* Print message */
772         pr->indent = pr->col;
773         print_string(pr, msg->text);
774         pr->row++;
775
776         /* Clear matches */
777         watch_end();
778 }
779
780 static void print_status(printer_t *pr)
781 {
782         pr->col = 0;
783         print_format(pr, " Windows: ");
784         pr->indent = pr->col;
785         pr->margin = 1;
786         for (window_t *cur = windows; cur; cur = cur->next) {
787                 if (cur->hide)
788                         continue;
789                 if (cur == focus)
790                         cur->flag = 0;
791                 if (cur == focus)
792                         attron(A_BOLD);
793
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);
798                 else if (cur->flag)
799                         print_format(pr, "%s:%d", cur->name, cur->flag);
800                 else
801                         print_format(pr, "%s", cur->name);
802
803                 if (cur == focus)
804                         attroff(A_BOLD);
805                 pr->col++;
806         }
807 }
808
809 static void print_clear(printer_t *pr)
810 {
811         for (int i = pr->top; i < pr->end; i++)
812                 mvhline(i, 0, ' ', COLS);
813 }
814
815 /* Message info */
816 static int get_lines(int pos)
817 {
818         printer_t pr = {.print=0};
819         print_message(&pr, &messages[pos]);
820         return pr.row;
821 }
822
823 static int next_message(int pos)
824 {
825         while (1) {
826                 if (--pos < 0)
827                         return -1;
828                 if (in_window(&messages[pos], focus))
829                         return pos;
830         }
831 }
832
833 static int prev_message(int pos)
834 {
835         while (1) {
836                 if (++pos >= history)
837                         return -1;
838                 if (in_window(&messages[pos], focus))
839                         return pos;
840         }
841 }
842
843 /* Menu items */
844 static void add_item(const char *prefix, const char *value, const char *desc)
845 {
846         item_t item = {
847                 .value = value,
848                 .desc  = desc,
849         };
850         append(&item_buf, (void*)&item, sizeof(item));
851         if (prefix)
852                 item_pos = prefix - cmd_buf;
853 }
854
855 static int num_items(void)
856 {
857         return item_buf.len / sizeof(item_t);
858 }
859
860 static item_t *get_item(int i)
861 {
862         if (i < 0)
863                 return NULL;
864         if (i >= item_buf.len/sizeof(item_t))
865                 return NULL;
866         return (item_t*)&item_buf.data[i*sizeof(item_t)];
867 }
868
869 static int compare_items(const void *_a, const void *_b)
870 {
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);
875 }
876
877 static void sort_items(void)
878 {
879         if (!item_buf.len)
880                 return;
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))
886                         *++dst = *src;
887         item_buf.len = (char*)(dst+1) - item_buf.data;
888 }
889
890 /* Commands */
891 static void send_complete(const char *text)
892 {
893         const char *arg;
894         if (prefix(text, "/theme ", &arg)) {
895                 complete_array(arg, themes, N_ELEMENTS(themes));
896         }
897         else if (prefix(text, "/abbrev ", &arg)) {
898                 complete_array(arg, abbrevs, N_ELEMENTS(abbrevs));
899         }
900         else if (prefix(text, "/server ", &arg)) {
901                 complete_server(arg);
902         }
903         else if (prefix(text, "/channel ", &arg)) {
904                 complete_channel(arg);
905         }
906         else if (prefix(text, "/watch ", &arg)) {
907                 complete_watch(arg);
908         }
909         else {
910                 complete_args(text,
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",
922                               NULL);
923         }
924
925         chat_complete(last_channel(focus), text);
926 }
927
928 static int send_command(const char *text)
929 {
930         const char *arg;
931         window_t   *win = focus;
932
933         if (match(text, "/quit")) {
934                 poll_quit();
935         }
936         else if (match(text, "/sort")) {
937                 qsort(messages, history, sizeof(message_t), msg_compare);
938         }
939         else if (prefix(text, "/theme", &arg)) {
940                 for (int i = 0; i < N_ELEMENTS(themes); i++)
941                         if (match(themes[i][0], arg))
942                                 theme = i;
943         }
944         else if (prefix(text, "/abbrev", &arg)) {
945                 for (int i = 0; i < N_ELEMENTS(abbrevs); i++)
946                         if (match(abbrevs[i][0], arg))
947                                 abbrev = i;
948         }
949         else if (prefix(text, "/watch", &arg) && arg) {
950                 set_watch(arg);
951         }
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;
957                 }
958                 focus = win;
959                 focus->hide = 0;
960         }
961         else if (match(text, "/close")) {
962                 focus->hide = 1;
963         }
964         else if (prefix(text, "/name", &arg) && arg) {
965                 focus->custom = 1;
966                 if (match(focus->name, arg) &&
967                     focus->dest &&
968                     focus->dest != &sys_chan)
969                         strset(&focus->dest->name, arg);
970                 else
971                         strset(&focus->name, arg);
972         }
973         else if (prefix(text, "/server", &arg)) {
974                 win->server = find_server(arg);
975         }
976         else if (prefix(text, "/channel", &arg)) {
977                 win->channel = find_channel(arg);
978                 win->dest    = win->channel ?: win->dest;
979         }
980         else if (prefix(text, "/filter", &arg)) {
981                 reg_set(&focus->filter, arg, 0);
982         }
983         else if (focus->dest != &sys_chan) {
984                 chat_send(focus->dest, text);
985         }
986         else {
987                 return 0;
988         }
989         return 1;
990 }
991
992 /* Drawing functions */
993 static void draw_header(void)
994 {
995         printer_t pr = {
996                 .print  = 1,
997                 .row    = 0,
998                 .col    = 1,
999                 .indent = 1,
1000                 .margin = 1,
1001                 .end    = 3,
1002         };
1003         const char *topic = focus->dest->topic ?: "No Topic";
1004         attron(color_title);
1005         print_clear(&pr);
1006         print_format(&pr, "%s", topic);
1007         attroff(color_title);
1008         hdr_lines = pr.row + 1;
1009 }
1010
1011 static void draw_chat(void)
1012 {
1013         /* Clear pinned if scrolling */
1014         if (focus->scroll && focus->pinned < 0)
1015                 focus->pinned = history-1;
1016
1017         /* Scroll up to previous messages */
1018         while (focus->scroll < 0) {
1019                 int lines = get_lines(focus->pinned);
1020
1021                 /* Scrolling to a warpped line in this message */
1022                 if (lines + focus->scroll > 0)
1023                         break;
1024
1025                 /* Find the previous message */
1026                 int prev = next_message(focus->pinned);
1027
1028                 /* At the top message already,
1029                  * just scroll to the top line. */
1030                 if (prev < 0) {
1031                         focus->scroll = -lines + 1;
1032                         break;
1033                 }
1034
1035                 /* Scroll back to the previous message */
1036                 focus->pinned  = prev;
1037                 focus->scroll += lines;
1038         }
1039
1040         /* Scroll down to next messages */
1041         while (focus->scroll > 0) {
1042                 /* Find the next message */
1043                 int next = prev_message(focus->pinned);
1044
1045                 /* At the bottom already,
1046                  * remove pin and scroll to last line */
1047                 if (next < 0) {
1048                         focus->pinned = -1;
1049                         focus->scroll = 0;
1050                         break;
1051                 }
1052
1053                 /* Scroll to the next message */
1054                 int lines = get_lines(next);
1055                 focus->pinned  = next;
1056                 focus->scroll -= lines;
1057         }
1058
1059         /* Find pinned message */
1060         int log  = history-1;
1061         int skip = 0;
1062
1063         if (focus->pinned >= 0) {
1064                 log = focus->pinned;
1065                 skip = focus->scroll;
1066         }
1067
1068         /* Setup printer */
1069         printer_t pr = {
1070                 .top = hdr_lines,
1071                 .end = LINES-cmd_lines-sts_lines,
1072         };
1073         print_clear(&pr);
1074
1075         /* Compute lines */
1076         pr.print = 0;
1077         pr.row = pr.top + skip;
1078         while (pr.row < pr.end && log >= 0)
1079                 print_message(&pr, &messages[log--]);
1080
1081         /* Compute skip lines */
1082         skip = pr.end - pr.row;
1083         log = log + 1;
1084
1085         /* Print lines */
1086         pr.print = 1;
1087         pr.row = pr.top + skip;
1088         while (pr.row < pr.end && log < history)
1089                 print_message(&pr, &messages[log++]);
1090 }
1091
1092 static void draw_menu(void)
1093 {
1094         int top = hdr_lines;
1095         int end = LINES-cmd_lines-sts_lines;
1096         int num = end - top;
1097
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;
1103         select      -= scroll;
1104
1105         /* Compute column size */
1106         int width0 = 0;
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);
1111
1112         /* Draw lines */
1113         for (int i = 0; i < num; i++) {
1114                 int     line   = i + top;
1115                 item_t *item   = get_item(i + item_scroll);
1116                 if (item && line == select) {
1117                         attron(A_REVERSE);
1118                         mvprintw(line, 0, " > %-*.*s | %-*.*s",
1119                                         width0, width0, item->value,
1120                                         width1, width1, item->desc);
1121                         attroff(A_REVERSE);
1122                 } else if (item) {
1123                         mvprintw(line, 0, "   %-*.*s | %-*.*s",
1124                                         width0, width0, item->value,
1125                                         width1, width1, item->desc);
1126                 } else {
1127                         mvhline(line, 0, ' ', COLS);
1128                 }
1129         }
1130
1131 }
1132
1133 static void draw_status(void)
1134 {
1135         /* Compute lines */
1136         printer_t pr = {};
1137         print_status(&pr);
1138         sts_lines = pr.row + 1;
1139
1140         /* Draw status */
1141         attron(color_status);
1142         pr.print = 1;
1143         pr.row = LINES-cmd_lines-sts_lines;
1144         pr.top = LINES-cmd_lines-sts_lines;
1145         pr.end = LINES-cmd_lines;
1146         print_clear(&pr);
1147         print_status(&pr);
1148         attroff(color_status);
1149 }
1150
1151 static void draw_cmdline(void)
1152 {
1153         const char *name = focus->dest->name;
1154         printer_t pr = {
1155                 .indent = 1 + strlen(name) + 2,
1156                 .margin = 1,
1157         };
1158
1159         /* Terminate buffer */
1160         cmd_buf[cmd_len] = '\0';
1161
1162         /* Compute lines */
1163         pr.col = pr.indent;
1164         print_string(&pr, cmd_buf);
1165         cmd_lines = pr.row + 1;
1166
1167         /* Clear screen */
1168         pr.print = 1;
1169         pr.col = pr.indent;
1170         pr.row = LINES-cmd_lines;
1171         pr.top = LINES-cmd_lines;
1172         pr.end = LINES;
1173         print_clear(&pr);
1174
1175         /* Print cmdline */
1176         mvprintw(pr.top, 0, "[%s]", name);
1177         pr.spell = 1;
1178         pr.pick  = cmd_pos;
1179         print_string(&pr, cmd_buf);
1180         cmd_row = pr.prow;
1181         cmd_col = pr.pcol;
1182 }
1183
1184 /* Command line editing */
1185 static void insert(const char *buf, int len)
1186 {
1187         if (cmd_len+len+1 < sizeof(cmd_buf)) {
1188                 memmove(&cmd_buf[cmd_pos+len],
1189                         &cmd_buf[cmd_pos],
1190                         (cmd_len-cmd_pos));
1191                 memcpy(&cmd_buf[cmd_pos], buf, len);
1192                 cmd_pos += len;
1193                 cmd_len += len;
1194         } else {
1195                 debug("form: out of space");
1196         }
1197 }
1198
1199 static void insert_mb(wint_t chr)
1200 {
1201         char buf[MB_CUR_MAX];
1202         int n = wctomb(buf, chr);
1203         insert(buf, n);
1204 }
1205
1206 static void delete(int before, int after)
1207 {
1208         if (before > 0 && (cmd_pos-before) >= 0) {
1209                 memmove(&cmd_buf[cmd_pos-before],
1210                         &cmd_buf[cmd_pos],
1211                         (cmd_len-cmd_pos)+before);
1212                 cmd_len -= before;
1213                 cmd_pos -= before;
1214         }
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);
1219                 cmd_len -= after;
1220         }
1221 }
1222
1223 /* Tab completion */
1224 static void show_completion(void)
1225 {
1226         debug("complete: show");
1227
1228         /* Apply completion */
1229         const char *pick = get_item(item_idx)->value;
1230         const char *save = &save_buf[save_pos];
1231         cmd_pos = item_pos;
1232         cmd_len = item_pos;
1233         insert(pick, strlen(pick));
1234         insert(save, save_len-save_pos);
1235         cmd_pos -= save_len-save_pos;
1236 }
1237
1238 static void start_completion(void)
1239 {
1240         debug("complete: start");
1241
1242         /* Reset item buffers */
1243         reset(&item_buf);
1244         item_idx = 0;
1245         item_pos = cmd_pos;
1246         item_scroll = 0;
1247
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;
1253
1254         /* Sort matches */
1255         sort_items();
1256
1257         /* Search for matches */
1258         int num = num_items();
1259         if (num == 0) {
1260                 beep();
1261         } else {
1262                 memcpy(save_buf, cmd_buf, cmd_len);
1263                 save_pos = cmd_pos;
1264                 save_len = cmd_len;
1265                 show_completion();
1266         }
1267
1268         /* Switch to menu */
1269         if (num == 1) {
1270                 debug("complete: one");
1271                 if (cmd_buf[cmd_pos-1] != ' ')
1272                         insert(" ", 1);
1273         } else if (num > 1) {
1274                 debug("complete: menu");
1275                 view = VIEW_MENU;
1276         }
1277 }
1278
1279 /* Input handling */
1280 static void input_chat(wint_t chr)
1281 {
1282         /* Window management */
1283         if (chr == KEY_RESIZE) {
1284                 clear();
1285         }
1286         else if (chr == KEY_CTRL_L) {
1287                 clear();
1288         }
1289         else if (chr == KEY_CTRL_G) {
1290                 view_draw();
1291         }
1292
1293         /* View management */
1294         else if (chr == KEY_CTRL_N) {
1295                 focus = next_window(focus) ?: focus;
1296         }
1297         else if (chr == KEY_CTRL_P) {
1298                 focus = prev_window(focus) ?: focus;
1299         }
1300         else if (chr == KEY_CTRL_X) {
1301                 focus->dest = cycle_channel(focus);
1302         }
1303         else if (chr == KEY_CTRL_Y) {
1304                 focus->scroll -= 1;
1305         }
1306         else if (chr == KEY_CTRL_E) {
1307                 focus->scroll += 1;
1308         }
1309         else if (chr == KEY_CTRL_U) {
1310                 focus->scroll -= (LINES-3)/2;
1311         }
1312         else if (chr == KEY_CTRL_D) {
1313                 focus->scroll += (LINES-3)/2;
1314         }
1315         else if (chr == KEY_PPAGE) {
1316                 focus->scroll -= (LINES-3)-1;
1317         }
1318         else if (chr == KEY_NPAGE) {
1319                 focus->scroll += (LINES-3)-1;
1320         }
1321         else if (chr == KEY_CTRL_F) {
1322                 full ^= 1;
1323         }
1324         else if (chr == KEY_CTRL_T) {
1325                 date ^= 1;
1326         }
1327
1328         /* Cmdline Input */
1329         else if (chr == KEY_RETURN) {
1330                 cmd_buf[cmd_len] = '\0';
1331                 if (send_command(cmd_buf)) {
1332                         cmd_pos = 0;
1333                         cmd_len = 0;
1334                 }
1335         }
1336         else if (chr == KEY_TAB) {
1337                 if (cmd_pos == 0)
1338                         focus->dest = last_channel(focus);
1339                 else
1340                         start_completion();
1341         }
1342         else if (chr == KEY_ESCAPE) {
1343                 cmd_pos = 0;
1344                 cmd_len = 0;
1345         }
1346         else if (chr == KEY_LEFT) {
1347                 if (cmd_pos > 0)
1348                         cmd_pos--;
1349         }
1350         else if (chr == KEY_RIGHT) {
1351                 if (cmd_pos < cmd_len)
1352                         cmd_pos++;
1353         }
1354         else if (chr == KEY_UP) {
1355                 /* Todo */
1356         }
1357         else if (chr == KEY_DOWN) {
1358                 /* Todo */
1359         }
1360         else if (chr == KEY_HOME) {
1361                 cmd_pos = 0;
1362         }
1363         else if (chr == KEY_END) {
1364                 cmd_pos = cmd_len;
1365         }
1366         else if (chr == KEY_BACKSPACE ||
1367                  chr == KEY_DELETE) {
1368                 delete(1, 0);
1369         }
1370         else if (chr == KEY_DC) {
1371                 delete(0, 1);
1372         }
1373         else if (iswprint(chr)) {
1374                 insert_mb(chr);
1375         }
1376
1377         /* Unknown control character */
1378         else {
1379                 debug("main: Unhandled key - Dec %3d,  Hex %02x,  Oct %03o,  Chr <%c>",
1380                                 chr, chr, chr, chr);
1381         }
1382 }
1383
1384 static void input_menu(wint_t chr)
1385 {
1386         if (chr == KEY_CTRL_N ||
1387             chr == KEY_DOWN) {
1388                 if (item_idx+1 < num_items())
1389                         item_idx++;
1390                 show_completion();
1391         }
1392         else if (chr == KEY_CTRL_P ||
1393                  chr == KEY_UP) {
1394                 if (item_idx > 0)
1395                         item_idx--;
1396                 show_completion();
1397         }
1398         else if (chr == KEY_TAB) {
1399                 start_completion();
1400         }
1401         else if (chr == KEY_ESCAPE) {
1402                 debug("complete: escape");
1403                 memcpy(cmd_buf, save_buf, save_len);
1404                 cmd_pos = save_pos;
1405                 cmd_len = save_len;
1406                 view    = VIEW_CHAT;
1407         }
1408         else {
1409                 debug("complete: accept");
1410                 view    = VIEW_CHAT;
1411                 input_chat(chr);
1412         }
1413 }
1414
1415 /* View init */
1416 void view_init(void)
1417 {
1418         /* System windows */
1419         sys_srv.name = strcopy("system");
1420         sys_srv.protocol = -1;
1421
1422         sys_chan.name = strcopy("system");
1423         sys_chan.server = &sys_srv;
1424
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;
1430
1431         windows = focus = &sys_win;
1432
1433         /* Print welcome message */
1434         notice("Welcome to lamechat!");
1435
1436         if (sys_win.next) {
1437                 update_windows();
1438                 sys_win.hide = 1;
1439         }
1440
1441         /* Set default escape timeout */
1442         if (!getenv("ESCDELAY"))
1443                 putenv("ESCDELAY=25");
1444
1445         /* Setup Curses */
1446         setlocale(LC_ALL, "en_US.utf8");
1447         initscr();
1448         cbreak();
1449         noecho();
1450         keypad(stdscr, TRUE);
1451         start_color();
1452         timeout(0);
1453         use_default_colors();
1454
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);
1459
1460         /* Create signal FD */
1461         sigset_t mask;
1462         sigemptyset(&mask);
1463         sigaddset(&mask, SIGWINCH);
1464         if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0)
1465                 error("creating signal fd");
1466
1467         /* Register callback */
1468         poll_add(&poll_in, 0, (cb_t)view_sync, NULL);
1469         poll_ctl(&poll_in, 1, 0, 1);
1470
1471         poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL);
1472         poll_ctl(&poll_sig, 1, 0, 1);
1473
1474         /* Set running */
1475         running = 1;
1476
1477         /* Draw initial view */
1478         view_draw();
1479         view_sync();
1480 }
1481
1482 /* Config parser */
1483 void view_config(const char *group, const char *name, const char *key, const char *value)
1484 {
1485         window_t *win;
1486         watch_t *watch;
1487
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);
1495         }
1496
1497         if (match(group, "window")) {
1498                 win = find_window(name);
1499                 if (match(key, "")) {
1500                         win = add_window(&sys_chan);
1501                         win->custom = 1;
1502                         strset(&win->name, get_name(name));
1503                 }
1504                 else if (match(key, "server")) {
1505                         win->server = find_server(get_string(value));
1506                 }
1507                 else if (match(key, "channel")) {
1508                         win->channel = find_channel(get_string(value));
1509                         win->dest    = win->channel ?: win->dest;
1510                 }
1511                 else if (match(key, "filter")) {
1512                         reg_set(&win->filter, get_string(value), 0);
1513                 }
1514         }
1515
1516         if (match(group, "watch")) {
1517                 watch = find_watch(name);
1518                 if (match(key, "")) {
1519                         watch = add_watch(get_name(name));
1520                 }
1521                 else if (match(key, "regex")) {
1522                         reg_set(&watch->regex, get_string(value), MATCHES);
1523                 }
1524                 else if (match(key, "color")) {
1525                         watch->color = get_number(value);
1526                 }
1527         }
1528 }
1529
1530 void view_message(const char *prefix, const char *msg)
1531 {
1532         if (match(prefix, "notice"))
1533                 chat_recv(&sys_chan, NULL, msg);
1534 }
1535
1536 /* View event */
1537 void view_sync(void)
1538 {
1539         wint_t wch = 0;
1540         while (get_wch(&wch) != ERR) {
1541                 if (view == VIEW_CHAT)
1542                         input_chat(wch);
1543                 else if (view == VIEW_MENU)
1544                         input_menu(wch);
1545                 deadline = time(NULL) + defocus;
1546                 draw = 1;
1547         }
1548
1549         if (defocus && time(NULL) > deadline &&
1550             focus->dest != &sys_chan) {
1551                 focus->saved = focus->dest;
1552                 focus->dest = &sys_chan;
1553                 draw  = 1;
1554         }
1555
1556         if (draw) {
1557                 debug("view: flush draw");
1558
1559                 update_windows();
1560
1561                 draw_cmdline();
1562                 draw_status();
1563                 draw_header();
1564                 if (view == VIEW_CHAT)
1565                         draw_chat();
1566                 else if (view == VIEW_MENU)
1567                         draw_menu();
1568
1569                 move(cmd_row, cmd_col);
1570
1571                 refresh();
1572
1573                 draw = 0;
1574         }
1575 }
1576
1577 void view_draw(void)
1578 {
1579         debug("view: queue draw");
1580         draw = 1;
1581 }
1582
1583 void view_exit(void)
1584 {
1585         if (!running)
1586                 return;
1587         endwin();
1588 }
1589
1590 /* Completion */
1591 void complete_item(const char *prefix, const char *value, const char *desc)
1592 {
1593         add_item(prefix, value, desc ?: "");
1594 }
1595
1596 void complete_user(const char *prefix)
1597 {
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);
1605         }
1606 }
1607
1608 void complete_channel(const char *prefix)
1609 {
1610         for (channel_t *cur = channels; cur; cur = cur->next)
1611                 if (starts(prefix, cur->name))
1612                         add_item(prefix, cur->name, cur->topic);
1613 }
1614
1615 void complete_server(const char *prefix)
1616 {
1617         for (server_t *cur = servers; cur; cur = cur->next)
1618                 if (starts(prefix, cur->name))
1619                         add_item(prefix, cur->name, NULL);
1620 }
1621
1622 void complete_watch(const char *prefix)
1623 {
1624         for (watch_t *cur = watches; cur; cur = cur->next) {
1625                 if (starts(prefix, cur->name)) {
1626                         snprintf(cur->desc, sizeof(cur->desc),
1627                                         "%s %hd %s",
1628                                         cur->name,
1629                                         cur->color,
1630                                         cur->regex.pattern);
1631                         add_item(prefix, cur->desc, NULL);
1632                 }
1633         }
1634 }
1635
1636 void complete_array(const char *prefix, const char *list[][2], int n)
1637 {
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]);
1641 }
1642
1643 void complete_args(const char *prefix, ...)
1644 {
1645         const char *value, *desc;
1646         va_list ap;
1647         va_start(ap, prefix);
1648         while (1) {
1649                 value = va_arg(ap, const char *);
1650                 if (!value)
1651                         break;
1652                 desc = va_arg(ap, const char *);
1653                 if (!starts(prefix, value))
1654                         continue;
1655                 add_item(prefix, value, desc);
1656         }
1657         va_end(ap);
1658 }