]> Pileus Git - ~andy/lamechat/blob - view.c
f8fff32ce9eabd239dcb17c107a369bb73c49c94
[~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 <ncurses.h>
27 #include <signal.h>
28 #include <sys/signalfd.h>
29 #include <unistd.h>
30 #include <regex.h>
31
32 #include "util.h"
33 #include "conf.h"
34 #include "chat.h"
35 #include "view.h"
36
37 /* View constants */
38 #define RE_FLAGS   (REG_EXTENDED|REG_NOSUB)
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_G '\7'
47 #define KEY_CTRL_L '\14'
48 #define KEY_CTRL_N '\16'
49 #define KEY_CTRL_P '\20'
50 #define KEY_CTRL_U '\25'
51 #define KEY_CTRL_X '\30'
52 #define KEY_CTRL_Y '\31'
53 #define KEY_RETURN '\12'
54 #define KEY_ESCAPE '\33'
55
56 /* View types */
57 typedef struct window_t window_t;
58
59 typedef struct window_t {
60         char      *name;
61         int        hide;
62         int        flag;
63         char      *filter;
64         int        scroll;
65         int        pinned;
66         regex_t    regex;
67         server_t  *server;
68         channel_t *channel;
69         window_t  *next;
70 } window_t;
71
72 typedef enum {
73         THEME_NORMAL,
74         THEME_DARK,
75         THEME_LIGHT,
76 } theme_t;
77
78 /* Local data */
79 static poll_t poll_in;
80 static poll_t poll_sig;
81 static int    sig_fd;
82 static int    running;
83 static int    theme;
84 static int    draw;
85
86 static char   cmd_buf[512];
87 static int    cmd_pos;
88 static int    cmd_len;
89
90 static window_t *focus;
91 static window_t *windows;
92
93 static int   ncolors = 1;
94 static short colors[256+1][256+1];
95 static short color_title;
96 static short color_date;
97 static short color_error;
98
99 static const char *themes[] = {
100         [THEME_NORMAL] "normal",
101         [THEME_DARK]   "dark",
102         [THEME_LIGHT]  "light",
103 };
104
105 /* Helper functions */
106 static short color(int fg, int bg)
107 {
108         if (!colors[fg+1][bg+1]) {
109                 init_pair(ncolors, fg, bg);
110                 colors[fg+1][bg+1] = ncolors;
111                 ncolors++;
112         }
113         return COLOR_PAIR(colors[fg+1][bg+1]);
114 }
115
116 static int msg_compare(const void *_a, const void *_b)
117 {
118         const message_t *a = _a;
119         const message_t *b = _b;
120         return a->when < b->when ? -1 :
121                a->when > b->when ?  1 : 0;
122 }
123
124 static unsigned str_hash(const char *str)
125 {
126         unsigned long hash = 0;
127         for (int i = 0; str[i]; i++)
128                 hash = str[i] + (hash<<6) + (hash<<16) - hash;
129         for (int i = 0; str[i]; i++)
130                 hash = str[i] + (hash<<6) + (hash<<16) - hash;
131         return hash;
132 }
133
134 static int str_color(const char *str)
135 {
136         int h,s,l, t,c,x,m, r,g,b;
137
138         unsigned int n = str_hash(str);
139
140         h = (n /     0x1) % 0x100;            // 0..256
141         s = (n /   0x100) % 0x100;            // 0..256
142         l = (n / 0x10000) % 0x100;            // 0..256
143
144         h = h * 360 / 0x100;                  // 0..360
145         s = s / 0x4 + 0xC0;                   // 0..256
146
147         switch (theme) {
148                 case THEME_DARK:   l =  l / 0x4 + 0x80; break;
149                 case THEME_LIGHT:  l =  l / 0x4 + 0x30; break;
150                 case THEME_NORMAL: l =  l / 0x4 + 0x60; break;
151         }
152
153         t = h / 60;                           // ,1,2,3,4,5
154         c = ((0x80-abs(l-0x80))*s)/0x80;      // ..256
155         x = (c*(60-abs((h%120)-60)))/60;      // ..256
156         m = l - c/2;                          // ..128
157
158         r = t==0 || t==5 ? c+m :
159             t==1 || t==4 ? x+m : m;
160         g = t==1 || t==2 ? c+m :
161             t==0 || t==3 ? x+m : m;
162         b = t==3 || t==4 ? c+m :
163             t==2 || t==5 ? x+m : m;
164
165         x = MIN(r*6/0x100, 5)*6*6 +
166             MIN(g*6/0x100, 5)*6   +
167             MIN(b*6/0x100, 5)     + 16;
168
169         //debug("%12s %3d,%02x,%02x -> "
170         //      "tcxm=%d,%02x,%02x,%02x -> "
171         //      "rgb=%02x,%02x,%02x -> %d",
172         //           str, h,s,l, t,c,x,m, r,g,b, x);
173
174         return color(x, -1);
175 }
176
177 /* Window functions */
178 static int in_window(message_t *msg, window_t *win)
179 {
180         channel_t  *chan = msg->channel;
181         server_t   *srv  = chan->server;
182         const char *name = chan->name;
183         if (!win)
184                 return 0;
185         if (win->hide)
186                 return 0;
187         if (win->server)
188                 if (srv != win->server)
189                         return 0;
190         if (win->filter)
191                 return !regexec(&win->regex, name, 0, 0, 0);
192         else
193                 return win->channel == chan;
194 }
195
196 static void add_window(window_t *win)
197 {
198         window_t **last = &windows;
199         while (*last)
200                 last = &(*last)->next;
201         *last = win;
202 }
203
204 static window_t *find_window(const char *name, int create)
205 {
206         window_t *win;
207         for (win = windows; win; win = win->next)
208                 if (match(win->name, name))
209                         break;
210         if (!win && create) {
211                 win = new0(window_t);
212                 win->name = strcopy(name);
213                 win->pinned = -1;
214                 add_window(win);
215         }
216         return win;
217 }
218
219 static window_t *next_window(window_t *win)
220 {
221         if (!win)
222                 return NULL;
223         for (window_t *cur = win->next; cur; cur = cur->next)
224                 if (!cur->hide)
225                         return cur;
226         return win && !win->hide ? win : NULL;
227 }
228
229 static window_t *prev_window(window_t *win)
230 {
231         if (!win)
232                 return NULL;
233         window_t *prev = windows;
234         for (window_t *cur = windows; cur != win; cur = cur->next)
235                 if (!cur->hide)
236                         prev = cur;
237         return prev && !prev->hide ? prev : NULL;
238 }
239
240 static void cycle_channel(void)
241 {
242         if (!focus)
243                 return;
244
245         /* Clear seen flags */
246         for (int i = 0; i < history; i++)
247                 messages[i].channel->seen = 0;
248
249         /* Find current channel */
250         channel_t *first = NULL, *cur = NULL, *next = NULL;
251         for (int i = 0; i < history && !next; i++) {
252                 message_t *msg = &messages[i];
253                 if (msg->channel->seen)
254                         continue;
255                 msg->channel->seen = 1;
256                 if (!in_window(msg, focus))
257                         continue;
258                 if (cur)
259                         next = msg->channel;
260                 if (msg->channel == focus->channel)
261                         cur = msg->channel;
262                 if (!first)
263                         first = msg->channel;
264         }
265         focus->channel = next ?: first;
266 }
267
268 static void update_windows(void)
269 {
270         static int seen = 0;
271         for (; seen < history; seen++) {
272                 window_t  *win  = NULL;
273                 message_t *msg  = &messages[seen];
274                 channel_t *chan = msg->channel;
275
276                 /* Flag existing windows */
277                 for (window_t *cur = windows; cur; cur = cur->next) {
278                         if (in_window(msg, cur)) {
279                                 win = cur;
280                                 win->hide  = 0;
281                                 win->flag += 1;
282                         }
283                 }
284
285                 /* No window, create a new one */
286                 if (!win) {
287                         win = new0(window_t);
288                         win->channel = chan;
289                         win->name = strcopy(chan->name);
290                         win->pinned = -1;
291                         add_window(win);
292                 }
293         }
294
295         /* Update focus */
296         if (!focus)
297                 focus = windows;
298         if (focus && focus->hide)
299                 focus = next_window(focus) ?:
300                         prev_window(focus);
301 }
302
303 /* Print functions */
304 static void print_word(char **msg, int *row, int *col, int indent, int print)
305 {
306         char *start = *msg;
307         while (*start && isspace(*start))
308                 start++;
309         int space = start-*msg;
310
311         char *end = start;
312         while (*end && !isspace(*end))
313                 end++;
314         int len = end-start;
315
316         if ((*col != indent) && (*col + space + len > COLS)) {
317                 *col = indent;
318                 *row = (*row)+1;
319                 space = 0;
320         }
321
322         if (*row < 1 || *row > LINES-2)
323                 print = 0;
324
325         if (print)
326                 mvaddnstr(*row, *col, *msg, space);
327         *col += space;
328         if (print)
329                 mvaddnstr(*row, *col, start, len);
330         *col += len;
331
332         *msg = end;
333 }
334
335 static void print_msg(message_t *msg, int *row, int print)
336 {
337         static char buf[512];
338         char *hdr = buf;
339         char *txt = msg->text;
340         int col = 0, indent = 0;
341         time_t timep = msg->when;
342         struct tm *tm = localtime(&timep);
343
344         if (!in_window(msg, focus))
345                 return;
346
347         if (focus->filter && msg->from)
348                 snprintf(buf, sizeof(buf), "%02d:%02d [%s] %s: ",
349                                 tm->tm_hour, tm->tm_min,
350                                 msg->channel->name, msg->from);
351         else if (focus->filter)
352                 snprintf(buf, sizeof(buf), "%02d:%02d [%s] *** ",
353                                 tm->tm_hour, tm->tm_min, msg->channel->name);
354         else if (msg->from)
355                 snprintf(buf, sizeof(buf), "%02d:%02d %s: ",
356                                 tm->tm_hour, tm->tm_min, msg->from);
357         else
358                 snprintf(buf, sizeof(buf), "%02d:%02d *** ",
359                                 tm->tm_hour, tm->tm_min);
360
361         const char *chan = focus->filter ? msg->channel->name : NULL;
362         const char *from = msg->from;
363         int attr, word = 0;
364         while (*hdr) {
365                 if      (word==0)         attr = color_date;
366                 else if (word==1 && chan) attr = str_color(chan) | A_BOLD;
367                 else if (word==1 && from) attr = str_color(from);
368                 else if (word>=2 && from) attr = str_color(from);
369                 else                      attr = 0;
370                 attron(attr);
371                 print_word(&hdr, row, &col, indent, print);
372                 attroff(attr);
373                 word++;
374         }
375         indent = col;
376         while (*txt)
377                 print_word(&txt, row, &col, indent, print);
378
379         (*row)++;
380 }
381
382 /* Commands */
383 static int send_command(const char *text)
384 {
385         const char *arg;
386         window_t   *win = focus;
387
388         if (match(text, "/quit")) {
389                 poll_quit();
390         }
391         else if (match(text, "/sort")) {
392                 qsort(messages, history, sizeof(message_t), msg_compare);
393         }
394         else if (prefix(text, "/theme", &arg)) {
395                 for (int i = 0; i < N_ELEMENTS(themes); i++)
396                         if (match(themes[i], arg))
397                                 theme = i;
398         }
399         else if (prefix(text, "/open", &arg)) {
400                 if (!arg && focus)
401                         arg = focus->channel->name;
402                 if (!arg)
403                         arg = "(new)";
404                 focus = find_window(arg, 1);
405                 focus->hide = 0;
406         }
407         else if (prefix(text, "/close", &arg)) {
408                 if (arg)
409                         win = find_window(arg, 0);
410                 if (win)
411                         win->hide = 1;
412         }
413         else if (focus && prefix(text, "/name", &arg)) {
414                 strset(&focus->name, arg);
415         }
416         else if (focus && prefix(text, "/server", &arg)) {
417                 server_t *srv = NULL;
418                 if (arg)
419                         srv = find_server(arg);
420                 win->server = srv;
421         }
422         else if (focus && prefix(text, "/channel", &arg)) {
423                 channel_t *chan = NULL;
424                 if (arg)
425                         chan = find_channel(arg);
426                 win->channel = chan;
427         }
428         else if (focus && prefix(text, "/filter", &arg)) {
429                 strset(&focus->filter, arg);
430                 if (focus->filter)
431                         regcomp(&focus->regex, focus->filter, RE_FLAGS);
432         }
433         else if (focus && focus->channel) {
434                 chat_send(focus->channel, text);
435         }
436         else {
437                 return 0;
438         }
439         return 1;
440 }
441
442 /* Drawing functions */
443 void draw_header(void)
444 {
445         const char *topic = "No Topic";
446         if (focus && focus->channel && focus->channel->topic)
447                 topic = focus->channel->topic;
448         attron(color_title);
449         mvhline(0, 0, ' ', COLS);
450         mvprintw(0, 0, " %s", topic);
451         attroff(color_title);
452 }
453
454 int get_lines(int pos)
455 {
456         int lines = 0;
457         print_msg(&messages[pos], &lines, 0);
458         return lines;
459 }
460
461 int get_prev(int pos)
462 {
463         while (1) {
464                 if (--pos < 0)
465                         return -1;
466                 if (get_lines(pos))
467                         return pos;
468         }
469 }
470
471 int get_next(int pos)
472 {
473         while (1) {
474                 if (++pos >= history)
475                         return -1;
476                 if (get_lines(pos))
477                         return pos;
478         }
479 }
480
481 void draw_chat(void)
482 {
483         int space = LINES-3;
484         int row   = 0;
485         int log   = history-1;
486
487         /* Clear window */
488         for (int i = 1; i < LINES-2; i++) {
489                 move(i, 0);
490                 clrtoeol();
491         }
492
493         /* Clear pinned if scrolling */
494         if (focus->scroll && focus->pinned < 0)
495                 focus->pinned = history-1;
496
497         /* Scroll up to previous messages */
498         while (focus->scroll < 0) {
499                 int lines = get_lines(focus->pinned);
500
501                 /* Scrolling to a warpped line in this message */
502                 if (lines + focus->scroll > 0)
503                         break;
504
505                 /* Find the previous message */
506                 int prev = get_prev(focus->pinned);
507
508                 /* At the top message already,
509                  * just scroll to the top line. */
510                 if (prev < 0) {
511                         focus->scroll = -lines + 1;
512                         break;
513                 }
514
515                 /* Scroll back to the previous message */
516                 focus->pinned  = prev;
517                 focus->scroll += lines;
518         }
519
520         /* Scroll down to next messages */
521         while (focus->scroll > 0) {
522                 /* Find the next message */
523                 int next = get_next(focus->pinned);
524
525                 /* At the bottom already,
526                  * remove pin and scroll to last line */
527                 if (next < 0) {
528                         focus->pinned = -1;
529                         focus->scroll = 0;
530                         break;
531                 }
532
533                 /* Scroll to the next message */
534                 int lines = get_lines(next);
535                 focus->pinned  = next;
536                 focus->scroll -= lines;
537         }
538
539         /* Find pinned message */
540         if (focus->pinned >= 0) {
541                 log = focus->pinned;
542                 row = focus->scroll;
543         }
544
545         /* Compute lines */
546         while (row < space && log >= 0)
547                 print_msg(&messages[log--], &row, 0);
548
549         /* Compute skip lines */
550         row = 1 + (space - row);
551         log = log + 1;
552
553         /* Print lines */
554         while (row <= space && log < history)
555                 print_msg(&messages[log++], &row, 1);
556 }
557
558 void draw_status(void)
559 {
560         attron(color_title);
561         mvhline(LINES-2, 0, ' ', COLS);
562         move(LINES-2, 0);
563         printw(" Windows:");
564         if (!windows)
565                 printw(" none");
566         for (window_t *cur = windows; cur; cur = cur->next) {
567                 if (cur->hide)
568                         continue;
569                 printw(" ");
570                 if (cur == focus)
571                         cur->flag = 0;
572                 if (cur == focus)
573                         attron(A_BOLD);
574                 if (cur->flag)
575                         printw("[%s:%d]", cur->name, cur->flag);
576                 else
577                         printw("[%s]", cur->name);
578                 if (cur == focus)
579                         attroff(A_BOLD);
580         }
581         attroff(color_title);
582 }
583
584 void draw_cmdline(void)
585 {
586         const char *name;
587         if (focus && focus->channel)
588                 name = focus->channel->name;
589         else
590                 name = "(none)";
591
592         move(LINES-1, 0);
593         clrtoeol();
594         printw("[%s] %.*s", name, cmd_len, cmd_buf);
595         move(LINES-1, 1 + strlen(name) + 2 + cmd_pos);
596 }
597
598 /* Handle input */
599 static void process(int chr)
600 {
601         /* Window management */
602         if (chr == KEY_RESIZE) {
603                 clear();
604                 view_draw();
605         }
606         else if (chr == KEY_CTRL_L) {
607                 clear();
608                 view_draw();
609         }
610         else if (chr == KEY_CTRL_G) {
611                 view_draw();
612         }
613
614         /* View management */
615         else if (chr == KEY_CTRL_N) {
616                 focus = next_window(focus) ?: focus;
617                 view_draw();
618         }
619         else if (chr == KEY_CTRL_P) {
620                 focus = prev_window(focus) ?: focus;
621                 view_draw();
622         }
623         else if (chr == KEY_CTRL_X) {
624                 cycle_channel();
625                 view_draw();
626         }
627         else if (chr == KEY_CTRL_Y && focus) {
628                 focus->scroll -= 1;
629                 view_draw();
630         }
631         else if (chr == KEY_CTRL_E && focus) {
632                 focus->scroll += 1;
633                 view_draw();
634         }
635         else if (chr == KEY_CTRL_U && focus) {
636                 focus->scroll -= (LINES-3)/2;
637                 view_draw();
638         }
639         else if (chr == KEY_CTRL_D && focus) {
640                 focus->scroll += (LINES-3)/2;
641                 view_draw();
642         }
643         else if (chr == KEY_PPAGE && focus) {
644                 focus->scroll -= (LINES-3)-1;
645                 view_draw();
646         }
647         else if (chr == KEY_NPAGE && focus) {
648                 focus->scroll += (LINES-3)-1;
649                 view_draw();
650         }
651
652         /* Cmdline Input */
653         else if (chr == KEY_RETURN) {
654                 cmd_buf[cmd_len] = '\0';
655                 if (send_command(cmd_buf)) {
656                         cmd_pos = 0;
657                         cmd_len = 0;
658                 }
659                 view_draw();
660         }
661         else if (chr == KEY_ESCAPE) {
662                 cmd_pos = 0;
663                 cmd_len = 0;
664                 draw_cmdline();
665         }
666         else if (chr == KEY_LEFT) {
667                 if (cmd_pos > 0)
668                         cmd_pos--;
669                 draw_cmdline();
670         }
671         else if (chr == KEY_RIGHT) {
672                 if (cmd_pos < cmd_len)
673                         cmd_pos++;
674                 draw_cmdline();
675         }
676         else if (chr == KEY_BACKSPACE) {
677                 if (cmd_pos > 0) {
678                         memmove(&cmd_buf[cmd_pos-1],
679                                 &cmd_buf[cmd_pos],
680                                 (cmd_len-cmd_pos)+1);
681                         cmd_pos--;
682                         cmd_len--;
683                 }
684                 draw_cmdline();
685         }
686         else if (chr == KEY_DC) {
687                 if (cmd_pos < cmd_len) {
688                         memmove(&cmd_buf[cmd_pos],
689                                 &cmd_buf[cmd_pos+1],
690                                 (cmd_len-cmd_pos)+1);
691                         cmd_len--;
692                         draw_cmdline();
693                 }
694         }
695         else if (isprint(chr)) {
696                 if (cmd_len+2 < sizeof(cmd_buf)) {
697                         memmove(&cmd_buf[cmd_pos+1],
698                                 &cmd_buf[cmd_pos],
699                                 (cmd_len-cmd_pos)+1);
700                         cmd_buf[cmd_pos] = chr;
701                         cmd_pos++;
702                         cmd_len++;
703                         draw_cmdline();
704                 } else {
705                         debug("form: out of space");
706                 }
707         }
708
709         /* Unknown control character */
710         else {
711                 debug("main: Unhandled key - Dec %3d,  Hex %02x,  Oct %03o,  Chr <%c>",
712                                 chr, chr, chr, chr);
713         }
714 }
715
716 /* View init */
717 void view_init(void)
718 {
719         /* Setup windows */
720         for (window_t *win = windows; win; win = win->next)
721                 if (win->filter)
722                         regcomp(&win->regex, win->filter, RE_FLAGS);
723
724         /* Set default escape timeout */
725         if (!getenv("ESCDELAY"))
726                 putenv("ESCDELAY=25");
727
728         /* Setup Curses */
729         setlocale(LC_ALL, "");
730         initscr();
731         cbreak();
732         noecho();
733         keypad(stdscr, TRUE);
734         start_color();
735         timeout(0);
736         use_default_colors();
737
738         color_title = color(COLOR_WHITE, COLOR_BLUE);
739         color_date  = color(COLOR_BROWN, -1);
740         color_error = color(COLOR_RED,   -1);
741
742         /* Create signal FD */
743         sigset_t mask;
744         sigemptyset(&mask);
745         sigaddset(&mask, SIGWINCH);
746         if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0)
747                 error("creating signal fd");
748
749         /* Register callback */
750         poll_add(&poll_in, 0, (cb_t)view_sync, NULL);
751         poll_ctl(&poll_in, 1, 0, 1);
752
753         poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL);
754         poll_ctl(&poll_sig, 1, 0, 1);
755
756         /* Set running */
757         running = 1;
758
759         /* Draw initial view */
760         view_draw();
761         refresh();
762 }
763
764 /* Config parser */
765 void view_config(const char *group, const char *name, const char *key, const char *value)
766 {
767         window_t *win;
768
769         if (match(group, "general")) {
770                 if (match(key, "theme"))
771                         theme = get_enum(value, themes, sizeof(themes));
772         }
773
774         if (match(group, "window")) {
775                 if (match(key, ""))
776                         get_name(name);
777                 win = find_window(name, 1);
778                 if (match(key, "server"))
779                         win->server = find_server(get_string(value));
780                 if (match(key, "channel"))
781                         win->channel = find_channel(get_string(value));
782                 if (match(key, "filter"))
783                         win->filter = get_string(value);
784         }
785 }
786
787 /* View event */
788 void view_sync(void)
789 {
790         int chr;
791         while ((chr = getch()) != ERR)
792                 process(chr);
793
794         if (draw) {
795                 debug("view: flush draw");
796
797                 update_windows();
798
799                 draw_header();
800                 draw_chat();
801                 draw_status();
802                 draw_cmdline();
803
804                 refresh();
805
806                 draw = 0;
807         }
808 }
809
810 void view_draw(void)
811 {
812         debug("view: queue draw");
813         draw = 1;
814 }
815
816 void view_exit(void)
817 {
818         if (!running)
819                 return;
820         endwin();
821 }