summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJune McEnroe2022-02-19 18:28:45 -0500
committerJune McEnroe2022-02-19 18:28:45 -0500
commit3359a5d69b0fe3c08812f7db83e27958ffec820f (patch)
treef3a358df92531a55880e0d5c793b6ed963e76920
parent70f627bc47e944a63f32fcf60db73913845772c6 (diff)
Factor out window management to window.c
-rw-r--r--Makefile1
-rw-r--r--README.74
-rw-r--r--chat.c8
-rw-r--r--chat.h74
-rw-r--r--command.c22
-rw-r--r--handle.c2
-rw-r--r--ui.c735
-rw-r--r--window.c656
8 files changed, 828 insertions, 674 deletions
diff --git a/Makefile b/Makefile
index fd06260..dffe4e8 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,7 @@ OBJS += irc.o
OBJS += log.o
OBJS += ui.o
OBJS += url.o
+OBJS += window.o
OBJS += xdg.o
TESTS += edit.t
diff --git a/README.7 b/README.7
index 8f13f7c..b98b589 100644
--- a/README.7
+++ b/README.7
@@ -1,5 +1,5 @@
.\" To view this file, run: man ./README.7
-.Dd February 12, 2022
+.Dd February 19, 2022
.Dt README 7
.Os "Causal Agency"
.
@@ -181,6 +181,8 @@ startup and event loop
IRC connection and parsing
.It Pa ui.c
curses interface
+.It Pa window.c
+window management
.It Pa handle.c
IRC message handling
.It Pa command.c
diff --git a/chat.c b/chat.c
index 454ae31..57cae6d 100644
--- a/chat.c
+++ b/chat.c
@@ -293,8 +293,8 @@ int main(int argc, char *argv[]) {
break; case 'R': self.restricted = true;
break; case 'S': bind = optarg;
break; case 'T': {
- uiTime.enable = true;
- if (optarg) uiTime.format = optarg;
+ windowTime.enable = true;
+ if (optarg) windowTime.format = optarg;
}
break; case 'a': sasl = true; parsePlain(optarg);
break; case 'c': cert = optarg;
@@ -309,7 +309,7 @@ int main(int argc, char *argv[]) {
break; case 'n': nick = optarg;
break; case 'o': printCert = true;
break; case 'p': port = optarg;
- break; case 'q': uiThreshold = Warm;
+ break; case 'q': windowThreshold = Warm;
break; case 'r': real = optarg;
break; case 's': save = optarg;
break; case 't': trust = optarg;
@@ -381,7 +381,7 @@ int main(int argc, char *argv[]) {
uiLoad(save);
atexit(exitSave);
}
- uiShowID(Network);
+ windowShow(windowFor(Network));
uiFormat(
Network, Cold, NULL,
"\3%dcatgirl\3\tis GPLv3 fwee softwawe ^w^ "
diff --git a/chat.h b/chat.h
index 8a9a48f..e9a2926 100644
--- a/chat.h
+++ b/chat.h
@@ -293,26 +293,34 @@ const char *commandIsAction(uint id, const char *input);
size_t commandWillSplit(uint id, const char *input);
void commandCompleteAdd(void);
-enum Heat { Ice, Cold, Warm, Hot };
-enum { TimeCap = 64 };
-extern enum Heat uiThreshold;
-extern struct Time {
- bool enable;
- const char *format;
- int width;
-} uiTime;
+enum Heat {
+ Ice,
+ Cold,
+ Warm,
+ Hot,
+};
+
+enum {
+ TitleCap = 256,
+ StatusLines = 1,
+ MarkerLines = 1,
+ SplitLines = 5,
+ InputLines = 1,
+ InputCols = 1024,
+};
+extern char uiTitle[TitleCap];
+extern struct _win_st *uiStatus;
+extern struct _win_st *uiMain;
extern struct Util uiNotifyUtil;
void uiInitEarly(void);
void uiInitLate(void);
+uint uiAttr(struct Style style);
+short uiPair(struct Style style);
+void uiUpdate(void);
void uiShow(void);
void uiHide(void);
+void uiWait(void);
void uiDraw(void);
-void uiWindows(void);
-void uiShowID(uint id);
-void uiShowNum(uint num);
-void uiMoveID(uint id, uint num);
-void uiCloseID(uint id);
-void uiCloseNum(uint id);
void uiRead(void);
void uiWrite(uint id, enum Heat heat, const time_t *time, const char *str);
void uiFormat(
@@ -321,6 +329,44 @@ void uiFormat(
void uiLoad(const char *name);
int uiSave(void);
+enum Scroll {
+ ScrollOne,
+ ScrollPage,
+ ScrollAll,
+ ScrollUnread,
+ ScrollHot,
+};
+extern struct Time {
+ bool enable;
+ const char *format;
+ int width;
+} windowTime;
+extern enum Heat windowThreshold;
+void windowInit(void);
+void windowUpdate(void);
+void windowResize(void);
+bool windowWrite(uint id, enum Heat heat, const time_t *time, const char *str);
+void windowBare(void);
+uint windowID(void);
+uint windowNum(void);
+uint windowFor(uint id);
+void windowShow(uint num);
+void windowAuto(void);
+void windowSwap(void);
+void windowMove(uint from, uint to);
+void windowClose(uint num);
+void windowList(void);
+void windowMark(void);
+void windowUnmark(void);
+void windowToggleMute(void);
+void windowToggleTime(void);
+void windowToggleThresh(int n);
+bool windowTimeEnable(void);
+void windowScroll(enum Scroll by, int n);
+void windowSearch(const char *str, int dir);
+int windowSave(FILE *file);
+void windowLoad(FILE *file, size_t version);
+
enum { BufferCap = 1024 };
struct Buffer;
struct Line {
diff --git a/command.c b/command.c
index 335c396..a127af3 100644
--- a/command.c
+++ b/command.c
@@ -144,7 +144,7 @@ static void commandMsg(uint id, char *params) {
if (params) {
splitMessage("PRIVMSG", msg, params);
} else {
- uiShowID(msg);
+ windowShow(windowFor(msg));
}
}
@@ -376,25 +376,25 @@ static void commandQuery(uint id, char *params) {
if (idColors[query] == Default) {
idColors[query] = completeColor(id, params);
}
- uiShowID(query);
+ windowShow(windowFor(query));
}
static void commandWindow(uint id, char *params) {
if (!params) {
- uiWindows();
+ windowList();
} else if (isdigit(params[0])) {
- uiShowNum(strtoul(params, NULL, 10));
+ windowShow(strtoul(params, NULL, 10));
} else {
id = idFind(params);
if (id) {
- uiShowID(id);
+ windowShow(windowFor(id));
return;
}
for (const char *match; (match = completeSubstr(None, params));) {
id = idFind(match);
if (!id) continue;
completeAccept();
- uiShowID(id);
+ windowShow(windowFor(id));
break;
}
}
@@ -405,20 +405,20 @@ static void commandMove(uint id, char *params) {
char *name = strsep(&params, " ");
if (params) {
id = idFind(name);
- if (id) uiMoveID(id, strtoul(params, NULL, 10));
+ if (id) windowMove(windowFor(id), strtoul(params, NULL, 10));
} else {
- uiMoveID(id, strtoul(name, NULL, 10));
+ windowMove(windowFor(id), strtoul(name, NULL, 10));
}
}
static void commandClose(uint id, char *params) {
if (!params) {
- uiCloseID(id);
+ windowClose(windowFor(id));
} else if (isdigit(params[0])) {
- uiCloseNum(strtoul(params, NULL, 10));
+ windowClose(strtoul(params, NULL, 10));
} else {
id = idFind(params);
- if (id) uiCloseID(id);
+ if (id) windowClose(windowFor(id));
}
}
diff --git a/handle.c b/handle.c
index d663ca1..f4cf68e 100644
--- a/handle.c
+++ b/handle.c
@@ -343,7 +343,7 @@ static void handleJoin(struct Message *msg) {
idColors[id] = hash(msg->params[0]);
completeTouch(None, msg->params[0], idColors[id]);
if (replies[ReplyJoin]) {
- uiShowID(id);
+ windowShow(windowFor(id));
replies[ReplyJoin]--;
}
}
diff --git a/ui.c b/ui.c
index 212d205..85b5a72 100644
--- a/ui.c
+++ b/ui.c
@@ -33,15 +33,15 @@
#include <err.h>
#include <errno.h>
#include <fcntl.h>
-#include <limits.h>
+#include <inttypes.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sysexits.h>
#include <sys/file.h>
+#include <sysexits.h>
#include <term.h>
#include <termios.h>
#include <time.h>
@@ -60,102 +60,14 @@
#undef lines
#undef tab
-enum {
- StatusLines = 1,
- MarkerLines = 1,
- SplitLines = 5,
- InputLines = 1,
- InputCols = 1024,
-};
-
#define BOTTOM (LINES - 1)
#define RIGHT (COLS - 1)
#define MAIN_LINES (LINES - StatusLines - InputLines)
-static WINDOW *status;
-static WINDOW *main;
+WINDOW *uiStatus;
+WINDOW *uiMain;
static WINDOW *input;
-struct Window {
- uint id;
- int scroll;
- bool mark;
- bool mute;
- bool time;
- enum Heat thresh;
- enum Heat heat;
- uint unreadSoft;
- uint unreadHard;
- uint unreadWarm;
- struct Buffer *buffer;
-};
-
-static struct {
- struct Window *ptrs[IDCap];
- uint len;
- uint show;
- uint swap;
- uint user;
-} windows;
-
-static uint windowPush(struct Window *window) {
- assert(windows.len < IDCap);
- windows.ptrs[windows.len] = window;
- return windows.len++;
-}
-
-static uint windowInsert(uint num, struct Window *window) {
- assert(windows.len < IDCap);
- assert(num <= windows.len);
- memmove(
- &windows.ptrs[num + 1],
- &windows.ptrs[num],
- sizeof(*windows.ptrs) * (windows.len - num)
- );
- windows.ptrs[num] = window;
- windows.len++;
- return num;
-}
-
-static struct Window *windowRemove(uint num) {
- assert(num < windows.len);
- struct Window *window = windows.ptrs[num];
- windows.len--;
- memmove(
- &windows.ptrs[num],
- &windows.ptrs[num + 1],
- sizeof(*windows.ptrs) * (windows.len - num)
- );
- return window;
-}
-
-enum Heat uiThreshold = Cold;
-
-static uint windowFor(uint id) {
- for (uint num = 0; num < windows.len; ++num) {
- if (windows.ptrs[num]->id == id) return num;
- }
- struct Window *window = calloc(1, sizeof(*window));
- if (!window) err(EX_OSERR, "malloc");
- window->id = id;
- window->mark = true;
- window->time = uiTime.enable;
- if (id == Network || id == Debug) {
- window->thresh = Cold;
- } else {
- window->thresh = uiThreshold;
- }
- window->buffer = bufferAlloc();
- completeAdd(None, idNames[id], idColors[id]);
- return windowPush(window);
-}
-
-static void windowFree(struct Window *window) {
- completeRemove(None, idNames[window->id]);
- bufferFree(window->buffer);
- free(window);
-}
-
static short colorPairs;
static void colorInit(void) {
@@ -240,8 +152,6 @@ enum {
static const char *FocusMode[2] = { "\33[?1004l", "\33[?1004h" };
static const char *PasteMode[2] = { "\33[?2004l", "\33[?2004h" };
-struct Time uiTime = { .format = "%X" };
-
static void errExit(void) {
putp(FocusMode[false]);
putp(PasteMode[false]);
@@ -271,30 +181,18 @@ void uiInitEarly(void) {
ENUM_KEY
#undef X
- status = newwin(StatusLines, COLS, 0, 0);
- if (!status) err(EX_OSERR, "newwin");
-
- main = newwin(MAIN_LINES, COLS, StatusLines, 0);
- if (!main) err(EX_OSERR, "newwin");
-
- int y;
- char fmt[TimeCap];
- char buf[TimeCap];
- styleStrip(fmt, sizeof(fmt), uiTime.format);
- struct tm *time = localtime(&(time_t) { -22100400 });
- size_t len = strftime(buf, sizeof(buf), fmt, time);
- if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", fmt);
- waddstr(main, buf);
- waddch(main, ' ');
- getyx(main, y, uiTime.width);
- (void)y;
+ uiStatus = newwin(StatusLines, COLS, 0, 0);
+ if (!uiStatus) err(EX_OSERR, "newwin");
+
+ uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0);
+ if (!uiMain) err(EX_OSERR, "newwin");
input = newpad(InputLines, InputCols);
if (!input) err(EX_OSERR, "newpad");
keypad(input, true);
nodelay(input, true);
- windowFor(Network);
+ windowInit();
uiShow();
}
@@ -323,13 +221,13 @@ void uiInitLate(void) {
static bool hidden = true;
static bool waiting;
-static char title[256];
-static char prevTitle[sizeof(title)];
+char uiTitle[TitleCap];
+static char prevTitle[TitleCap];
void uiDraw(void) {
if (hidden) return;
- wnoutrefresh(status);
- wnoutrefresh(main);
+ wnoutrefresh(uiStatus);
+ wnoutrefresh(uiMain);
int y, x;
getyx(input, y, x);
pnoutrefresh(
@@ -342,10 +240,10 @@ void uiDraw(void) {
doupdate();
if (!to_status_line) return;
- if (!strcmp(title, prevTitle)) return;
- strcpy(prevTitle, title);
+ if (!strcmp(uiTitle, prevTitle)) return;
+ strcpy(prevTitle, uiTitle);
putp(to_status_line);
- putp(title);
+ putp(uiTitle);
putp(from_status_line);
fflush(stdout);
}
@@ -377,7 +275,7 @@ static const short Colors[ColorCap] = {
16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
};
-static attr_t styleAttr(struct Style style) {
+uint uiAttr(struct Style style) {
attr_t attr = A_NORMAL;
if (style.attr & Bold) attr |= A_BOLD;
if (style.attr & Reverse) attr |= A_REVERSE;
@@ -388,96 +286,13 @@ static attr_t styleAttr(struct Style style) {
static bool spoilerReveal;
-static short stylePair(struct Style style) {
+short uiPair(struct Style style) {
if (spoilerReveal && style.fg == style.bg) {
return colorPair(Colors[Default], Colors[style.bg]);
}
return colorPair(Colors[style.fg], Colors[style.bg]);
}
-static int styleAdd(WINDOW *win, struct Style init, const char *str) {
- struct Style style = init;
- while (*str) {
- size_t len = styleParse(&style, &str);
- wattr_set(win, styleAttr(style), stylePair(style), NULL);
- if (waddnstr(win, str, len) == ERR)
- return -1;
- str += len;
- }
- return 0;
-}
-
-static void statusUpdate(void) {
- struct {
- uint unread;
- enum Heat heat;
- } others = { 0, Cold };
-
- wmove(status, 0, 0);
- for (uint num = 0; num < windows.len; ++num) {
- const struct Window *window = windows.ptrs[num];
- if (num != windows.show && !window->scroll) {
- if (window->heat < Warm) continue;
- if (window->mute && window->heat < Hot) continue;
- }
- if (num != windows.show) {
- others.unread += window->unreadWarm;
- if (window->heat > others.heat) others.heat = window->heat;
- }
- char buf[256], *end = &buf[sizeof(buf)];
- char *ptr = seprintf(
- buf, end, "\3%d%s %u%s%s %s ",
- idColors[window->id], (num == windows.show ? "\26" : ""),
- num, window->thresh[(const char *[]) { "-", "", "+", "++" }],
- &"="[!window->mute], idNames[window->id]
- );
- if (window->mark && window->unreadWarm) {
- ptr = seprintf(
- ptr, end, "\3%d+%d\3%d%s",
- (window->heat > Warm ? White : idColors[window->id]),
- window->unreadWarm, idColors[window->id],
- (window->scroll ? "" : " ")
- );
- }
- if (window->scroll) {
- ptr = seprintf(ptr, end, "~%d ", window->scroll);
- }
- if (styleAdd(status, StyleDefault, buf) < 0) break;
- }
- wclrtoeol(status);
-
- const struct Window *window = windows.ptrs[windows.show];
- char *end = &title[sizeof(title)];
- char *ptr = seprintf(
- title, end, "%s %s", network.name, idNames[window->id]
- );
- if (window->mark && window->unreadWarm) {
- ptr = seprintf(
- ptr, end, " +%d%s", window->unreadWarm, &"!"[window->heat < Hot]
- );
- }
- if (others.unread) {
- ptr = seprintf(
- ptr, end, " (+%d%s)", others.unread, &"!"[others.heat < Hot]
- );
- }
-}
-
-static void mark(struct Window *window) {
- if (window->scroll) return;
- window->mark = true;
- window->unreadSoft = 0;
- window->unreadWarm = 0;
-}
-
-static void unmark(struct Window *window) {
- if (!window->scroll) {
- window->mark = false;
- window->heat = Cold;
- }
- statusUpdate();
-}
-
void uiShow(void) {
if (!hidden) return;
prevTitle[0] = '\0';
@@ -485,86 +300,20 @@ void uiShow(void) {
putp(PasteMode[true]);
fflush(stdout);
hidden = false;
- unmark(windows.ptrs[windows.show]);
+ windowUnmark();
}
void uiHide(void) {
if (hidden) return;
- mark(windows.ptrs[windows.show]);
+ windowMark();
hidden = true;
putp(FocusMode[false]);
putp(PasteMode[false]);
endwin();
}
-static size_t windowTop(const struct Window *window) {
- size_t top = BufferCap - MAIN_LINES - window->scroll;
- if (window->scroll) top += MarkerLines;
- return top;
-}
-
-static size_t windowBottom(const struct Window *window) {
- size_t bottom = BufferCap - (window->scroll ?: 1);
- if (window->scroll) bottom -= SplitLines + MarkerLines;
- return bottom;
-}
-
-static int windowCols(const struct Window *window) {
- return COLS - (window->time ? uiTime.width : 0);
-}
-
-static void mainAdd(int y, bool time, const struct Line *line) {
- int ny, nx;
- wmove(main, y, 0);
- if (!line || !line->str[0]) {
- wclrtoeol(main);
- return;
- }
- if (time && line->time) {
- char buf[TimeCap];
- strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time));
- struct Style init = { .fg = Gray, .bg = Default };
- styleAdd(main, init, buf);
- waddch(main, ' ');
- } else if (time) {
- whline(main, ' ', uiTime.width);
- wmove(main, y, uiTime.width);
- }
- styleAdd(main, StyleDefault, line->str);
- getyx(main, ny, nx);
- if (ny != y) return;
- wclrtoeol(main);
- (void)nx;
-}
-
-static void mainUpdate(void) {
- struct Window *window = windows.ptrs[windows.show];
-
- int y = 0;
- int marker = MAIN_LINES - SplitLines - MarkerLines;
- for (size_t i = windowTop(window); i < BufferCap; ++i) {
- mainAdd(y++, window->time, bufferHard(window->buffer, i));
- if (window->scroll && y == marker) break;
- }
- if (!window->scroll) return;
-
- y = MAIN_LINES - SplitLines;
- for (size_t i = BufferCap - SplitLines; i < BufferCap; ++i) {
- mainAdd(y++, window->time, bufferHard(window->buffer, i));
- }
- wattr_set(main, A_NORMAL, 0, NULL);
- mvwhline(main, marker, 0, ACS_BULLET, COLS);
-}
-
-static void windowScroll(struct Window *window, int n) {
- mark(window);
- window->scroll += n;
- if (window->scroll > BufferCap - MAIN_LINES) {
- window->scroll = BufferCap - MAIN_LINES;
- }
- if (window->scroll < 0) window->scroll = 0;
- unmark(window);
- if (window == windows.ptrs[windows.show]) mainUpdate();
+void uiWait(void) {
+ waiting = true;
}
struct Util uiNotifyUtil;
@@ -593,36 +342,8 @@ static void notify(uint id, const char *str) {
}
void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) {
- struct Window *window = windows.ptrs[windowFor(id)];
- time_t ts = (src ? *src : time(NULL));
-
- if (heat >= window->thresh) {
- if (!window->unreadSoft++) window->unreadHard = 0;
- }
- if (window->mark && heat > Cold) {
- if (!window->unreadWarm++) {
- int lines = bufferPush(
- window->buffer, windowCols(window),
- window->thresh, Warm, ts, ""
- );
- if (window->scroll) windowScroll(window, lines);
- if (window->unreadSoft > 1) {
- window->unreadSoft++;
- window->unreadHard += lines;
- }
- }
- if (heat > window->heat) window->heat = heat;
- statusUpdate();
- }
- int lines = bufferPush(
- window->buffer, windowCols(window),
- window->thresh, heat, ts, str
- );
- window->unreadHard += lines;
- if (window->scroll) windowScroll(window, lines);
- if (window == windows.ptrs[windows.show]) mainUpdate();
-
- if (window->mark && heat > Warm) {
+ bool note = windowWrite(id, heat, src, str);
+ if (note) {
beep();
notify(id, str);
}
@@ -640,78 +361,10 @@ void uiFormat(
uiWrite(id, heat, time, buf);
}
-static void scrollTo(struct Window *window, int top) {
- window->scroll = 0;
- windowScroll(window, top - MAIN_LINES + MarkerLines);
-}
-
-static void windowReflow(struct Window *window) {
- uint num = 0;
- const struct Line *line = bufferHard(window->buffer, windowTop(window));
- if (line) num = line->num;
- window->unreadHard = bufferReflow(
- window->buffer, windowCols(window),
- window->thresh, window->unreadSoft
- );
- if (!window->scroll || !num) return;
- for (size_t i = 0; i < BufferCap; ++i) {
- line = bufferHard(window->buffer, i);
- if (!line || line->num != num) continue;
- scrollTo(window, BufferCap - i);
- break;
- }
-}
-
static void resize(void) {
- wclear(main);
- wresize(main, MAIN_LINES, COLS);
- for (uint num = 0; num < windows.len; ++num) {
- windowReflow(windows.ptrs[num]);
- }
- statusUpdate();
- mainUpdate();
-}
-
-static void windowList(const struct Window *window) {
- uiHide();
- waiting = true;
-
- uint num = 0;
- const struct Line *line = bufferHard(window->buffer, windowBottom(window));
- if (line) num = line->num;
- for (size_t i = 0; i < BufferCap; ++i) {
- line = bufferSoft(window->buffer, i);
- if (!line) continue;
- if (line->num > num) break;
- if (!line->str[0]) {
- printf("\n");
- continue;
- }
-
- char buf[TimeCap];
- strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time));
- vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL);
- printf("%s ", buf);
-
- bool align = false;
- struct Style style = StyleDefault;
- for (const char *str = line->str; *str;) {
- if (*str == '\t') {
- printf("%c", (align ? '\t' : ' '));
- align = true;
- str++;
- }
-
- size_t len = styleParse(&style, &str);
- size_t tab = strcspn(str, "\t");
- if (tab < len) len = tab;
-
- vid_attr(styleAttr(style), stylePair(style), NULL);
- printf("%.*s", (int)len, str);
- str += len;
- }
- printf("\n");
- }
+ wclear(uiMain);
+ wresize(uiMain, MAIN_LINES, COLS);
+ windowResize();
}
static void inputAdd(struct Style reset, struct Style *style, const char *str) {
@@ -736,7 +389,7 @@ static void inputAdd(struct Style reset, struct Style *style, const char *str) {
}
size_t nl = strcspn(str, "\n");
if (nl < len) len = nl;
- wattr_set(input, styleAttr(*style), stylePair(*style), NULL);
+ wattr_set(input, uiAttr(*style), uiPair(*style), NULL);
waddnstr(input, str, len);
str += len;
}
@@ -755,9 +408,9 @@ static char *inputStop(
static struct Edit edit;
-static void inputUpdate(void) {
+void uiUpdate(void) {
char *buf = editString(&edit);
- struct Window *window = windows.ptrs[windows.show];
+ uint id = windowID();
const char *prefix = "";
const char *prompt = self.nick;
@@ -766,10 +419,10 @@ static void inputUpdate(void) {
struct Style stylePrompt = { .fg = self.color, .bg = Default };
struct Style styleInput = StyleDefault;
- size_t split = commandWillSplit(window->id, buf);
- const char *privmsg = commandIsPrivmsg(window->id, buf);
- const char *notice = commandIsNotice(window->id, buf);
- const char *action = commandIsAction(window->id, buf);
+ size_t split = commandWillSplit(id, buf);
+ const char *privmsg = commandIsPrivmsg(id, buf);
+ const char *notice = commandIsNotice(id, buf);
+ const char *action = commandIsAction(id, buf);
if (privmsg) {
prefix = "<"; suffix = "> ";
skip = privmsg;
@@ -782,7 +435,7 @@ static void inputUpdate(void) {
stylePrompt.attr |= Italic;
styleInput.attr |= Italic;
skip = action;
- } else if (window->id == Debug && buf[0] != '/') {
+ } else if (id == Debug && buf[0] != '/') {
prompt = "<< ";
stylePrompt.fg = Gray;
} else {
@@ -795,11 +448,11 @@ static void inputUpdate(void) {
int y, x;
wmove(input, 0, 0);
- if (window->time && window->id != Network) {
- whline(input, ' ', uiTime.width);
- wmove(input, 0, uiTime.width);
+ if (windowTimeEnable() && id != Network) {
+ whline(input, ' ', windowTime.width);
+ wmove(input, 0, windowTime.width);
}
- wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), NULL);
+ wattr_set(input, uiAttr(stylePrompt), uiPair(stylePrompt), NULL);
waddstr(input, prefix);
waddstr(input, prompt);
waddstr(input, suffix);
@@ -823,209 +476,61 @@ static void inputUpdate(void) {
wmove(input, y, pos);
}
-void uiWindows(void) {
- for (uint num = 0; num < windows.len; ++num) {
- const struct Window *window = windows.ptrs[num];
- uiFormat(
- Network, Warm, NULL, "\3%02d%u %s",
- idColors[window->id], num, idNames[window->id]
- );
- }
-}
-
-static void windowShow(uint num) {
- if (num != windows.show) {
- windows.swap = windows.show;
- mark(windows.ptrs[windows.swap]);
- }
- windows.show = num;
- windows.user = num;
- unmark(windows.ptrs[windows.show]);
- mainUpdate();
- inputUpdate();
-}
-
-void uiShowID(uint id) {
- windowShow(windowFor(id));
-}
-
-void uiShowNum(uint num) {
- if (num < windows.len) windowShow(num);
-}
-
-void uiMoveID(uint id, uint num) {
- struct Window *window = windowRemove(windowFor(id));
- if (num < windows.len) {
- windowShow(windowInsert(num, window));
- } else {
- windowShow(windowPush(window));
- }
-}
-
-static void windowClose(uint num) {
- if (windows.ptrs[num]->id == Network) return;
- struct Window *window = windowRemove(num);
- completeClear(window->id);
- windowFree(window);
- if (windows.swap >= num) windows.swap--;
- if (windows.show == num) {
- windowShow(windows.swap);
- windows.swap = windows.show;
- } else if (windows.show > num) {
- windows.show--;
- mainUpdate();
- }
- statusUpdate();
-}
-
-void uiCloseID(uint id) {
- windowClose(windowFor(id));
-}
-
-void uiCloseNum(uint num) {
- if (num < windows.len) windowClose(num);
-}
-
-static void scrollPage(struct Window *window, int n) {
- windowScroll(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1));
-}
-
-static void scrollTop(struct Window *window) {
- for (size_t i = 0; i < BufferCap; ++i) {
- if (!bufferHard(window->buffer, i)) continue;
- scrollTo(window, BufferCap - i);
- break;
- }
-}
-
-static void scrollHot(struct Window *window, int dir) {
- for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) {
- const struct Line *line = bufferHard(window->buffer, i);
- const struct Line *prev = bufferHard(window->buffer, i - 1);
- if (!line || line->heat < Hot) continue;
- if (prev && prev->heat > Warm) continue;
- scrollTo(window, BufferCap - i);
- break;
- }
-}
-
-static void scrollSearch(struct Window *window, const char *str, int dir) {
- for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) {
- const struct Line *line = bufferHard(window->buffer, i);
- if (!line || !strcasestr(line->str, str)) continue;
- scrollTo(window, BufferCap - i);
- break;
- }
-}
-
-static void toggleTime(struct Window *window) {
- window->time ^= true;
- windowReflow(window);
- statusUpdate();
- mainUpdate();
- inputUpdate();
-}
-
-static void incThresh(struct Window *window, int n) {
- if (n > 0 && window->thresh == Hot) return;
- if (n < 0 && window->thresh == Ice) {
- window->thresh = Cold;
- } else {
- window->thresh += n;
- }
- windowReflow(window);
- statusUpdate();
- mainUpdate();
- statusUpdate();
-}
-
-static void showAuto(void) {
- uint minHot = UINT_MAX, numHot = 0;
- uint minWarm = UINT_MAX, numWarm = 0;
- for (uint num = 0; num < windows.len; ++num) {
- struct Window *window = windows.ptrs[num];
- if (window->heat >= Hot) {
- if (window->unreadWarm >= minHot) continue;
- minHot = window->unreadWarm;
- numHot = num;
- }
- if (window->heat >= Warm && !window->mute) {
- if (window->unreadWarm >= minWarm) continue;
- minWarm = window->unreadWarm;
- numWarm = num;
- }
- }
- uint user = windows.user;
- if (minHot < UINT_MAX) {
- windowShow(numHot);
- windows.user = user;
- } else if (minWarm < UINT_MAX) {
- windowShow(numWarm);
- windows.user = user;
- } else if (user != windows.show) {
- windowShow(user);
- }
-}
-
-static void inputEnter(uint id) {
- command(id, editString(&edit));
+static void inputEnter(void) {
+ command(windowID(), editString(&edit));
editFn(&edit, EditClear);
}
static void keyCode(int code) {
- struct Window *window = windows.ptrs[windows.show];
- uint id = window->id;
switch (code) {
break; case KEY_RESIZE: resize();
- break; case KeyFocusIn: unmark(window);
- break; case KeyFocusOut: mark(window);
+ break; case KeyFocusIn: windowUnmark();
+ break; case KeyFocusOut: windowMark();
break; case KeyMetaEnter: editInsert(&edit, L'\n');
- break; case KeyMetaEqual: window->mute ^= true; statusUpdate();
- break; case KeyMetaMinus: incThresh(window, -1);
- break; case KeyMetaPlus: incThresh(window, +1);
- break; case KeyMetaSlash: windowShow(windows.swap);
+ break; case KeyMetaEqual: windowToggleMute();
+ break; case KeyMetaMinus: windowToggleThresh(-1);
+ break; case KeyMetaPlus: windowToggleThresh(+1);
+ break; case KeyMetaSlash: windowSwap();
- break; case KeyMetaGt: scrollTo(window, 0);
- break; case KeyMetaLt: scrollTop(window);
+ break; case KeyMetaGt: windowScroll(ScrollAll, -1);
+ break; case KeyMetaLt: windowScroll(ScrollAll, +1);
- break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0);
- break; case KeyMetaA: showAuto();
+ break; case KeyMeta0 ... KeyMeta9: windowShow(code - KeyMeta0);
+ break; case KeyMetaA: windowAuto();
break; case KeyMetaB: editFn(&edit, EditPrevWord);
break; case KeyMetaD: editFn(&edit, EditDeleteNextWord);
break; case KeyMetaF: editFn(&edit, EditNextWord);
- break; case KeyMetaL: windowList(window);
- break; case KeyMetaM: uiWrite(id, Warm, NULL, "");
- break; case KeyMetaN: scrollHot(window, +1);
- break; case KeyMetaP: scrollHot(window, -1);
+ break; case KeyMetaL: windowBare();
+ break; case KeyMetaM: uiWrite(windowID(), Warm, NULL, "");
+ break; case KeyMetaN: windowScroll(ScrollHot, +1);
+ break; case KeyMetaP: windowScroll(ScrollHot, -1);
break; case KeyMetaQ: editFn(&edit, EditCollapse);
- break; case KeyMetaS: spoilerReveal ^= true; mainUpdate();
- break; case KeyMetaT: toggleTime(window);
- break; case KeyMetaU: scrollTo(window, window->unreadHard);
- break; case KeyMetaV: scrollPage(window, +1);
+ break; case KeyMetaS: spoilerReveal ^= true; windowUpdate();
+ break; case KeyMetaT: windowToggleTime();
+ break; case KeyMetaU: windowScroll(ScrollUnread, 0);
+ break; case KeyMetaV: windowScroll(ScrollPage, +1);
break; case KeyCtrlLeft: editFn(&edit, EditPrevWord);
break; case KeyCtrlRight: editFn(&edit, EditNextWord);
break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev);
break; case KEY_DC: editFn(&edit, EditDeleteNext);
- break; case KEY_DOWN: windowScroll(window, -1);
+ break; case KEY_DOWN: windowScroll(ScrollOne, -1);
break; case KEY_END: editFn(&edit, EditTail);
- break; case KEY_ENTER: inputEnter(id);
+ break; case KEY_ENTER: inputEnter();
break; case KEY_HOME: editFn(&edit, EditHead);
break; case KEY_LEFT: editFn(&edit, EditPrev);
- break; case KEY_NPAGE: scrollPage(window, -1);
- break; case KEY_PPAGE: scrollPage(window, +1);
+ break; case KEY_NPAGE: windowScroll(ScrollPage, -1);
+ break; case KEY_PPAGE: windowScroll(ScrollPage, +1);
break; case KEY_RIGHT: editFn(&edit, EditNext);
- break; case KEY_SEND: scrollTo(window, 0);
- break; case KEY_SHOME: scrollTo(window, BufferCap);
- break; case KEY_UP: windowScroll(window, +1);
+ break; case KEY_SEND: windowScroll(ScrollAll, -1);
+ break; case KEY_SHOME: windowScroll(ScrollAll, +1);
+ break; case KEY_UP: windowScroll(ScrollOne, +1);
}
}
static void keyCtrl(wchar_t ch) {
- struct Window *window = windows.ptrs[windows.show];
- uint id = window->id;
switch (ch ^ L'@') {
break; case L'?': editFn(&edit, EditDeletePrev);
break; case L'A': editFn(&edit, EditHead);
@@ -1035,16 +540,16 @@ static void keyCtrl(wchar_t ch) {
break; case L'E': editFn(&edit, EditTail);
break; case L'F': editFn(&edit, EditNext);
break; case L'H': editFn(&edit, EditDeletePrev);
- break; case L'J': inputEnter(id);
+ break; case L'J': inputEnter();
break; case L'K': editFn(&edit, EditDeleteTail);
break; case L'L': clearok(curscr, true);
- break; case L'N': uiShowNum(windows.show + 1);
- break; case L'P': uiShowNum(windows.show - 1);
- break; case L'R': scrollSearch(window, editString(&edit), -1);
- break; case L'S': scrollSearch(window, editString(&edit), +1);
+ break; case L'N': windowShow(windowNum() + 1);
+ break; case L'P': windowShow(windowNum() - 1);
+ break; case L'R': windowSearch(editString(&edit), -1);
+ break; case L'S': windowSearch(editString(&edit), +1);
break; case L'T': editFn(&edit, EditTranspose);
break; case L'U': editFn(&edit, EditDeleteHead);
- break; case L'V': scrollPage(window, -1);
+ break; case L'V': windowScroll(ScrollPage, -1);
break; case L'W': editFn(&edit, EditDeletePrevWord);
break; case L'Y': editFn(&edit, EditPaste);
}
@@ -1127,13 +632,15 @@ void uiRead(void) {
literal = false;
if (spr) {
spoilerReveal = false;
- mainUpdate();
+ windowUpdate();
}
}
- inputUpdate();
+ uiUpdate();
}
-static const time_t Signatures[] = {
+static FILE *saveFile;
+
+static const uint64_t Signatures[] = {
0x6C72696774616301, // no heat, unread, unreadWarm
0x6C72696774616302, // no self.pos
0x6C72696774616303, // no buffer line heat
@@ -1144,68 +651,33 @@ static const time_t Signatures[] = {
0x6C72696774616308,
};
-static size_t signatureVersion(time_t signature) {
+static size_t signatureVersion(uint64_t signature) {
for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) {
if (signature == Signatures[i]) return i;
}
- errx(EX_DATAERR, "unknown file signature %jX", (uintmax_t)signature);
+ errx(EX_DATAERR, "unknown file signature %" PRIX64, signature);
}
-static int writeTime(FILE *file, time_t time) {
- return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1);
+static int writeUint64(FILE *file, uint64_t u) {
+ return (fwrite(&u, sizeof(u), 1, file) ? 0 : -1);
}
-static int writeString(FILE *file, const char *str) {
- return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1);
-}
-
-static FILE *saveFile;
int uiSave(void) {
- int error = 0
- || ftruncate(fileno(saveFile), 0)
- || writeTime(saveFile, Signatures[7])
- || writeTime(saveFile, self.pos);
- if (error) return error;
- for (uint num = 0; num < windows.len; ++num) {
- const struct Window *window = windows.ptrs[num];
- error = 0
- || writeString(saveFile, idNames[window->id])
- || writeTime(saveFile, window->mute)
- || writeTime(saveFile, window->time)
- || writeTime(saveFile, window->thresh)
- || writeTime(saveFile, window->heat)
- || writeTime(saveFile, window->unreadSoft)
- || writeTime(saveFile, window->unreadWarm);
- if (error) return error;
- for (size_t i = 0; i < BufferCap; ++i) {
- const struct Line *line = bufferSoft(window->buffer, i);
- if (!line) continue;
- error = 0
- || writeTime(saveFile, line->time)
- || writeTime(saveFile, line->heat)
- || writeString(saveFile, line->str);
- if (error) return error;
- }
- error = writeTime(saveFile, 0);
- if (error) return error;
- }
return 0
- || writeString(saveFile, "")
+ || ftruncate(fileno(saveFile), 0)
+ || writeUint64(saveFile, Signatures[7])
+ || writeUint64(saveFile, self.pos)
+ || windowSave(saveFile)
|| urlSave(saveFile)
|| fclose(saveFile);
}
-static time_t readTime(FILE *file) {
- time_t time;
- fread(&time, sizeof(time), 1, file);
+static uint64_t readUint64(FILE *file) {
+ uint64_t u;
+ fread(&u, sizeof(u), 1, file);
if (ferror(file)) err(EX_IOERR, "fread");
if (feof(file)) errx(EX_DATAERR, "unexpected eof");
- return time;
-}
-static ssize_t readString(FILE *file, char **buf, size_t *cap) {
- ssize_t len = getdelim(buf, cap, '\0', file);
- if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
- return len;
+ return u;
}
void uiLoad(const char *name) {
@@ -1235,31 +707,8 @@ void uiLoad(const char *name) {
size_t version = signatureVersion(signature);
if (version > 1) {
- self.pos = readTime(saveFile);
- }
-
- char *buf = NULL;
- size_t cap = 0;
- while (0 < readString(saveFile, &buf, &cap) && buf[0]) {
- struct Window *window = windows.ptrs[windowFor(idFor(buf))];
- if (version > 3) window->mute = readTime(saveFile);
- if (version > 6) window->time = readTime(saveFile);
- if (version > 5) window->thresh = readTime(saveFile);
- if (version > 0) {
- window->heat = readTime(saveFile);
- window->unreadSoft = readTime(saveFile);
- window->unreadWarm = readTime(saveFile);
- }
- for (;;) {
- time_t time = readTime(saveFile);
- if (!time) break;
- enum Heat heat = (version > 2 ? readTime(saveFile) : Cold);
- readString(saveFile, &buf, &cap);
- bufferPush(window->buffer, COLS, window->thresh, heat, time, buf);
- }
- windowReflow(window);
+ self.pos = readUint64(saveFile);
}
+ windowLoad(saveFile, version);
urlLoad(saveFile, version);
-
- free(buf);
}
diff --git a/window.c b/window.c
new file mode 100644
index 0000000..ce3ec4d
--- /dev/null
+++ b/window.c
@@ -0,0 +1,656 @@
+/* Copyright (C) 2020 June McEnroe <june@causal.agency>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
+ */
+
+#define _XOPEN_SOURCE_EXTENDED
+
+#include <assert.h>
+#include <curses.h>
+#include <err.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <time.h>
+
+#include "chat.h"
+
+#define MAIN_LINES (LINES - StatusLines - InputLines)
+
+static struct Window {
+ uint id;
+ int scroll;
+ bool mark;
+ bool mute;
+ bool time;
+ enum Heat thresh;
+ enum Heat heat;
+ uint unreadSoft;
+ uint unreadHard;
+ uint unreadWarm;
+ struct Buffer *buffer;
+} *windows[IDCap];
+
+static uint count;
+static uint show;
+static uint swap;
+static uint user;
+
+static uint windowPush(struct Window *window) {
+ assert(count < IDCap);
+ windows[count] = window;
+ return count++;
+}
+
+static uint windowInsert(uint num, struct Window *window) {
+ assert(count < IDCap);
+ assert(num <= count);
+ memmove(
+ &windows[num + 1],
+ &windows[num],
+ sizeof(*windows) * (count - num)
+ );
+ windows[num] = window;
+ count++;
+ return num;
+}
+
+static struct Window *windowRemove(uint num) {
+ assert(num < count);
+ struct Window *window = windows[num];
+ count--;
+ memmove(
+ &windows[num],
+ &windows[num + 1],
+ sizeof(*windows) * (count - num)
+ );
+ return window;
+}
+
+static void windowFree(struct Window *window) {
+ completeRemove(None, idNames[window->id]);
+ bufferFree(window->buffer);
+ free(window);
+}
+
+enum Heat windowThreshold = Cold;
+struct Time windowTime = { .format = "%X" };
+
+uint windowFor(uint id) {
+ for (uint num = 0; num < count; ++num) {
+ if (windows[num]->id == id) return num;
+ }
+
+ struct Window *window = calloc(1, sizeof(*window));
+ if (!window) err(EX_OSERR, "malloc");
+
+ window->id = id;
+ window->mark = true;
+ window->time = windowTime.enable;
+ if (id == Network || id == Debug) {
+ window->thresh = Cold;
+ } else {
+ window->thresh = windowThreshold;
+ }
+ window->buffer = bufferAlloc();
+ completeAdd(None, idNames[id], idColors[id]);
+
+ return windowPush(window);
+}
+
+enum { TimeCap = 64 };
+
+void windowInit(void) {
+ char fmt[TimeCap];
+ char buf[TimeCap];
+ styleStrip(fmt, sizeof(fmt), windowTime.format);
+
+ struct tm *time = localtime(&(time_t) { -22100400 });
+ size_t len = strftime(buf, sizeof(buf), fmt, time);
+ if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", fmt);
+
+ int y;
+ waddstr(uiMain, buf);
+ waddch(uiMain, ' ');
+ getyx(uiMain, y, windowTime.width);
+ (void)y;
+
+ windowFor(Network);
+}
+
+static int styleAdd(WINDOW *win, struct Style init, const char *str) {
+ struct Style style = init;
+ while (*str) {
+ size_t len = styleParse(&style, &str);
+ wattr_set(win, uiAttr(style), uiPair(style), NULL);
+ if (waddnstr(win, str, len) == ERR)
+ return -1;
+ str += len;
+ }
+ return 0;
+}
+
+static void statusUpdate(void) {
+ struct {
+ uint unread;
+ enum Heat heat;
+ } others = { 0, Cold };
+
+ wmove(uiStatus, 0, 0);
+ for (uint num = 0; num < count; ++num) {
+ const struct Window *window = windows[num];
+ if (num != show && !window->scroll) {
+ if (window->heat < Warm) continue;
+ if (window->mute && window->heat < Hot) continue;
+ }
+ if (num != show) {
+ others.unread += window->unreadWarm;
+ if (window->heat > others.heat) others.heat = window->heat;
+ }
+ char buf[256], *end = &buf[sizeof(buf)];
+ char *ptr = seprintf(
+ buf, end, "\3%d%s %u%s%s %s ",
+ idColors[window->id], (num == show ? "\26" : ""),
+ num, window->thresh[(const char *[]) { "-", "", "+", "++" }],
+ &"="[!window->mute], idNames[window->id]
+ );
+ if (window->mark && window->unreadWarm) {
+ ptr = seprintf(
+ ptr, end, "\3%d+%d\3%d%s",
+ (window->heat > Warm ? White : idColors[window->id]),
+ window->unreadWarm, idColors[window->id],
+ (window->scroll ? "" : " ")
+ );
+ }
+ if (window->scroll) {
+ ptr = seprintf(ptr, end, "~%d ", window->scroll);
+ }
+ if (styleAdd(uiStatus, StyleDefault, buf) < 0) break;
+ }
+ wclrtoeol(uiStatus);
+
+ const struct Window *window = windows[show];
+ char *end = &uiTitle[sizeof(uiTitle)];
+ char *ptr = seprintf(
+ uiTitle, end, "%s %s", network.name, idNames[window->id]
+ );
+ if (window->mark && window->unreadWarm) {
+ ptr = seprintf(
+ ptr, end, " +%d%s", window->unreadWarm, &"!"[window->heat < Hot]
+ );
+ }
+ if (others.unread) {
+ ptr = seprintf(
+ ptr, end, " (+%d%s)", others.unread, &"!"[others.heat < Hot]
+ );
+ }
+}
+
+static size_t windowTop(const struct Window *window) {
+ size_t top = BufferCap - MAIN_LINES - window->scroll;
+ if (window->scroll) top += MarkerLines;
+ return top;
+}
+
+static size_t windowBottom(const struct Window *window) {
+ size_t bottom = BufferCap - (window->scroll ?: 1);
+ if (window->scroll) bottom -= SplitLines + MarkerLines;
+ return bottom;
+}
+
+static void mainAdd(int y, bool time, const struct Line *line) {
+ int ny, nx;
+ wmove(uiMain, y, 0);
+ if (!line || !line->str[0]) {
+ wclrtoeol(uiMain);
+ return;
+ }
+ if (time && line->time) {
+ char buf[TimeCap];
+ strftime(buf, sizeof(buf), windowTime.format, localtime(&line->time));
+ struct Style init = { .fg = Gray, .bg = Default };
+ styleAdd(uiMain, init, buf);
+ waddch(uiMain, ' ');
+ } else if (time) {
+ whline(uiMain, ' ', windowTime.width);
+ wmove(uiMain, y, windowTime.width);
+ }
+ styleAdd(uiMain, StyleDefault, line->str);
+ getyx(uiMain, ny, nx);
+ if (ny != y) return;
+ wclrtoeol(uiMain);
+ (void)nx;
+}
+
+static void mainUpdate(void) {
+ const struct Window *window = windows[show];
+
+ int y = 0;
+ int marker = MAIN_LINES - SplitLines - MarkerLines;
+ for (size_t i = windowTop(window); i < BufferCap; ++i) {
+ mainAdd(y++, window->time, bufferHard(window->buffer, i));
+ if (window->scroll && y == marker) break;
+ }
+ if (!window->scroll) return;
+
+ y = MAIN_LINES - SplitLines;
+ for (size_t i = BufferCap - SplitLines; i < BufferCap; ++i) {
+ mainAdd(y++, window->time, bufferHard(window->buffer, i));
+ }
+ wattr_set(uiMain, A_NORMAL, 0, NULL);
+ mvwhline(uiMain, marker, 0, ACS_BULLET, COLS);
+}
+
+void windowUpdate(void) {
+ statusUpdate();
+ mainUpdate();
+}
+
+void windowBare(void) {
+ uiHide();
+ uiWait();
+
+ const struct Window *window = windows[show];
+ const struct Line *line = bufferHard(window->buffer, windowBottom(window));
+
+ uint num = 0;
+ if (line) num = line->num;
+ for (size_t i = 0; i < BufferCap; ++i) {
+ line = bufferSoft(window->buffer, i);
+ if (!line) continue;
+ if (line->num > num) break;
+ if (!line->str[0]) {
+ printf("\n");
+ continue;
+ }
+
+ char buf[TimeCap];
+ struct Style style = { .fg = Gray, .bg = Default };
+ strftime(buf, sizeof(buf), windowTime.format, localtime(&line->time));
+ vid_attr(uiAttr(style), uiPair(style), NULL);
+ printf("%s ", buf);
+
+ bool align = false;
+ style = StyleDefault;
+ for (const char *str = line->str; *str;) {
+ if (*str == '\t') {
+ printf("%c", (align ? '\t' : ' '));
+ align = true;
+ str++;
+ }
+
+ size_t len = styleParse(&style, &str);
+ size_t tab = strcspn(str, "\t");
+ if (tab < len) len = tab;
+
+ vid_attr(uiAttr(style), uiPair(style), NULL);
+ printf("%.*s", (int)len, str);
+ str += len;
+ }
+ printf("\n");
+ }
+}
+
+static void mark(struct Window *window) {
+ if (window->scroll) return;
+ window->mark = true;
+ window->unreadSoft = 0;
+ window->unreadWarm = 0;
+}
+
+static void unmark(struct Window *window) {
+ if (!window->scroll) {
+ window->mark = false;
+ window->heat = Cold;
+ }
+ statusUpdate();
+}
+
+static void scrollN(struct Window *window, int n) {
+ mark(window);
+ window->scroll += n;
+ if (window->scroll > BufferCap - MAIN_LINES) {
+ window->scroll = BufferCap - MAIN_LINES;
+ }
+ if (window->scroll < 0) window->scroll = 0;
+ unmark(window);
+ if (window == windows[show]) mainUpdate();
+}
+
+static void scrollTo(struct Window *window, int top) {
+ window->scroll = 0;
+ scrollN(window, top - MAIN_LINES + MarkerLines);
+}
+
+static int windowCols(const struct Window *window) {
+ return COLS - (window->time ? windowTime.width : 0);
+}
+
+bool windowWrite(uint id, enum Heat heat, const time_t *src, const char *str) {
+ struct Window *window = windows[windowFor(id)];
+ time_t ts = (src ? *src : time(NULL));
+
+ if (heat >= window->thresh) {
+ if (!window->unreadSoft++) window->unreadHard = 0;
+ }
+ if (window->mark && heat > Cold) {
+ if (!window->unreadWarm++) {
+ int lines = bufferPush(
+ window->buffer, windowCols(window),
+ window->thresh, Warm, ts, ""
+ );
+ if (window->scroll) scrollN(window, lines);
+ if (window->unreadSoft > 1) {
+ window->unreadSoft++;
+ window->unreadHard += lines;
+ }
+ }
+ if (heat > window->heat) window->heat = heat;
+ statusUpdate();
+ }
+ int lines = bufferPush(
+ window->buffer, windowCols(window),
+ window->thresh, heat, ts, str
+ );
+ window->unreadHard += lines;
+ if (window->scroll) scrollN(window, lines);
+ if (window == windows[show]) mainUpdate();
+
+ return window->mark && heat > Warm;
+}
+
+static void reflow(struct Window *window) {
+ uint num = 0;
+ const struct Line *line = bufferHard(window->buffer, windowTop(window));
+ if (line) num = line->num;
+ window->unreadHard = bufferReflow(
+ window->buffer, windowCols(window),
+ window->thresh, window->unreadSoft
+ );
+ if (!window->scroll || !num) return;
+ for (size_t i = 0; i < BufferCap; ++i) {
+ line = bufferHard(window->buffer, i);
+ if (!line || line->num != num) continue;
+ scrollTo(window, BufferCap - i);
+ break;
+ }
+}
+
+void windowResize(void) {
+ for (uint num = 0; num < count; ++num) {
+ reflow(windows[num]);
+ }
+ windowUpdate();
+}
+
+uint windowID(void) {
+ return windows[show]->id;
+}
+
+uint windowNum(void) {
+ return show;
+}
+
+void windowShow(uint num) {
+ if (num >= count) return;
+ if (num != show) {
+ swap = show;
+ mark(windows[swap]);
+ }
+ show = num;
+ user = num;
+ unmark(windows[show]);
+ mainUpdate();
+ uiUpdate();
+}
+
+void windowAuto(void) {
+ uint minHot = UINT_MAX, numHot = 0;
+ uint minWarm = UINT_MAX, numWarm = 0;
+ for (uint num = 0; num < count; ++num) {
+ struct Window *window = windows[num];
+ if (window->heat >= Hot) {
+ if (window->unreadWarm >= minHot) continue;
+ minHot = window->unreadWarm;
+ numHot = num;
+ }
+ if (window->heat >= Warm && !window->mute) {
+ if (window->unreadWarm >= minWarm) continue;
+ minWarm = window->unreadWarm;
+ numWarm = num;
+ }
+ }
+ uint oldUser = user;
+ if (minHot < UINT_MAX) {
+ windowShow(numHot);
+ user = oldUser;
+ } else if (minWarm < UINT_MAX) {
+ windowShow(numWarm);
+ user = oldUser;
+ } else if (user != show) {
+ windowShow(user);
+ }
+}
+
+void windowSwap(void) {
+ windowShow(swap);
+}
+
+void windowMove(uint from, uint to) {
+ if (from >= count) return;
+ struct Window *window = windowRemove(from);
+ if (to < count) {
+ windowShow(windowInsert(to, window));
+ } else {
+ windowShow(windowPush(window));
+ }
+}
+
+void windowClose(uint num) {
+ if (num >= count) return;
+ if (windows[num]->id == Network) return;
+ struct Window *window = windowRemove(num);
+ completeClear(window->id);
+ windowFree(window);
+ if (swap >= num) swap--;
+ if (show == num) {
+ windowShow(swap);
+ swap = show;
+ } else if (show > num) {
+ show--;
+ mainUpdate();
+ }
+ statusUpdate();
+}
+
+void windowList(void) {
+ for (uint num = 0; num < count; ++num) {
+ const struct Window *window = windows[num];
+ uiFormat(
+ Network, Warm, NULL, "\3%02d%u %s",
+ idColors[window->id], num, idNames[window->id]
+ );
+ }
+}
+
+void windowMark(void) {
+ mark(windows[show]);
+}
+
+void windowUnmark(void) {
+ unmark(windows[show]);
+}
+
+void windowToggleMute(void) {
+ windows[show]->mute ^= true;
+ statusUpdate();
+}
+
+void windowToggleTime(void) {
+ windows[show]->time ^= true;
+ reflow(windows[show]);
+ windowUpdate();
+ uiUpdate();
+}
+
+void windowToggleThresh(int n) {
+ struct Window *window = windows[show];
+ if (n > 0 && window->thresh == Hot) return;
+ if (n < 0 && window->thresh == Ice) {
+ window->thresh = Cold;
+ } else {
+ window->thresh += n;
+ }
+ reflow(window);
+ windowUpdate();
+}
+
+bool windowTimeEnable(void) {
+ return windows[show]->time;
+}
+
+void windowScroll(enum Scroll by, int n) {
+ struct Window *window = windows[show];
+ switch (by) {
+ break; case ScrollOne: {
+ scrollN(window, n);
+ }
+ break; case ScrollPage: {
+ scrollN(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1));
+ }
+ break; case ScrollAll: {
+ if (n < 0) {
+ scrollTo(window, 0);
+ break;
+ }
+ for (size_t i = 0; i < BufferCap; ++i) {
+ if (!bufferHard(window->buffer, i)) continue;
+ scrollTo(window, BufferCap - i);
+ break;
+ }
+ }
+ break; case ScrollUnread: {
+ scrollTo(window, window->unreadHard);
+ }
+ break; case ScrollHot: {
+ for (size_t i = windowTop(window) + n; i < BufferCap; i += n) {
+ const struct Line *line = bufferHard(window->buffer, i);
+ const struct Line *prev = bufferHard(window->buffer, i - 1);
+ if (!line || line->heat < Hot) continue;
+ if (prev && prev->heat > Warm) continue;
+ scrollTo(window, BufferCap - i);
+ break;
+ }
+ }
+ }
+}
+
+void windowSearch(const char *str, int dir) {
+ struct Window *window = windows[show];
+ for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) {
+ const struct Line *line = bufferHard(window->buffer, i);
+ if (!line || !strcasestr(line->str, str)) continue;
+ scrollTo(window, BufferCap - i);
+ break;
+ }
+}
+
+static int writeTime(FILE *file, time_t time) {
+ return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1);
+}
+
+static int writeString(FILE *file, const char *str) {
+ return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1);
+}
+
+int windowSave(FILE *file) {
+ int error;
+ for (uint num = 0; num < count; ++num) {
+ const struct Window *window = windows[num];
+ error = 0
+ || writeString(file, idNames[window->id])
+ || writeTime(file, window->mute)
+ || writeTime(file, window->time)
+ || writeTime(file, window->thresh)
+ || writeTime(file, window->heat)
+ || writeTime(file, window->unreadSoft)
+ || writeTime(file, window->unreadWarm);
+ if (error) return error;
+ for (size_t i = 0; i < BufferCap; ++i) {
+ const struct Line *line = bufferSoft(window->buffer, i);
+ if (!line) continue;
+ error = 0
+ || writeTime(file, line->time)
+ || writeTime(file, line->heat)
+ || writeString(file, line->str);
+ if (error) return error;
+ }
+ error = writeTime(file, 0);
+ if (error) return error;
+ }
+ return writeString(file, "");
+}
+
+static time_t readTime(FILE *file) {
+ time_t time;
+ fread(&time, sizeof(time), 1, file);
+ if (ferror(file)) err(EX_IOERR, "fread");
+ if (feof(file)) errx(EX_DATAERR, "unexpected eof");
+ return time;
+}
+
+static ssize_t readString(FILE *file, char **buf, size_t *cap) {
+ ssize_t len = getdelim(buf, cap, '\0', file);
+ if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
+ return len;
+}
+
+void windowLoad(FILE *file, size_t version) {
+ size_t cap = 0;
+ char *buf = NULL;
+ while (0 < readString(file, &buf, &cap) && buf[0]) {
+ struct Window *window = windows[windowFor(idFor(buf))];
+ if (version > 3) window->mute = readTime(file);
+ if (version > 6) window->time = readTime(file);
+ if (version > 5) window->thresh = readTime(file);
+ if (version > 0) {
+ window->heat = readTime(file);
+ window->unreadSoft = readTime(file);
+ window->unreadWarm = readTime(file);
+ }
+ for (;;) {
+ time_t time = readTime(file);
+ if (!time) break;
+ enum Heat heat = (version > 2 ? readTime(file) : Cold);
+ readString(file, &buf, &cap);
+ bufferPush(window->buffer, COLS, window->thresh, heat, time, buf);
+ }
+ reflow(window);
+ }
+ free(buf);
+}