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'
53 #define KEY_RETURN '\12'
54 #define KEY_ESCAPE '\33'
57 typedef struct window_t window_t;
59 typedef struct window_t {
79 static poll_t poll_in;
80 static poll_t poll_sig;
86 static char cmd_buf[512];
90 static window_t *focus;
91 static window_t *windows;
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;
99 static const char *themes[] = {
100 [THEME_NORMAL] "normal",
102 [THEME_LIGHT] "light",
105 /* Helper functions */
106 static short color(int fg, int bg)
108 if (!colors[fg+1][bg+1]) {
109 init_pair(ncolors, fg, bg);
110 colors[fg+1][bg+1] = ncolors;
113 return COLOR_PAIR(colors[fg+1][bg+1]);
116 static int msg_compare(const void *_a, const void *_b)
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;
124 static unsigned str_hash(const char *str)
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;
134 static int str_color(const char *str)
136 int h,s,l, t,c,x,m, r,g,b;
138 unsigned int n = str_hash(str);
140 h = (n / 0x1) % 0x100; // 0..256
141 s = (n / 0x100) % 0x100; // 0..256
142 l = (n / 0x10000) % 0x100; // 0..256
144 h = h * 360 / 0x100; // 0..360
145 s = s / 0x4 + 0xC0; // 0..256
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;
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
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;
165 x = MIN(r*6/0x100, 5)*6*6 +
166 MIN(g*6/0x100, 5)*6 +
167 MIN(b*6/0x100, 5) + 16;
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);
177 /* Window functions */
178 static int in_window(message_t *msg, window_t *win)
180 channel_t *chan = msg->channel;
181 server_t *srv = chan->server;
182 const char *name = chan->name;
188 if (srv != win->server)
191 return !regexec(&win->regex, name, 0, 0, 0);
193 return win->channel == chan;
196 static void add_window(window_t *win)
198 window_t **last = &windows;
200 last = &(*last)->next;
204 static window_t *find_window(const char *name, int create)
207 for (win = windows; win; win = win->next)
208 if (match(win->name, name))
210 if (!win && create) {
211 win = new0(window_t);
212 win->name = strcopy(name);
219 static window_t *next_window(window_t *win)
223 for (window_t *cur = win->next; cur; cur = cur->next)
226 return win && !win->hide ? win : NULL;
229 static window_t *prev_window(window_t *win)
233 window_t *prev = windows;
234 for (window_t *cur = windows; cur != win; cur = cur->next)
237 return prev && !prev->hide ? prev : NULL;
240 static void cycle_channel(void)
245 /* Clear seen flags */
246 for (int i = 0; i < history; i++)
247 messages[i].channel->seen = 0;
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)
255 msg->channel->seen = 1;
256 if (!in_window(msg, focus))
260 if (msg->channel == focus->channel)
263 first = msg->channel;
265 focus->channel = next ?: first;
268 static void update_windows(void)
271 for (; seen < history; seen++) {
272 window_t *win = NULL;
273 message_t *msg = &messages[seen];
274 channel_t *chan = msg->channel;
276 /* Flag existing windows */
277 for (window_t *cur = windows; cur; cur = cur->next) {
278 if (in_window(msg, cur)) {
285 /* No window, create a new one */
287 win = new0(window_t);
289 win->name = strcopy(chan->name);
298 if (focus && focus->hide)
299 focus = next_window(focus) ?:
303 /* Print functions */
304 static void print_word(char **msg, int *row, int *col, int indent, int print)
307 while (*start && isspace(*start))
309 int space = start-*msg;
312 while (*end && !isspace(*end))
316 if ((*col != indent) && (*col + space + len > COLS)) {
322 if (*row < 1 || *row > LINES-2)
326 mvaddnstr(*row, *col, *msg, space);
329 mvaddnstr(*row, *col, start, len);
335 static void print_msg(message_t *msg, int *row, int print)
337 static char buf[512];
339 char *txt = msg->text;
340 int col = 0, indent = 0;
341 time_t timep = msg->when;
342 struct tm *tm = localtime(&timep);
344 if (!in_window(msg, focus))
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);
355 snprintf(buf, sizeof(buf), "%02d:%02d %s: ",
356 tm->tm_hour, tm->tm_min, msg->from);
358 snprintf(buf, sizeof(buf), "%02d:%02d *** ",
359 tm->tm_hour, tm->tm_min);
361 const char *chan = focus->filter ? msg->channel->name : NULL;
362 const char *from = msg->from;
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);
371 print_word(&hdr, row, &col, indent, print);
377 print_word(&txt, row, &col, indent, print);
383 static int send_command(const char *text)
386 window_t *win = focus;
388 if (match(text, "/quit")) {
391 else if (match(text, "/sort")) {
392 qsort(messages, history, sizeof(message_t), msg_compare);
394 else if (prefix(text, "/theme", &arg)) {
395 for (int i = 0; i < N_ELEMENTS(themes); i++)
396 if (match(themes[i], arg))
399 else if (prefix(text, "/open", &arg)) {
401 arg = focus->channel->name;
404 focus = find_window(arg, 1);
407 else if (prefix(text, "/close", &arg)) {
409 win = find_window(arg, 0);
413 else if (focus && prefix(text, "/name", &arg)) {
414 strset(&focus->name, arg);
416 else if (focus && prefix(text, "/server", &arg)) {
417 server_t *srv = NULL;
419 srv = find_server(arg);
422 else if (focus && prefix(text, "/channel", &arg)) {
423 channel_t *chan = NULL;
425 chan = find_channel(arg);
428 else if (focus && prefix(text, "/filter", &arg)) {
429 strset(&focus->filter, arg);
431 regcomp(&focus->regex, focus->filter, RE_FLAGS);
433 else if (focus && focus->channel) {
434 chat_send(focus->channel, text);
442 /* Drawing functions */
443 void draw_header(void)
445 const char *topic = "No Topic";
446 if (focus && focus->channel && focus->channel->topic)
447 topic = focus->channel->topic;
449 mvhline(0, 0, ' ', COLS);
450 mvprintw(0, 0, " %s", topic);
451 attroff(color_title);
454 int get_lines(int pos)
457 print_msg(&messages[pos], &lines, 0);
461 int get_prev(int pos)
471 int get_next(int pos)
474 if (++pos >= history)
488 for (int i = 1; i < LINES-2; i++) {
493 /* Clear pinned if scrolling */
494 if (focus->scroll && focus->pinned < 0)
495 focus->pinned = history-1;
497 /* Scroll up to previous messages */
498 while (focus->scroll < 0) {
499 int lines = get_lines(focus->pinned);
501 /* Scrolling to a warpped line in this message */
502 if (lines + focus->scroll > 0)
505 /* Find the previous message */
506 int prev = get_prev(focus->pinned);
508 /* At the top message already,
509 * just scroll to the top line. */
511 focus->scroll = -lines + 1;
515 /* Scroll back to the previous message */
516 focus->pinned = prev;
517 focus->scroll += lines;
520 /* Scroll down to next messages */
521 while (focus->scroll > 0) {
522 /* Find the next message */
523 int next = get_next(focus->pinned);
525 /* At the bottom already,
526 * remove pin and scroll to last line */
533 /* Scroll to the next message */
534 int lines = get_lines(next);
535 focus->pinned = next;
536 focus->scroll -= lines;
539 /* Find pinned message */
540 if (focus->pinned >= 0) {
546 while (row < space && log >= 0)
547 print_msg(&messages[log--], &row, 0);
549 /* Compute skip lines */
550 row = 1 + (space - row);
554 while (row <= space && log < history)
555 print_msg(&messages[log++], &row, 1);
558 void draw_status(void)
561 mvhline(LINES-2, 0, ' ', COLS);
566 for (window_t *cur = windows; cur; cur = cur->next) {
575 printw("[%s:%d]", cur->name, cur->flag);
577 printw("[%s]", cur->name);
581 attroff(color_title);
584 void draw_cmdline(void)
587 if (focus && focus->channel)
588 name = focus->channel->name;
594 printw("[%s] %.*s", name, cmd_len, cmd_buf);
595 move(LINES-1, 1 + strlen(name) + 2 + cmd_pos);
599 static void process(int chr)
601 /* Window management */
602 if (chr == KEY_RESIZE) {
606 else if (chr == KEY_CTRL_L) {
610 else if (chr == KEY_CTRL_G) {
614 /* View management */
615 else if (chr == KEY_CTRL_N) {
616 focus = next_window(focus) ?: focus;
619 else if (chr == KEY_CTRL_P) {
620 focus = prev_window(focus) ?: focus;
623 else if (chr == KEY_CTRL_X) {
627 else if (chr == KEY_CTRL_Y && focus) {
631 else if (chr == KEY_CTRL_E && focus) {
635 else if (chr == KEY_CTRL_U && focus) {
636 focus->scroll -= (LINES-3)/2;
639 else if (chr == KEY_CTRL_D && focus) {
640 focus->scroll += (LINES-3)/2;
643 else if (chr == KEY_PPAGE && focus) {
644 focus->scroll -= (LINES-3)-1;
647 else if (chr == KEY_NPAGE && focus) {
648 focus->scroll += (LINES-3)-1;
653 else if (chr == KEY_RETURN) {
654 cmd_buf[cmd_len] = '\0';
655 if (send_command(cmd_buf)) {
661 else if (chr == KEY_ESCAPE) {
666 else if (chr == KEY_LEFT) {
671 else if (chr == KEY_RIGHT) {
672 if (cmd_pos < cmd_len)
676 else if (chr == KEY_BACKSPACE) {
678 memmove(&cmd_buf[cmd_pos-1],
680 (cmd_len-cmd_pos)+1);
686 else if (chr == KEY_DC) {
687 if (cmd_pos < cmd_len) {
688 memmove(&cmd_buf[cmd_pos],
690 (cmd_len-cmd_pos)+1);
695 else if (isprint(chr)) {
696 if (cmd_len+2 < sizeof(cmd_buf)) {
697 memmove(&cmd_buf[cmd_pos+1],
699 (cmd_len-cmd_pos)+1);
700 cmd_buf[cmd_pos] = chr;
705 debug("form: out of space");
709 /* Unknown control character */
711 debug("main: Unhandled key - Dec %3d, Hex %02x, Oct %03o, Chr <%c>",
720 for (window_t *win = windows; win; win = win->next)
722 regcomp(&win->regex, win->filter, RE_FLAGS);
724 /* Set default escape timeout */
725 if (!getenv("ESCDELAY"))
726 putenv("ESCDELAY=25");
729 setlocale(LC_ALL, "");
733 keypad(stdscr, TRUE);
736 use_default_colors();
738 color_title = color(COLOR_WHITE, COLOR_BLUE);
739 color_date = color(COLOR_BROWN, -1);
740 color_error = color(COLOR_RED, -1);
742 /* Create signal FD */
745 sigaddset(&mask, SIGWINCH);
746 if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0)
747 error("creating signal fd");
749 /* Register callback */
750 poll_add(&poll_in, 0, (cb_t)view_sync, NULL);
751 poll_ctl(&poll_in, 1, 0, 1);
753 poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL);
754 poll_ctl(&poll_sig, 1, 0, 1);
759 /* Draw initial view */
765 void view_config(const char *group, const char *name, const char *key, const char *value)
769 if (match(group, "general")) {
770 if (match(key, "theme"))
771 theme = get_enum(value, themes, sizeof(themes));
774 if (match(group, "window")) {
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);
791 while ((chr = getch()) != ERR)
795 debug("view: flush draw");
812 debug("view: queue draw");