2 * Copyright (C) 2012-2013 Andy Spencer <andy753421@gmail.com>
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #define _XOPEN_SOURCE_EXTENDED
28 #include <sys/signalfd.h>
38 #define RE_FLAGS (REG_EXTENDED|REG_NOSUB)
41 #define COLOR_BROWN 130
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'
54 #define KEY_RETURN '\12'
55 #define KEY_ESCAPE '\33'
56 #define KEY_DELETE '\177'
59 typedef struct window_t window_t;
61 typedef struct window_t {
98 static poll_t poll_in;
99 static poll_t poll_sig;
107 static char cmd_buf[4096];
113 static int hdr_lines;
114 static int sts_lines;
115 static int cmd_lines;
117 static server_t sys_srv;
118 static channel_t sys_chan;
119 static window_t sys_win;
121 static window_t *focus;
122 static window_t *windows;
124 static int ncolors = 1;
125 static short colors[256+1][256+1];
126 static short color_title;
127 static short color_date;
128 static short color_error;
130 static const char *themes[] = {
131 [THEME_NORMAL] "normal",
133 [THEME_LIGHT] "light",
136 /* Helper functions */
137 static short color(int fg, int bg)
139 if (!colors[fg+1][bg+1]) {
140 init_pair(ncolors, fg, bg);
141 colors[fg+1][bg+1] = ncolors;
144 return COLOR_PAIR(colors[fg+1][bg+1]);
147 static int msg_compare(const void *_a, const void *_b)
149 const message_t *a = _a;
150 const message_t *b = _b;
151 return a->when < b->when ? -1 :
152 a->when > b->when ? 1 : 0;
155 static unsigned str_hash(const char *str)
157 unsigned long hash = 0;
158 for (int i = 0; str[i]; i++)
159 hash = str[i] + (hash<<6) + (hash<<16) - hash;
160 for (int i = 0; str[i]; i++)
161 hash = str[i] + (hash<<6) + (hash<<16) - hash;
165 static int str_color(const char *str)
167 int h,s,l, t,c,x,m, r,g,b;
169 unsigned int n = str_hash(str);
171 h = (n / 0x1) % 0x100; // 0..256
172 s = (n / 0x100) % 0x100; // 0..256
173 l = (n / 0x10000) % 0x100; // 0..256
175 h = h * 360 / 0x100; // 0..360
176 s = s / 0x4 + 0xC0; // 0..256
179 case THEME_DARK: l = l / 0x4 + 0x80; break;
180 case THEME_LIGHT: l = l / 0x4 + 0x30; break;
181 case THEME_NORMAL: l = l / 0x4 + 0x60; break;
184 t = h / 60; // ,1,2,3,4,5
185 c = ((0x80-abs(l-0x80))*s)/0x80; // ..256
186 x = (c*(60-abs((h%120)-60)))/60; // ..256
187 m = l - c/2; // ..128
189 r = t==0 || t==5 ? c+m :
190 t==1 || t==4 ? x+m : m;
191 g = t==1 || t==2 ? c+m :
192 t==0 || t==3 ? x+m : m;
193 b = t==3 || t==4 ? c+m :
194 t==2 || t==5 ? x+m : m;
196 x = MIN(r*6/0x100, 5)*6*6 +
197 MIN(g*6/0x100, 5)*6 +
198 MIN(b*6/0x100, 5) + 16;
200 //debug("%12s %3d,%02x,%02x -> "
201 // "tcxm=%d,%02x,%02x,%02x -> "
202 // "rgb=%02x,%02x,%02x -> %d",
203 // str, h,s,l, t,c,x,m, r,g,b, x);
208 /* Window functions */
209 static int in_window(message_t *msg, window_t *win)
211 if (win->server && win->server != msg->channel->server)
213 if (win->channel && win->channel != msg->channel)
215 if (win->filter && regexec(&win->regex, msg->channel->name, 0, 0, 0))
220 static window_t *add_window(channel_t *dest)
222 window_t *win = new0(window_t);
223 win->name = strcopy(dest->name);
228 window_t **last = &windows;
230 last = &(*last)->next;
235 static window_t *find_window(const char *name)
237 for (window_t *win = windows; win; win = win->next)
238 if (match(win->name, name))
243 static window_t *next_window(window_t *win)
245 for (window_t *cur = win->next; cur; cur = cur->next)
251 static window_t *prev_window(window_t *win)
253 window_t *prev = NULL;
254 for (window_t *cur = windows; cur != win; cur = cur->next)
260 static void cycle_channel(void)
262 /* Clear seen flags */
263 for (int i = 0; i < history; i++)
264 messages[i].channel->seen = 0;
266 /* Find current channel */
267 channel_t *first = NULL, *cur = NULL, *next = NULL;
268 for (int i = 0; i < history && !next; i++) {
269 message_t *msg = &messages[i];
270 if (msg->channel->seen)
272 msg->channel->seen = 1;
273 if (!in_window(msg, focus))
277 if (msg->channel == focus->dest)
280 first = msg->channel;
282 focus->dest = next ?: first ?: &sys_chan;
285 static void last_channel(void)
287 if (focus->dest == &sys_chan &&
288 focus->saved != &sys_chan) {
289 focus->dest = focus->saved;
292 for (int i = history-1; i>=0; i--) {
293 if (in_window(&messages[i], focus)) {
294 focus->dest = messages[i].channel;
300 static void update_windows(void)
303 for (; seen < history; seen++) {
304 window_t *win = NULL;
305 message_t *msg = &messages[seen];
306 channel_t *chan = msg->channel;
308 /* Flag existing windows */
309 for (window_t *cur = windows; cur; cur = cur->next) {
310 if (in_window(msg, cur)) {
317 /* No window, create a new one */
319 win = add_window(chan);
326 focus = next_window(focus) ?:
327 prev_window(focus) ?: &sys_win;
332 static server_t *find_server(const char *name)
334 for (server_t *cur = servers; cur; cur = cur->next)
335 if (match(cur->name, name))
340 static channel_t *find_channel(const char *name)
342 for (channel_t *cur = channels; cur; cur = cur->next)
343 if (match(cur->name, name))
348 /* Print functions */
349 static int word_wrap(printer_t *pr, const char *txt,
350 const char **l, const char **r)
352 /* In previous word? */
353 if (txt > *l && txt < *r)
356 /* Find left and right ends of the word */
358 while (*txt != '\0' &&
359 *txt != ' ' && *txt != '\t' &&
360 *txt != '\r' && *txt != '\n')
364 /* End goes past the margin? */
366 int edge = COLS - pr->margin;
367 if (pr->col + len > edge) {
368 if (pr->indent + len > edge)
369 *r = *l + (edge - pr->indent);
370 return pr->col != pr->indent;
376 static void print_string(printer_t *pr, const char *txt)
379 const char *l = 0, *r = 0;
380 for (i = 0; txt[i]; i++) {
381 if (word_wrap(pr, &txt[i], &l, &r)) {
383 pr->col = pr->indent;
394 pr->col -= pr->indent;
398 pr->col += pr->indent;
403 pr->col = pr->indent;
407 pr->row >= pr->top &&
409 mvaddch(pr->row, pr->col, txt[i]);
420 static void print_format(printer_t *pr, const char *fmt, ...)
422 static char buf[4096];
425 vsnprintf(buf, sizeof(buf), fmt, ap);
428 print_string(pr, buf);
431 static void print_message(printer_t *pr, message_t *msg)
433 if (!in_window(msg, focus))
436 time_t timep = msg->when;
437 struct tm *tm = localtime(&timep);
445 print_format(pr, "%02d:%02d ", tm->tm_hour, tm->tm_min);
449 if (!focus->channel) {
450 attron(str_color(msg->channel->name) | A_BOLD);
451 print_format(pr, "[%s] ", msg->channel->name);
452 attroff(str_color(msg->channel->name) | A_BOLD);
457 attron(str_color(msg->from->name));
458 print_format(pr, "%s: ", msg->from->name);
459 attroff(str_color(msg->from->name));
461 print_format(pr, "*** ");
465 pr->indent = pr->col;
466 print_string(pr, msg->text);
470 static void print_status(printer_t *pr)
473 print_format(pr, " Windows: ");
474 pr->indent = pr->col;
476 for (window_t *cur = windows; cur; cur = cur->next) {
484 print_format(pr, "[%s:%d]", cur->name, cur->flag);
486 print_format(pr, "[%s]", cur->name);
493 static void print_clear(printer_t *pr)
495 for (int i = pr->top; i < pr->end; i++)
496 mvhline(i, 0, ' ', COLS);
500 static int get_lines(int pos)
502 printer_t pr = {.print=0};
503 print_message(&pr, &messages[pos]);
507 static int next_message(int pos)
512 if (in_window(&messages[pos], focus))
517 static int prev_message(int pos)
520 if (++pos >= history)
522 if (in_window(&messages[pos], focus))
528 static int send_command(const char *text)
531 window_t *win = focus;
533 if (match(text, "/quit")) {
536 else if (match(text, "/sort")) {
537 qsort(messages, history, sizeof(message_t), msg_compare);
539 else if (prefix(text, "/theme", &arg)) {
540 for (int i = 0; i < N_ELEMENTS(themes); i++)
541 if (match(themes[i], arg))
544 else if (match(text, "/open")) {
545 win = find_window(focus->dest->name);
546 if (!win || win == focus) {
547 win = add_window(focus->dest);
548 win->channel = focus->dest;
553 else if (match(text, "/close")) {
556 else if (prefix(text, "/name", &arg) && arg) {
557 strset(&focus->name, arg);
559 else if (prefix(text, "/server", &arg)) {
560 win->server = find_server(arg);
562 else if (prefix(text, "/channel", &arg)) {
563 win->channel = find_channel(arg);
564 win->dest = win->channel ?: win->dest;
566 else if (prefix(text, "/filter", &arg)) {
567 strset(&focus->filter, arg);
569 regcomp(&focus->regex, focus->filter, RE_FLAGS);
571 else if (focus->dest != &sys_chan) {
572 chat_send(focus->dest, text);
580 /* Drawing functions */
581 static void draw_header(void)
591 const char *topic = focus->dest->topic ?: "No Topic";
594 print_format(&pr, "%s", topic);
595 attroff(color_title);
596 hdr_lines = pr.row + 1;
599 static void draw_chat(void)
601 /* Clear pinned if scrolling */
602 if (focus->scroll && focus->pinned < 0)
603 focus->pinned = history-1;
605 /* Scroll up to previous messages */
606 while (focus->scroll < 0) {
607 int lines = get_lines(focus->pinned);
609 /* Scrolling to a warpped line in this message */
610 if (lines + focus->scroll > 0)
613 /* Find the previous message */
614 int prev = next_message(focus->pinned);
616 /* At the top message already,
617 * just scroll to the top line. */
619 focus->scroll = -lines + 1;
623 /* Scroll back to the previous message */
624 focus->pinned = prev;
625 focus->scroll += lines;
628 /* Scroll down to next messages */
629 while (focus->scroll > 0) {
630 /* Find the next message */
631 int next = prev_message(focus->pinned);
633 /* At the bottom already,
634 * remove pin and scroll to last line */
641 /* Scroll to the next message */
642 int lines = get_lines(next);
643 focus->pinned = next;
644 focus->scroll -= lines;
647 /* Find pinned message */
651 if (focus->pinned >= 0) {
653 skip = focus->scroll;
659 .end = LINES-cmd_lines-sts_lines,
665 pr.row = pr.top + skip;
666 while (pr.row < pr.end && log >= 0)
667 print_message(&pr, &messages[log--]);
669 /* Compute skip lines */
670 skip = pr.end - pr.row;
675 pr.row = pr.top + skip;
676 while (pr.row < pr.end && log < history)
677 print_message(&pr, &messages[log++]);
680 static void draw_status(void)
685 sts_lines = pr.row + 1;
690 pr.row = LINES-cmd_lines-sts_lines;
691 pr.top = LINES-cmd_lines-sts_lines;
692 pr.end = LINES-cmd_lines;
695 attroff(color_title);
698 static void draw_cmdline(void)
700 const char *name = focus->dest->name;
702 .indent = 1 + strlen(name) + 2,
706 /* Terminate buffer */
707 cmd_buf[cmd_len] = '\0';
711 print_string(&pr, cmd_buf);
712 cmd_lines = pr.row + 1;
717 pr.row = LINES-cmd_lines;
718 pr.top = LINES-cmd_lines;
723 mvprintw(pr.top, 0, "[%s]", name);
725 print_string(&pr, cmd_buf);
731 static void process(int chr)
733 /* Window management */
734 if (chr == KEY_RESIZE) {
737 else if (chr == KEY_CTRL_L) {
740 else if (chr == KEY_CTRL_G) {
744 /* View management */
745 else if (chr == KEY_CTRL_N) {
746 focus = next_window(focus) ?: focus;
748 else if (chr == KEY_CTRL_P) {
749 focus = prev_window(focus) ?: focus;
751 else if (chr == KEY_CTRL_X) {
754 else if (chr == KEY_CTRL_Y) {
757 else if (chr == KEY_CTRL_E) {
760 else if (chr == KEY_CTRL_U) {
761 focus->scroll -= (LINES-3)/2;
763 else if (chr == KEY_CTRL_D) {
764 focus->scroll += (LINES-3)/2;
766 else if (chr == KEY_PPAGE) {
767 focus->scroll -= (LINES-3)-1;
769 else if (chr == KEY_NPAGE) {
770 focus->scroll += (LINES-3)-1;
774 else if (chr == KEY_RETURN) {
775 cmd_buf[cmd_len] = '\0';
776 if (send_command(cmd_buf)) {
781 else if (chr == KEY_TAB) {
787 else if (chr == KEY_ESCAPE) {
791 else if (chr == KEY_LEFT) {
795 else if (chr == KEY_RIGHT) {
796 if (cmd_pos < cmd_len)
799 else if (chr == KEY_HOME) {
802 else if (chr == KEY_END) {
805 else if (chr == KEY_BACKSPACE ||
808 memmove(&cmd_buf[cmd_pos-1],
810 (cmd_len-cmd_pos)+1);
815 else if (chr == KEY_DC) {
816 if (cmd_pos < cmd_len) {
817 memmove(&cmd_buf[cmd_pos],
819 (cmd_len-cmd_pos)+1);
823 else if (isprint(chr)) {
824 if (cmd_len+2 < sizeof(cmd_buf)) {
825 memmove(&cmd_buf[cmd_pos+1],
827 (cmd_len-cmd_pos)+1);
828 cmd_buf[cmd_pos] = chr;
832 debug("form: out of space");
836 /* Unknown control character */
838 debug("main: Unhandled key - Dec %3d, Hex %02x, Oct %03o, Chr <%c>",
847 sys_srv.name = strcopy("system");
848 sys_srv.protocol = -1;
850 sys_chan.name = strcopy("system");
851 sys_chan.server = &sys_srv;
853 sys_win.name = strcopy("system");
854 sys_win.dest = &sys_chan;
855 sys_win.channel = &sys_chan;
857 sys_win.next = windows;
859 windows = focus = &sys_win;
862 for (window_t *win = windows; win; win = win->next)
864 regcomp(&win->regex, win->filter, RE_FLAGS);
866 /* Print welcome message */
867 chat_recv(&sys_chan, NULL, "Welcome to lamechat!");
874 /* Set default escape timeout */
875 if (!getenv("ESCDELAY"))
876 putenv("ESCDELAY=25");
879 setlocale(LC_ALL, "");
883 keypad(stdscr, TRUE);
886 use_default_colors();
888 color_title = color(COLOR_WHITE, COLOR_BLUE);
889 color_date = color(COLOR_BROWN, -1);
890 color_error = color(COLOR_RED, -1);
892 /* Create signal FD */
895 sigaddset(&mask, SIGWINCH);
896 if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0)
897 error("creating signal fd");
899 /* Register callback */
900 poll_add(&poll_in, 0, (cb_t)view_sync, NULL);
901 poll_ctl(&poll_in, 1, 0, 1);
903 poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL);
904 poll_ctl(&poll_sig, 1, 0, 1);
909 /* Draw initial view */
915 void view_config(const char *group, const char *name, const char *key, const char *value)
919 if (match(group, "general")) {
920 if (match(key, "theme"))
921 theme = get_map(value, themes);
922 else if (match(key, "defocus"))
923 defocus = get_number(value);
926 if (match(group, "window")) {
927 win = find_window(name);
928 if (match(key, "")) {
929 win = add_window(&sys_chan);
930 strset(&win->name, get_name(name));
932 else if (match(key, "server")) {
933 win->server = find_server(get_string(value));
935 else if (match(key, "channel")) {
936 win->channel = find_channel(get_string(value));
937 win->dest = win->channel ?: win->dest;
939 else if (match(key, "filter")) {
940 win->filter = get_string(value);
949 while ((chr = getch()) != ERR) {
951 deadline = time(NULL) + defocus;
955 if (defocus && time(NULL) > deadline &&
956 focus->dest != &sys_chan) {
957 focus->saved = focus->dest;
958 focus->dest = &sys_chan;
963 debug("view: flush draw");
972 move(cmd_row, cmd_col);
982 debug("view: queue draw");