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