/* * 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 "util.h" #include "conf.h" #include "chat.h" #include "view.h" /* View constants */ #define MATCHES 16 /* Extra colors */ #define COLOR_BROWN 130 /* Extra keys */ #define KEY_CTRL_D '\4' #define KEY_CTRL_E '\5' #define KEY_CTRL_F '\6' #define KEY_CTRL_G '\7' #define KEY_CTRL_L '\14' #define KEY_CTRL_N '\16' #define KEY_CTRL_P '\20' #define KEY_CTRL_T '\24' #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' #define KEY_DELETE '\177' /* View types */ typedef struct window_t window_t; typedef struct watch_t watch_t; typedef struct window_t { char *name; int hide; int flag; int scroll; int pinned; int custom; reg_t filter; server_t *server; channel_t *channel; channel_t *dest; channel_t *saved; window_t *next; } window_t; typedef struct watch_t { char *name; char desc[512]; reg_t regex; short color; watch_t *next; } watch_t; typedef struct { const char *value; const char *desc; } item_t; typedef struct { int top; int end; int row; int col; int indent; int margin; int limit; int print; int spell; int pick; int prow; int pcol; } printer_t; typedef enum { VIEW_CHAT, VIEW_MENU, } view_t; typedef enum { THEME_NORMAL, THEME_DARK, THEME_LIGHT, } theme_t; typedef enum { ABBREV_NONE, ABBREV_FIRST, ABBREV_LAST, ABBREV_FLAST, ABBREV_INITIALS, } abbrev_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 abbrev; static int full; static int date; static int draw; static int view; static char cmd_buf[4096]; static int cmd_pos; static int cmd_len; static int cmd_row; static int cmd_col; static buf_t item_buf; static int item_idx; static int item_pos; static int item_scroll; static char save_buf[4096]; static int save_pos; static int save_len; 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 watch_t *watches; static int ncolors = 1; static short colors[256+1][256+1]; static short color_title; static short color_status; static short color_date; static short color_spell; static const char *themes[][2] = { [THEME_NORMAL] { "normal", "Default theme" }, [THEME_DARK] { "dark", "Dark background" }, [THEME_LIGHT] { "light", "Light background" }, }; static const char *abbrevs[][2] = { [ABBREV_NONE] { "none", "Do not abbreviate" }, [ABBREV_FIRST] { "first", "First name only" }, [ABBREV_LAST] { "last", "Last name only" }, [ABBREV_FLAST] { "flast", "First initial last name" }, [ABBREV_INITIALS] { "initials", "Initials only" }, }; /* 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 void abbreviate(char **dst, const char *name) { char first[32]; char last[32]; char flast[32]; char initials[32]; int max = 32, fi = 0, fli = 0, li = 0, ii = 0; int word = 0, space = 1; if (!name) return; for (int i = 0; name[i]; i++) { if (name[i] == ' ' || name[i] == '-') { space = 1; word++; continue; } if (word == 0 && fi < max) first[fi++] = tolower(name[i]); if (word > 0 && li < max) last[li++] = tolower(name[i]); if (((space && word == 0) || word > 0) && fli < max) flast[fli++] = tolower(name[i]); if (space && ii < max) initials[ii++] = tolower(name[i]); if (space) space = 0; } first[fi] = '\0'; last[li] = '\0'; flast[fli] = '\0'; initials[ii] = '\0'; switch (abbrev) { case ABBREV_NONE: strset(dst, name); break; case ABBREV_FIRST: strset(dst, first); break; case ABBREV_LAST: strset(dst, li?last:first); break; case ABBREV_FLAST: strset(dst, li?flast:first); break; case ABBREV_INITIALS: strset(dst, initials); break; } } 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 rgb_color(int r, int g, int b) { return MIN(r*6/0x100, 5)*6*6 + MIN(g*6/0x100, 5)*6 + MIN(b*6/0x100, 5) + 16; } 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 = rgb_color(r, g, b); //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 (!reg_match(&win->filter, msg->channel->name)) return 0; return 1; } static window_t *add_window(channel_t *dest) { window_t *win = new0(window_t); win->dest = dest; win->saved = dest; win->pinned = -1; abbreviate(&win->name, dest->name); 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 channel_t *cycle_channel(window_t *win) { /* 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, win)) continue; if (cur) next = msg->channel; if (msg->channel == win->dest) cur = msg->channel; if (!first) first = msg->channel; } return next ?: first ?: &sys_chan; } static channel_t *last_channel(window_t *win) { if (win->dest == &sys_chan && win->saved != &sys_chan) return win->saved; for (int i = history-1; i>=0; i--) if (in_window(&messages[i], win)) return messages[i].channel; return win->dest; } static void update_windows(void) { static int seen = 0; /* Update focus */ if (focus->hide) { focus = next_window(focus) ?: prev_window(focus) ?: &sys_win; focus->hide = 0; } /* Update names */ for (window_t *cur = windows; cur; cur = cur->next) if (cur->channel && !cur->custom) abbreviate(&cur->name, cur->channel->name); /* Process new messages */ for (; seen < history; seen++) { message_t *msg = &messages[seen]; channel_t *chan = msg->channel; window_t *prime = NULL; int found = 0; /* Skip focused messages */ if (in_window(msg, focus)) continue; /* Flag existing windows */ for (window_t *cur = windows; cur; cur = cur->next) { if (!in_window(msg, cur)) continue; if (cur->channel == chan) prime = cur; if (!cur->hide) found = 1; cur->flag += 1; } /* No window, create a new one */ if (!found) { if (prime) { prime->hide = 0; prime->flag = 1; } else { prime = add_window(chan); prime->channel = chan; prime->flag = 1; } } } } 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; } /* Watch functions */ static watch_t *add_watch(const char *name) { watch_t *watch = new0(watch_t); strset(&watch->name, name); watch_t **last = &watches; while (*last) last = &(*last)->next; *last = watch; return watch; } static watch_t *find_watch(const char *name) { for (watch_t *watch = watches; watch; watch = watch->next) if (match(watch->name, name)) return watch; return NULL; } static void set_watch(const char *text) { static char name[64]; static char regex[512]; static int color; sscanf(text, "%s %d %[^\n]", name, &color, regex); watch_t *watch = find_watch(name); if (!watch) watch = add_watch(name); reg_set(&watch->regex, regex, MATCHES); watch->color = color; } static void watch_run(const char *text) { for (watch_t *watch = watches; watch; watch = watch->next) reg_match(&watch->regex, text); } static void watch_end(void) { for (watch_t *watch = watches; watch; watch = watch->next) watch->regex.count = 0; } static void watch_on(int pos) { for (watch_t *watch = watches; watch; watch = watch->next) if (reg_check(&watch->regex, pos) & MATCH_START) attron(color(watch->color, -1) | A_BOLD); } static void watch_off(int pos) { for (watch_t *watch = watches; watch; watch = watch->next) if (reg_check(&watch->regex, pos) & MATCH_STOP) attroff(color(watch->color, -1) | A_BOLD); } /* Print functions */ static int word_wrap(printer_t *pr, const char *txt, const char **l, const char **r) { /* In previous word? */ if (txt > *l && txt < *r) return 0; /* Find left and right ends of the word */ *l = txt; while (*txt != '\0' && *txt != ' ' && *txt != '\t' && *txt != '\r' && *txt != '\n') txt++; *r = txt; /* End goes past the margin? */ int len = *r - *l; int edge = COLS - pr->margin; if (pr->col + len > edge) { if (pr->indent + len > edge) *r = *l + (edge - pr->indent); return pr->col != pr->indent; } return 0; } static int irc_number(const char *txt, int *i) { int n = 0; int r = 0; char a = txt[*i]; char b = a ? txt[(*i)+1] : 0; if ('0' == a) n = ('0' <= b && b <= '9') ? 2 : 1; if ('1' == a) n = ('0' <= b && b <= '5') ? 2 : 1; if ('2' <= a && a <= '9') n = 1; if (n == 0) return -1; for (int j = 0; j < n; j++) r = r*10 + txt[(*i)++]-'0'; return r; } static int irc_color(const char *txt, int *i) { static int color_map[16][3] = { [0x0] { 0xFF, 0xFF, 0xFF }, // White [0x1] { 0x00, 0x00, 0x00 }, // Black [0x2] { 0x00, 0x00, 0x80 }, // Navy Blue [0x3] { 0x00, 0x80, 0x00 }, // Green [0x4] { 0xFF, 0x00, 0x00 }, // Red [0x5] { 0x80, 0x40, 0x40 }, // Brown [0x6] { 0x80, 0x00, 0xFF }, // Purple [0x7] { 0x80, 0x80, 0x00 }, // Olive [0x8] { 0xFF, 0xFF, 0x00 }, // Yellow [0x9] { 0x00, 0xFF, 0x00 }, // Lime Green [0xA] { 0x00, 0x80, 0x80 }, // Teal [0xB] { 0x00, 0xFF, 0xFF }, // Aqua Light [0xC] { 0x00, 0x00, 0xFF }, // Royal Blue [0xD] { 0xFF, 0x00, 0xFF }, // Hot Pink [0xE] { 0x80, 0x80, 0x80 }, // Dark Gray [0xF] { 0xC0, 0xC0, 0xC0 }, // Light Gray }; int attr = 0; int reset = 0; int fg = -1; int bg = -1; switch (txt[*i]) { // Format attributes case '\002': attr = A_BOLD; break; // bold case '\011': attr = A_ITALIC; break; // italic case '\023': attr = A_UNDERLINE; break; // strike case '\025': attr = A_UNDERLINE; break; // underline case '\037': attr = A_UNDERLINE; break; // underline case '\026': attr = A_REVERSE; break; // reverse // Reset case '\017': reset = 1; break; // reset // Colors case '\003': case '\013': (*i)++; fg = irc_number(txt, i); if (txt[*i] == ',') { (*i)++; bg = irc_number(txt, i); if (bg == -1) (*i)--; } (*i)--; break; // Not IRC Colors default: return 0; } if (reset) { attrset(0); } else if (attr){ attr_t tmp = 0; short pair = 0; attr_get(&tmp, &pair, 0); attr_set(tmp^attr, pair, 0); } else if (fg != -1 || bg != -1) { if (0 <= fg && fg <= 15) fg = rgb_color(color_map[fg][0], color_map[fg][1], color_map[fg][2]); if (0 <= bg && bg <= 15) bg = rgb_color(color_map[bg][0], color_map[bg][1], color_map[bg][2]); attron(color(fg, bg)); } return 1; } static void print_string(printer_t *pr, const char *txt) { int i, n; int li = 0; wchar_t wc; const char *l = 0, *r = 0; for (i = 0; txt[i]; i++) { if (word_wrap(pr, &txt[i], &l, &r)) { pr->row += 1; pr->col = pr->indent; } if (i == pr->pick) { pr->prow = pr->row; pr->pcol = pr->col; } watch_on(i); if (irc_color(txt, &i)) continue; if (pr->spell && (i == 0 || txt[i-1] == ' ')) if (spell_check(&txt[i], NULL)) attron(color_spell); switch (txt[i]) { case ' ': pr->col += 1; break; case '\t': pr->col -= pr->indent; pr->col /= 8; pr->col += 1; pr->col *= 8; pr->col += pr->indent; break; case '\r': case '\n': pr->row += 1; pr->col = pr->indent; if (pr->limit && li++ > pr->limit) txt = "\n\011[continued]\017" - i; break; default: if ((n = mbtowc(&wc, &txt[i], 1)) == -1) break; i += n-1; if (pr->print && pr->row >= pr->top && pr->row < pr->end) mvaddnwstr(pr->row, pr->col, &wc, 1); pr->col += 1; break; } if (pr->spell && (txt[i] == ' ' || !txt[i+1])) attroff(color_spell); watch_off(i); } if (i <= pr->pick) { pr->prow = pr->row; pr->pcol = pr->col; } } 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); print_string(pr, buf); } static void print_message(printer_t *pr, message_t *msg) { if (!in_window(msg, focus)) return; time_t timep = msg->when; struct tm *tm = localtime(&timep); /* Set line limit */ pr->limit = full ? 0 : 5; /* Start new line */ pr->col = 0; pr->indent = 0; /* Print time */ attron(color_date); if (date) print_format(pr, "%04d-%02d-%02d %02d:%02d ", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min); else print_format(pr, "%02d:%02d ", tm->tm_hour, tm->tm_min); attroff(color_date); /* Print channel */ if (!focus->channel) { attron(str_color(msg->channel->name) | A_BOLD); print_format(pr, "[%s] ", msg->channel->name); attroff(str_color(msg->channel->name) | A_BOLD); } /* Print from */ if (msg->from) { attron(str_color(msg->from->name)); print_format(pr, "%s: ", msg->from->name); attroff(str_color(msg->from->name)); } else { print_format(pr, "*** "); } /* Set matches */ watch_run(msg->text); /* Print message */ pr->indent = pr->col; print_string(pr, msg->text); pr->row++; /* Clear matches */ watch_end(); } 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->channel && cur->flag) print_format(pr, "[%s:%d]", cur->name, cur->flag); else if (!cur->channel) print_format(pr, "[%s]", cur->name); else 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; } } /* Menu items */ static void add_item(const char *prefix, const char *value, const char *desc) { item_t item = { .value = value, .desc = desc, }; append(&item_buf, (void*)&item, sizeof(item)); if (prefix) item_pos = prefix - cmd_buf; } static int num_items(void) { return item_buf.len / sizeof(item_t); } static item_t *get_item(int i) { if (i < 0) return NULL; if (i >= item_buf.len/sizeof(item_t)) return NULL; return (item_t*)&item_buf.data[i*sizeof(item_t)]; } static int compare_items(const void *_a, const void *_b) { const item_t *a = _a; const item_t *b = _b; return compare(a->value, b->value) ?: compare(a->desc, b->desc); } static void sort_items(void) { if (!item_buf.len) return; qsort(item_buf.data, num_items(), sizeof(item_t), compare_items); item_t *dst = (item_t*)item_buf.data; item_t *src = (item_t*)item_buf.data; for (int i = 0; i < num_items(); i++, src++) if (compare_items(src, dst)) *++dst = *src; item_buf.len = (char*)(dst+1) - item_buf.data; } /* Commands */ static void send_complete(const char *text) { const char *arg; if (prefix(text, "/theme ", &arg)) { complete_array(arg, themes, N_ELEMENTS(themes)); } else if (prefix(text, "/abbrev ", &arg)) { complete_array(arg, abbrevs, N_ELEMENTS(abbrevs)); } else if (prefix(text, "/server ", &arg)) { complete_server(arg); } else if (prefix(text, "/channel ", &arg)) { complete_channel(arg); } else if (prefix(text, "/watch ", &arg)) { complete_watch(arg); } else { complete_args(text, "/quit", "Quit the program", "/sort", "Sort chat history", "/theme ", "Change the theme", "/abbrev ", "Abbreviate names", "/watch ", "Watch for regex", "/open", "Open window", "/close", "Close window", "/name ", "Set window name", "/server ", "Set window server", "/channel ", "Set window channel", "/filter ", "Set window filter", NULL); } chat_complete(last_channel(focus), text); } 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][0], arg)) theme = i; } else if (prefix(text, "/abbrev", &arg)) { for (int i = 0; i < N_ELEMENTS(abbrevs); i++) if (match(abbrevs[i][0], arg)) abbrev = i; } else if (prefix(text, "/watch", &arg) && arg) { set_watch(arg); } else if (prefix(text, "/open", &arg)) { win = find_window(arg ?: 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) { focus->custom = 1; if (match(focus->name, arg) && focus->dest && focus->dest != &sys_chan) strset(&focus->dest->name, arg); else 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)) { reg_set(&focus->filter, arg, 0); } 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 = 3, }; 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_menu(void) { int top = hdr_lines; int end = LINES-cmd_lines-sts_lines; int num = end - top; /* Scroll up / down */ int select = top + (item_idx - item_scroll); int scroll = select < top ? select - top : select >= end ? select - (end-1) : 0; item_scroll += scroll; select -= scroll; /* Compute column size */ int width0 = 0; for (int i = 0; i < num_items(); i++) width0 = MAX(width0, strlen(get_item(i)->value)); width0 = MIN(width0, 32); int width1 = COLS - (3+width0+3); /* Draw lines */ for (int i = 0; i < num; i++) { int line = i + top; item_t *item = get_item(i + item_scroll); if (item && line == select) { attron(A_REVERSE); mvprintw(line, 0, " > %-*.*s | %-*.*s", width0, width0, item->value, width1, width1, item->desc); attroff(A_REVERSE); } else if (item) { mvprintw(line, 0, " %-*.*s | %-*.*s", width0, width0, item->value, width1, width1, item->desc); } else { mvhline(line, 0, ' ', COLS); } } } static void draw_status(void) { /* Compute lines */ printer_t pr = {}; print_status(&pr); sts_lines = pr.row + 1; /* Draw status */ attron(color_status); 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_status); } static void draw_cmdline(void) { const char *name = focus->dest->name; printer_t pr = { .indent = 1 + strlen(name) + 2, .margin = 1, }; /* Terminate buffer */ cmd_buf[cmd_len] = '\0'; /* Compute lines */ pr.col = pr.indent; print_string(&pr, cmd_buf); 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 */ mvprintw(pr.top, 0, "[%s]", name); pr.spell = 1; pr.pick = cmd_pos; print_string(&pr, cmd_buf); cmd_row = pr.prow; cmd_col = pr.pcol; } /* Command line editing */ static void insert(const char *buf, int len) { if (cmd_len+len+1 < sizeof(cmd_buf)) { memmove(&cmd_buf[cmd_pos+len], &cmd_buf[cmd_pos], (cmd_len-cmd_pos)); memcpy(&cmd_buf[cmd_pos], buf, len); cmd_pos += len; cmd_len += len; } else { debug("form: out of space"); } } static void insert_mb(wint_t chr) { char buf[MB_CUR_MAX]; int n = wctomb(buf, chr); insert(buf, n); } static void delete(int before, int after) { if (before > 0 && (cmd_pos-before) >= 0) { memmove(&cmd_buf[cmd_pos-before], &cmd_buf[cmd_pos], (cmd_len-cmd_pos)+before); cmd_len -= before; cmd_pos -= before; } if (after > 0 && (cmd_pos+after) <= cmd_len) { memmove(&cmd_buf[cmd_pos], &cmd_buf[cmd_pos+after], (cmd_len-cmd_pos)+after); cmd_len -= after; } } /* Tab completion */ static void show_completion(void) { debug("complete: show"); /* Apply completion */ const char *pick = get_item(item_idx)->value; const char *save = &save_buf[save_pos]; cmd_pos = item_pos; cmd_len = item_pos; insert(pick, strlen(pick)); insert(save, save_len-save_pos); cmd_pos -= save_len-save_pos; } static void start_completion(void) { debug("complete: start"); /* Reset item buffers */ reset(&item_buf); item_idx = 0; item_pos = cmd_pos; item_scroll = 0; /* Run completion */ char tmp = cmd_buf[cmd_pos]; cmd_buf[cmd_pos] = '\0'; send_complete(cmd_buf); cmd_buf[cmd_pos] = tmp; /* Sort matches */ sort_items(); /* Search for matches */ int num = num_items(); if (num == 0) { beep(); } else { memcpy(save_buf, cmd_buf, cmd_len); save_pos = cmd_pos; save_len = cmd_len; show_completion(); } /* Switch to menu */ if (num == 1) { debug("complete: one"); if (cmd_buf[cmd_pos-1] != ' ') insert(" ", 1); } else if (num > 1) { debug("complete: menu"); view = VIEW_MENU; } } /* Input handling */ static void input_chat(wint_t 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) { focus->dest = cycle_channel(focus); } 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; } else if (chr == KEY_CTRL_F) { full ^= 1; } else if (chr == KEY_CTRL_T) { date ^= 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) focus->dest = last_channel(focus); else start_completion(); } 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_UP) { /* Todo */ } else if (chr == KEY_DOWN) { /* Todo */ } else if (chr == KEY_HOME) { cmd_pos = 0; } else if (chr == KEY_END) { cmd_pos = cmd_len; } else if (chr == KEY_BACKSPACE || chr == KEY_DELETE) { delete(1, 0); } else if (chr == KEY_DC) { delete(0, 1); } else if (iswprint(chr)) { insert_mb(chr); } /* Unknown control character */ else { debug("main: Unhandled key - Dec %3d, Hex %02x, Oct %03o, Chr <%c>", chr, chr, chr, chr); } } static void input_menu(wint_t chr) { if (chr == KEY_CTRL_N || chr == KEY_DOWN) { if (item_idx+1 < num_items()) item_idx++; show_completion(); } else if (chr == KEY_CTRL_P || chr == KEY_UP) { if (item_idx > 0) item_idx--; show_completion(); } else if (chr == KEY_TAB) { start_completion(); } else if (chr == KEY_ESCAPE) { debug("complete: escape"); memcpy(cmd_buf, save_buf, save_len); cmd_pos = save_pos; cmd_len = save_len; view = VIEW_CHAT; } else { debug("complete: accept"); view = VIEW_CHAT; input_chat(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; /* Print welcome message */ notice("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, "en_US.utf8"); initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); start_color(); timeout(0); use_default_colors(); color_title = color(COLOR_WHITE, COLOR_BLUE); color_status = color(COLOR_WHITE, COLOR_BLUE); color_date = color(COLOR_BROWN, -1); color_spell = color(-1, COLOR_RED); /* 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; watch_t *watch; if (match(group, "general")) { if (match(key, "theme")) theme = get_mapv(value, themes); else if (match(key, "abbrev")) abbrev = get_mapv(value, abbrevs); 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); win->custom = 1; 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")) { reg_set(&win->filter, get_string(value), 0); } } if (match(group, "watch")) { watch = find_watch(name); if (match(key, "")) { watch = add_watch(get_name(name)); } else if (match(key, "regex")) { reg_set(&watch->regex, get_string(value), MATCHES); } else if (match(key, "color")) { watch->color = get_number(value); } } } void view_message(const char *prefix, const char *msg) { if (match(prefix, "notice")) chat_recv(&sys_chan, NULL, msg); } /* View event */ void view_sync(void) { wint_t wch = 0; while (get_wch(&wch) != ERR) { if (view == VIEW_CHAT) input_chat(wch); else if (view == VIEW_MENU) input_menu(wch); 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(); if (view == VIEW_CHAT) draw_chat(); else if (view == VIEW_MENU) draw_menu(); 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(); } /* Completion */ void complete_item(const char *prefix, const char *value, const char *desc) { add_item(prefix, value, desc ?: ""); } void complete_user(const char *prefix) { for (user_t *cur = users; cur; cur = cur->next) { user_t *alias = cur; while (alias->alias) alias = alias->alias; if (starts(prefix, cur->name) || starts(prefix, alias->name)) add_item(prefix, alias->name, cur->name); } } void complete_channel(const char *prefix) { for (channel_t *cur = channels; cur; cur = cur->next) if (starts(prefix, cur->name)) add_item(prefix, cur->name, cur->topic); } void complete_server(const char *prefix) { for (server_t *cur = servers; cur; cur = cur->next) if (starts(prefix, cur->name)) add_item(prefix, cur->name, NULL); } void complete_watch(const char *prefix) { for (watch_t *cur = watches; cur; cur = cur->next) { if (starts(prefix, cur->name)) { snprintf(cur->desc, sizeof(cur->desc), "%s %hd %s", cur->name, cur->color, cur->regex.pattern); add_item(prefix, cur->desc, NULL); } } } void complete_array(const char *prefix, const char *list[][2], int n) { for (int i = 0; i < n; i++) if (starts(prefix, list[i][0])) add_item(prefix, list[i][0], list[i][1]); } void complete_args(const char *prefix, ...) { const char *value, *desc; va_list ap; va_start(ap, prefix); while (1) { value = va_arg(ap, const char *); if (!value) break; desc = va_arg(ap, const char *); if (!starts(prefix, value)) continue; add_item(prefix, value, desc); } va_end(ap); }