/* * Copyright (C) 2012-2013 Andy Spencer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define _XOPEN_SOURCE #define _XOPEN_SOURCE_EXTENDED #include #include #include #include #include #include #include #include #include #include #include "util.h" #include "conf.h" #include "chat.h" #include "view.h" /* View constants */ #define RE_FLAGS (REG_EXTENDED|REG_NOSUB) /* Extra colors */ #define COLOR_BROWN 130 /* Extra keys */ #define KEY_CTRL_D '\4' #define KEY_CTRL_E '\5' #define KEY_CTRL_G '\7' #define KEY_CTRL_L '\14' #define KEY_CTRL_N '\16' #define KEY_CTRL_P '\20' #define KEY_CTRL_U '\25' #define KEY_CTRL_X '\30' #define KEY_CTRL_Y '\31' #define KEY_TAB '\11' #define KEY_RETURN '\12' #define KEY_ESCAPE '\33' /* View types */ typedef struct window_t window_t; typedef struct window_t { char *name; int hide; int flag; int scroll; int pinned; char *filter; server_t *server; channel_t *channel; regex_t regex; channel_t *dest; channel_t *saved; window_t *next; } window_t; typedef struct { int top; int end; int row; int col; int indent; int margin; int print; } printer_t; typedef enum { THEME_NORMAL, THEME_DARK, THEME_LIGHT, } theme_t; /* Local data */ static poll_t poll_in; static poll_t poll_sig; static int sig_fd; static int running; static int defocus; static int deadline; static int theme; static int draw; static char cmd_buf[4096]; static int cmd_pos; static int cmd_len; static int cmd_row; static int cmd_col; static int hdr_lines; static int sts_lines; static int cmd_lines; static server_t sys_srv; static channel_t sys_chan; static window_t sys_win; static window_t *focus; static window_t *windows; static int ncolors = 1; static short colors[256+1][256+1]; static short color_title; static short color_date; static short color_error; static const char *themes[] = { [THEME_NORMAL] "normal", [THEME_DARK] "dark", [THEME_LIGHT] "light", }; /* Helper functions */ static short color(int fg, int bg) { if (!colors[fg+1][bg+1]) { init_pair(ncolors, fg, bg); colors[fg+1][bg+1] = ncolors; ncolors++; } return COLOR_PAIR(colors[fg+1][bg+1]); } static int msg_compare(const void *_a, const void *_b) { const message_t *a = _a; const message_t *b = _b; return a->when < b->when ? -1 : a->when > b->when ? 1 : 0; } static unsigned str_hash(const char *str) { unsigned long hash = 0; for (int i = 0; str[i]; i++) hash = str[i] + (hash<<6) + (hash<<16) - hash; for (int i = 0; str[i]; i++) hash = str[i] + (hash<<6) + (hash<<16) - hash; return hash; } static int str_color(const char *str) { int h,s,l, t,c,x,m, r,g,b; unsigned int n = str_hash(str); h = (n / 0x1) % 0x100; // 0..256 s = (n / 0x100) % 0x100; // 0..256 l = (n / 0x10000) % 0x100; // 0..256 h = h * 360 / 0x100; // 0..360 s = s / 0x4 + 0xC0; // 0..256 switch (theme) { case THEME_DARK: l = l / 0x4 + 0x80; break; case THEME_LIGHT: l = l / 0x4 + 0x30; break; case THEME_NORMAL: l = l / 0x4 + 0x60; break; } t = h / 60; // ,1,2,3,4,5 c = ((0x80-abs(l-0x80))*s)/0x80; // ..256 x = (c*(60-abs((h%120)-60)))/60; // ..256 m = l - c/2; // ..128 r = t==0 || t==5 ? c+m : t==1 || t==4 ? x+m : m; g = t==1 || t==2 ? c+m : t==0 || t==3 ? x+m : m; b = t==3 || t==4 ? c+m : t==2 || t==5 ? x+m : m; x = MIN(r*6/0x100, 5)*6*6 + MIN(g*6/0x100, 5)*6 + MIN(b*6/0x100, 5) + 16; //debug("%12s %3d,%02x,%02x -> " // "tcxm=%d,%02x,%02x,%02x -> " // "rgb=%02x,%02x,%02x -> %d", // str, h,s,l, t,c,x,m, r,g,b, x); return color(x, -1); } /* Window functions */ static int in_window(message_t *msg, window_t *win) { if (win->server && win->server != msg->channel->server) return 0; if (win->channel && win->channel != msg->channel) return 0; if (win->filter && regexec(&win->regex, msg->channel->name, 0, 0, 0)) return 0; return 1; } static window_t *add_window(channel_t *dest) { window_t *win = new0(window_t); win->name = strcopy(dest->name); win->dest = dest; win->saved = dest; win->pinned = -1; window_t **last = &windows; while (*last) last = &(*last)->next; *last = win; return win; } static window_t *find_window(const char *name) { for (window_t *win = windows; win; win = win->next) if (match(win->name, name)) return win; return NULL; } static window_t *next_window(window_t *win) { for (window_t *cur = win->next; cur; cur = cur->next) if (!cur->hide) return cur; return NULL; } static window_t *prev_window(window_t *win) { window_t *prev = NULL; for (window_t *cur = windows; cur != win; cur = cur->next) if (!cur->hide) prev = cur; return prev; } static void cycle_channel(void) { /* Clear seen flags */ for (int i = 0; i < history; i++) messages[i].channel->seen = 0; /* Find current channel */ channel_t *first = NULL, *cur = NULL, *next = NULL; for (int i = 0; i < history && !next; i++) { message_t *msg = &messages[i]; if (msg->channel->seen) continue; msg->channel->seen = 1; if (!in_window(msg, focus)) continue; if (cur) next = msg->channel; if (msg->channel == focus->dest) cur = msg->channel; if (!first) first = msg->channel; } focus->dest = next ?: first ?: &sys_chan; } static void last_channel(void) { if (focus->dest == &sys_chan && focus->saved != &sys_chan) { focus->dest = focus->saved; return; } for (int i = history-1; i>=0; i--) { if (in_window(&messages[i], focus)) { focus->dest = messages[i].channel; return; } } } static void update_windows(void) { static int seen = 0; for (; seen < history; seen++) { window_t *win = NULL; message_t *msg = &messages[seen]; channel_t *chan = msg->channel; /* Flag existing windows */ for (window_t *cur = windows; cur; cur = cur->next) { if (in_window(msg, cur)) { win = cur; win->hide = 0; win->flag += 1; } } /* No window, create a new one */ if (!win) { win = add_window(chan); win->channel = chan; } } /* Update focus */ if (focus->hide) { focus = next_window(focus) ?: prev_window(focus) ?: &sys_win; focus->hide = 0; } } static server_t *find_server(const char *name) { for (server_t *cur = servers; cur; cur = cur->next) if (match(cur->name, name)) return cur; return NULL; } static channel_t *find_channel(const char *name) { for (channel_t *cur = channels; cur; cur = cur->next) if (match(cur->name, name)) return cur; return NULL; } /* Print functions */ static void print_word(printer_t *pr, const char **msg) { const char *start = *msg; while (*start && isspace(*start)) start++; int space = start-*msg; const char *end = start; while (*end && !isspace(*end)) end++; int len = end-start; if ((pr->col != pr->indent) && (pr->col + space + len > COLS - pr->margin)) { pr->col = pr->indent; pr->row = pr->row+1; space = 0; } int print = pr->print; if (pr->row < pr->top || pr->row >= pr->end) print = 0; if (print) mvaddnstr(pr->row, pr->col, *msg, space); pr->col += space; if (print) mvaddnstr(pr->row, pr->col, start, len); pr->col += len; *msg = end; } static void print_format(printer_t *pr, const char *fmt, ...) { static char buf[4096]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); const char *txt = buf; while (*txt) print_word(pr, &txt); } static void print_message(printer_t *pr, message_t *msg) { static char buf[4096]; const char *hdr = buf; const char *txt = msg->text; time_t timep = msg->when; struct tm *tm = localtime(&timep); const char *chan = NULL; const char *from = NULL; int attr, word = 0; if (!in_window(msg, focus)) return; if (!focus->channel) chan = msg->channel->name; if (msg->from) from = msg->from->name; if (!focus->channel && msg->from) snprintf(buf, sizeof(buf), "%02d:%02d [%s] %s: ", tm->tm_hour, tm->tm_min, chan, from); else if (!focus->channel) snprintf(buf, sizeof(buf), "%02d:%02d [%s] *** ", tm->tm_hour, tm->tm_min, chan); else if (msg->from) snprintf(buf, sizeof(buf), "%02d:%02d %s: ", tm->tm_hour, tm->tm_min, from); else snprintf(buf, sizeof(buf), "%02d:%02d *** ", tm->tm_hour, tm->tm_min); pr->col = 0; pr->indent = 0; while (*hdr) { if (word==0) attr = color_date; else if (word==1 && chan) attr = str_color(chan) | A_BOLD; else if (word==1 && from) attr = str_color(from); else if (word>=2 && from) attr = str_color(from); else attr = 0; attron(attr); print_word(pr, &hdr); attroff(attr); word++; } pr->indent = pr->col; while (*txt) print_word(pr, &txt); pr->row++; } static void print_status(printer_t *pr) { pr->col = 0; print_format(pr, " Windows: "); pr->indent = pr->col; pr->margin = 1; for (window_t *cur = windows; cur; cur = cur->next) { if (cur->hide) continue; if (cur == focus) cur->flag = 0; if (cur == focus) attron(A_BOLD); if (cur->flag) print_format(pr, "[%s:%d]", cur->name, cur->flag); else print_format(pr, "[%s]", cur->name); if (cur == focus) attroff(A_BOLD); pr->col++; } } static void print_clear(printer_t *pr) { for (int i = pr->top; i < pr->end; i++) mvhline(i, 0, ' ', COLS); } /* Message info */ static int get_lines(int pos) { printer_t pr = {.print=0}; print_message(&pr, &messages[pos]); return pr.row; } static int next_message(int pos) { while (1) { if (--pos < 0) return -1; if (in_window(&messages[pos], focus)) return pos; } } static int prev_message(int pos) { while (1) { if (++pos >= history) return -1; if (in_window(&messages[pos], focus)) return pos; } } /* Commands */ static int send_command(const char *text) { const char *arg; window_t *win = focus; if (match(text, "/quit")) { poll_quit(); } else if (match(text, "/sort")) { qsort(messages, history, sizeof(message_t), msg_compare); } else if (prefix(text, "/theme", &arg)) { for (int i = 0; i < N_ELEMENTS(themes); i++) if (match(themes[i], arg)) theme = i; } else if (match(text, "/open")) { win = find_window(focus->dest->name); if (!win || win == focus) { win = add_window(focus->dest); win->channel = focus->dest; } focus = win; focus->hide = 0; } else if (match(text, "/close")) { focus->hide = 1; } else if (prefix(text, "/name", &arg) && arg) { strset(&focus->name, arg); } else if (prefix(text, "/server", &arg)) { win->server = find_server(arg); } else if (prefix(text, "/channel", &arg)) { win->channel = find_channel(arg); win->dest = win->channel ?: win->dest; } else if (prefix(text, "/filter", &arg)) { strset(&focus->filter, arg); if (focus->filter) regcomp(&focus->regex, focus->filter, RE_FLAGS); } else if (focus->dest != &sys_chan) { chat_send(focus->dest, text); } else { return 0; } return 1; } /* Drawing functions */ static void draw_header(void) { printer_t pr = { .print = 1, .row = 0, .col = 1, .indent = 1, .margin = 1, .end = 5, }; const char *topic = focus->dest->topic ?: "No Topic"; attron(color_title); print_clear(&pr); print_format(&pr, "%s", topic); attroff(color_title); hdr_lines = pr.row + 1; } static void draw_chat(void) { /* Clear pinned if scrolling */ if (focus->scroll && focus->pinned < 0) focus->pinned = history-1; /* Scroll up to previous messages */ while (focus->scroll < 0) { int lines = get_lines(focus->pinned); /* Scrolling to a warpped line in this message */ if (lines + focus->scroll > 0) break; /* Find the previous message */ int prev = next_message(focus->pinned); /* At the top message already, * just scroll to the top line. */ if (prev < 0) { focus->scroll = -lines + 1; break; } /* Scroll back to the previous message */ focus->pinned = prev; focus->scroll += lines; } /* Scroll down to next messages */ while (focus->scroll > 0) { /* Find the next message */ int next = prev_message(focus->pinned); /* At the bottom already, * remove pin and scroll to last line */ if (next < 0) { focus->pinned = -1; focus->scroll = 0; break; } /* Scroll to the next message */ int lines = get_lines(next); focus->pinned = next; focus->scroll -= lines; } /* Find pinned message */ int log = history-1; int skip = 0; if (focus->pinned >= 0) { log = focus->pinned; skip = focus->scroll; } /* Setup printer */ printer_t pr = { .top = hdr_lines, .end = LINES-cmd_lines-sts_lines, }; print_clear(&pr); /* Compute lines */ pr.print = 0; pr.row = pr.top + skip; while (pr.row < pr.end && log >= 0) print_message(&pr, &messages[log--]); /* Compute skip lines */ skip = pr.end - pr.row; log = log + 1; /* Print lines */ pr.print = 1; pr.row = pr.top + skip; while (pr.row < pr.end && log < history) print_message(&pr, &messages[log++]); } static void draw_status(void) { /* Compute lines */ printer_t pr = {}; print_status(&pr); sts_lines = pr.row + 1; /* Draw status */ attron(color_title); pr.print = 1; pr.row = LINES-cmd_lines-sts_lines; pr.top = LINES-cmd_lines-sts_lines; pr.end = LINES-cmd_lines; print_clear(&pr); print_status(&pr); attroff(color_title); } static void draw_cmdline(void) { const char *txt, *name = focus->dest->name; cmd_buf[cmd_len] = '\0'; printer_t pr = { .indent = 1 + strlen(name) + 2, .margin = 1, }; /* Compute lines */ txt = cmd_buf; pr.col = pr.indent; while (*txt) print_word(&pr, &txt); cmd_lines = pr.row + 1; /* Clear screen */ pr.print = 1; pr.col = pr.indent; pr.row = LINES-cmd_lines; pr.top = LINES-cmd_lines; pr.end = LINES; print_clear(&pr); /* Print cmdline */ txt = cmd_buf; mvprintw(pr.top, 0, "[%s]", name); while (txt < &cmd_buf[cmd_pos]) print_word(&pr, &txt); cmd_row = pr.row; cmd_col = pr.col - (txt - &cmd_buf[cmd_pos]); while (txt < &cmd_buf[cmd_len]) print_word(&pr, &txt); if (cmd_col < pr.indent) cmd_col = pr.indent; } /* Handle input */ static void process(int chr) { /* Window management */ if (chr == KEY_RESIZE) { clear(); } else if (chr == KEY_CTRL_L) { clear(); } else if (chr == KEY_CTRL_G) { view_draw(); } /* View management */ else if (chr == KEY_CTRL_N) { focus = next_window(focus) ?: focus; } else if (chr == KEY_CTRL_P) { focus = prev_window(focus) ?: focus; } else if (chr == KEY_CTRL_X) { cycle_channel(); } else if (chr == KEY_CTRL_Y) { focus->scroll -= 1; } else if (chr == KEY_CTRL_E) { focus->scroll += 1; } else if (chr == KEY_CTRL_U) { focus->scroll -= (LINES-3)/2; } else if (chr == KEY_CTRL_D) { focus->scroll += (LINES-3)/2; } else if (chr == KEY_PPAGE) { focus->scroll -= (LINES-3)-1; } else if (chr == KEY_NPAGE) { focus->scroll += (LINES-3)-1; } /* Cmdline Input */ else if (chr == KEY_RETURN) { cmd_buf[cmd_len] = '\0'; if (send_command(cmd_buf)) { cmd_pos = 0; cmd_len = 0; } } else if (chr == KEY_TAB) { if (cmd_pos == 0) last_channel(); else debug("todo"); } else if (chr == KEY_ESCAPE) { cmd_pos = 0; cmd_len = 0; } else if (chr == KEY_LEFT) { if (cmd_pos > 0) cmd_pos--; } else if (chr == KEY_RIGHT) { if (cmd_pos < cmd_len) cmd_pos++; } else if (chr == KEY_BACKSPACE) { if (cmd_pos > 0) { memmove(&cmd_buf[cmd_pos-1], &cmd_buf[cmd_pos], (cmd_len-cmd_pos)+1); cmd_pos--; cmd_len--; } } else if (chr == KEY_DC) { if (cmd_pos < cmd_len) { memmove(&cmd_buf[cmd_pos], &cmd_buf[cmd_pos+1], (cmd_len-cmd_pos)+1); cmd_len--; } } else if (isprint(chr)) { if (cmd_len+2 < sizeof(cmd_buf)) { memmove(&cmd_buf[cmd_pos+1], &cmd_buf[cmd_pos], (cmd_len-cmd_pos)+1); cmd_buf[cmd_pos] = chr; cmd_pos++; cmd_len++; } else { debug("form: out of space"); } } /* Unknown control character */ else { debug("main: Unhandled key - Dec %3d, Hex %02x, Oct %03o, Chr <%c>", chr, chr, chr, chr); } } /* View init */ void view_init(void) { /* System windows */ sys_srv.name = strcopy("system"); sys_srv.protocol = -1; sys_chan.name = strcopy("system"); sys_chan.server = &sys_srv; sys_win.name = strcopy("system"); sys_win.dest = &sys_chan; sys_win.channel = &sys_chan; sys_win.pinned = -1; sys_win.next = windows; windows = focus = &sys_win; /* Setup windows */ for (window_t *win = windows; win; win = win->next) if (win->filter) regcomp(&win->regex, win->filter, RE_FLAGS); /* Print welcome message */ chat_recv(&sys_chan, NULL, "Welcome to lamechat!"); if (sys_win.next) { update_windows(); sys_win.hide = 1; } /* Set default escape timeout */ if (!getenv("ESCDELAY")) putenv("ESCDELAY=25"); /* Setup Curses */ setlocale(LC_ALL, ""); initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); start_color(); timeout(0); use_default_colors(); color_title = color(COLOR_WHITE, COLOR_BLUE); color_date = color(COLOR_BROWN, -1); color_error = color(COLOR_RED, -1); /* Create signal FD */ sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGWINCH); if ((sig_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0) error("creating signal fd"); /* Register callback */ poll_add(&poll_in, 0, (cb_t)view_sync, NULL); poll_ctl(&poll_in, 1, 0, 1); poll_add(&poll_sig, sig_fd, (cb_t)view_sync, NULL); poll_ctl(&poll_sig, 1, 0, 1); /* Set running */ running = 1; /* Draw initial view */ view_draw(); view_sync(); } /* Config parser */ void view_config(const char *group, const char *name, const char *key, const char *value) { window_t *win; if (match(group, "general")) { if (match(key, "theme")) theme = get_map(value, themes); else if (match(key, "defocus")) defocus = get_number(value); } if (match(group, "window")) { win = find_window(name); if (match(key, "")) { win = add_window(&sys_chan); strset(&win->name, get_name(name)); } else if (match(key, "server")) { win->server = find_server(get_string(value)); } else if (match(key, "channel")) { win->channel = find_channel(get_string(value)); win->dest = win->channel ?: win->dest; } else if (match(key, "filter")) { win->filter = get_string(value); } } } /* View event */ void view_sync(void) { int chr; while ((chr = getch()) != ERR) { process(chr); deadline = time(NULL) + defocus; draw = 1; } if (defocus && time(NULL) > deadline && focus->dest != &sys_chan) { focus->saved = focus->dest; focus->dest = &sys_chan; draw = 1; } if (draw) { debug("view: flush draw"); update_windows(); draw_cmdline(); draw_status(); draw_header(); draw_chat(); move(cmd_row, cmd_col); refresh(); draw = 0; } } void view_draw(void) { debug("view: queue draw"); draw = 1; } void view_exit(void) { if (!running) return; endwin(); }