diff options
-rw-r--r-- | README.7 | 17 | ||||
-rw-r--r-- | catgirl.1 | 19 | ||||
-rw-r--r-- | chat.c | 7 | ||||
-rw-r--r-- | chat.h | 8 | ||||
-rw-r--r-- | command.c | 2 | ||||
-rw-r--r-- | edit.c | 3 | ||||
-rw-r--r-- | handle.c | 32 | ||||
-rw-r--r-- | scripts/chat.tmux.conf | 57 | ||||
-rw-r--r-- | ui.c | 180 |
9 files changed, 242 insertions, 83 deletions
diff --git a/README.7 b/README.7 index a67ec77..a895d7f 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd January 25, 2021 +.Dd February 8, 2021 .Dt README 7 .Os "Causal Agency" .\" To view this file, run: man ./README.7 @@ -98,9 +98,18 @@ provided by either .Lk https://git.causal.agency/libretls/about LibreTLS (for OpenSSL) or by LibreSSL. +.Nm +and +.Sy libtls +may be packaged for your system. +Check the Repology pages for +.Lk https://repology.org/project/catgirl/versions catgirl +and +.Lk https://repology.org/project/libretls/versions libretls . . .Pp -It targets +.Nm +targets .Fx , .Ox , macOS @@ -222,6 +231,10 @@ Contributions in any form can be sent to For sending patches by email, see .Aq Lk https://git-send-email.io . . +.Pp +Monetary contributions can be +.Lk https://liberapay.com/june/donate "donated via Liberapay" . +. .Sh SEE ALSO .Xr catgirl 1 . diff --git a/catgirl.1 b/catgirl.1 index b09d55e..0543be3 100644 --- a/catgirl.1 +++ b/catgirl.1 @@ -1,4 +1,4 @@ -.Dd January 25, 2021 +.Dd February 15, 2021 .Dt CATGIRL 1 .Os . @@ -15,6 +15,7 @@ .Op Fl N Ar notify .Op Fl O Ar open .Op Fl S Ar bind +.Op Fl T Ar timestamp .Op Fl a Ar plain .Op Fl c Ar cert .Op Fl h Ar host @@ -181,6 +182,14 @@ Bind to source address .Ar host when connecting to the server. . +.It Fl T Ar format , Cm timestamp Op = Ar format +Show timestamps by default, +in the specified +.Xr strftime 3 +.Ar format . +The default format is +.Qq \&%X . +. .It Fl a Ar user : Ns Ar pass , Cm sasl-plain = Ar user : Ns Ar pass Authenticate as .Ar user @@ -640,6 +649,8 @@ Insert a blank line in the window. Scroll to next highlight. .It Ic M-p Scroll to previous highlight. +.It Ic M-t +Toggle timestamps. .It Ic M-u Scroll to first unread line. .It Ic M-v @@ -647,7 +658,9 @@ Scroll up a page. .El . .Ss IRC Formatting -.Bl -tag -width Ds -compact +.Bl -tag -width "C-z C-v" -compact +.It Ic C-z C-v +Insert the next input character literally. .It Ic C-z b Toggle bold. .It Ic C-z c @@ -656,6 +669,8 @@ Set or reset color. Toggle italics. .It Ic C-z o Reset formatting. +.It Ic C-z p +Manually toggle paste mode. .It Ic C-z r Toggle reverse color. .It Ic C-z u diff --git a/chat.c b/chat.c index 6458925..60ec7d2 100644 --- a/chat.c +++ b/chat.c @@ -40,6 +40,7 @@ #include <sys/stat.h> #include <sys/wait.h> #include <sysexits.h> +#include <time.h> #include <tls.h> #include <unistd.h> @@ -122,6 +123,7 @@ uint32_t hashBound = 75; static void parseHash(char *str) { hashInit = strtoul(str, &str, 0); if (*str) hashBound = strtoul(&str[1], NULL, 0); + if (hashBound < 2) errx(EX_USAGE, "hash bound must be >= 2"); } #ifdef __OpenBSD__ @@ -197,6 +199,7 @@ int main(int argc, char *argv[]) { { .val = 'O', .name = "open", required_argument }, { .val = 'R', .name = "restrict", no_argument }, { .val = 'S', .name = "bind", required_argument }, + { .val = 'T', .name = "timestamp", optional_argument }, { .val = 'a', .name = "sasl-plain", required_argument }, { .val = 'c', .name = "cert", required_argument }, { .val = 'e', .name = "sasl-external", no_argument }, @@ -234,6 +237,10 @@ int main(int argc, char *argv[]) { break; case 'O': utilPush(&urlOpenUtil, optarg); break; case 'R': self.restricted = true; break; case 'S': bind = optarg; + break; case 'T': { + uiTime.enable = true; + if (optarg) uiTime.format = optarg; + } break; case 'a': sasl = true; self.plain = optarg; break; case 'c': cert = optarg; break; case 'e': sasl = true; diff --git a/chat.h b/chat.h index 6ecd91a..6a21930 100644 --- a/chat.h +++ b/chat.h @@ -262,7 +262,9 @@ enum Reply { ReplyList, ReplyMode, ReplyNames, + ReplyNamesAuto, ReplyTopic, + ReplyTopicAuto, ReplyWho, ReplyWhois, ReplyWhowas, @@ -279,6 +281,12 @@ const char *commandIsAction(uint id, const char *input); void commandCompleteAdd(void); enum Heat { Ice, Cold, Warm, Hot }; +enum { TimeCap = 64 }; +extern struct Time { + bool enable; + const char *format; + int width; +} uiTime; extern struct Util uiNotifyUtil; void uiInitEarly(void); void uiInitLate(void); diff --git a/command.c b/command.c index b1b4af4..1154942 100644 --- a/command.c +++ b/command.c @@ -468,7 +468,7 @@ static void commandHelp(uint id, char *params) { if (pid) return; char buf[256]; - snprintf(buf, sizeof(buf), "%spCOMMANDS$", (getenv("LESS") ?: "")); + snprintf(buf, sizeof(buf), "%sp^COMMANDS$", (getenv("LESS") ?: "")); setenv("LESS", buf, 1); execlp("man", "man", "1", "catgirl", NULL); dup2(utilPipe[1], STDERR_FILENO); diff --git a/edit.c b/edit.c index d9b7673..3e7e1af 100644 --- a/edit.c +++ b/edit.c @@ -114,6 +114,7 @@ static void macroExpand(void) { if (macro == pos) return; for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) { if (wcsncmp(Macros[i].name, &buf[macro], pos - macro)) continue; + if (wcstombs(NULL, Macros[i].string, 0) == (size_t)-1) continue; delete(false, macro, pos - macro); pos = macro; size_t expand = wcslen(Macros[i].string); @@ -259,6 +260,8 @@ void edit(uint id, enum Edit op, wchar_t ch) { } break; case EditInsert: { + char mb[MB_LEN_MAX]; + if (wctomb(mb, ch) < 0) return; if (reserve(pos, 1)) { buf[pos++] = ch; } diff --git a/handle.c b/handle.c index eae5451..d889f8e 100644 --- a/handle.c +++ b/handle.c @@ -244,9 +244,9 @@ static void handleReplyWelcome(struct Message *msg) { if (*ch == ',') count++; } ircFormat("JOIN %s\r\n", self.join); - replies[ReplyJoin] += count; - replies[ReplyTopic] += count; - replies[ReplyNames] += count; + if (count == 1) replies[ReplyJoin]++; + replies[ReplyTopicAuto] += count; + replies[ReplyNamesAuto] += count; } } @@ -529,17 +529,26 @@ static void handleReplyNames(struct Message *msg) { char *user = strsep(&name, "@"); enum Color color = (user ? hash(user) : Default); completeAdd(id, nick, color); - if (!replies[ReplyNames]) continue; + if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue; catf(&cat, "%s\3%02d%s\3", (buf[0] ? ", " : ""), color, prefixes); } if (!cat.len) return; uiFormat( - id, Warm, tagTime(msg), + id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg), "In \3%02d%s\3 are %s", hash(msg->params[2]), msg->params[2], buf ); } +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies[ReplyNamesAuto]) { + replies[ReplyNamesAuto]--; + } else if (replies[ReplyNames]) { + replies[ReplyNames]--; + } +} + static char whoBuf[1024]; static struct Cat whoCat = { whoBuf, sizeof(whoBuf), 0 }; @@ -595,11 +604,10 @@ static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); uint id = idFor(msg->params[1]); topicComplete(id, msg->params[2]); - if (!replies[ReplyTopic]) return; - replies[ReplyTopic]--; + if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return; urlScan(id, NULL, msg->params[2]); uiFormat( - id, Warm, tagTime(msg), + id, (replies[ReplyTopicAuto] ? Cold : Warm), tagTime(msg), "The sign in \3%02d%s\3 reads: %s", hash(msg->params[1]), msg->params[1], msg->params[2] ); @@ -607,6 +615,11 @@ static void handleReplyTopic(struct Message *msg) { id, tagTime(msg), "The sign in %s reads: %s", msg->params[1], msg->params[2] ); + if (replies[ReplyTopicAuto]) { + replies[ReplyTopicAuto]--; + } else { + replies[ReplyTopic]--; + } } static void swap(wchar_t *a, wchar_t *b) { @@ -1275,6 +1288,7 @@ static const struct Handler { { "330", +ReplyWhois, handleReplyWhoisGeneric }, { "331", -ReplyTopic, handleReplyNoTopic }, { "332", 0, handleReplyTopic }, + { "335", +ReplyWhois, handleReplyWhoisGeneric }, { "341", 0, handleReplyInviting }, { "346", +ReplyInvex, handleReplyInviteList }, { "347", -ReplyInvex, NULL }, @@ -1282,7 +1296,7 @@ static const struct Handler { { "349", -ReplyExcepts, NULL }, { "352", +ReplyWho, handleReplyWho }, { "353", 0, handleReplyNames }, - { "366", -ReplyNames, NULL }, + { "366", 0, handleReplyEndOfNames }, { "367", +ReplyBan, handleReplyBanList }, { "368", -ReplyBan, NULL }, { "369", -ReplyWhowas, handleReplyEndOfWhowas }, diff --git a/scripts/chat.tmux.conf b/scripts/chat.tmux.conf index 9191b1a..e58f955 100644 --- a/scripts/chat.tmux.conf +++ b/scripts/chat.tmux.conf @@ -1,31 +1,62 @@ # use `tmux -L chat -f ./chat.tmux.conf attach-session' (without any other # options or parameters) to access this session group in its own tmux server, # not interfering with existing servers/sessions/configurations + new-session -t chat +# catgirl(1) puts windows at the top +set-option -g -- status-position top + # intuitive navigation -set-option -g mode-keys vi -set-option -g mouse on +set-option -g -- mode-keys vi +set-option -g -- mouse on # indicate new messages -set-option -g monitor-activity on -set-option -g monitor-bell on +set-option -g -- monitor-activity on +set-option -g -- monitor-bell on # hardcode names during window creation -set-option -g automatic-rename off -set-option -g allow-rename off -set-option -g set-titles off -set-option -g renumber-windows on +set-option -g -- automatic-rename off +set-option -g -- allow-rename off +set-option -g -- set-titles off +set-option -g -- renumber-windows on # clients exit on network errors, restart them automatically # (use `kill-pane'/`C-b x' to destroy windows) -set-option -g remain-on-exit on -set-hook -g pane-died respawn-pane +set-option -g -- remain-on-exit on +set-hook -g -- pane-died respawn-pane + + +# disarm ^C to avoid accidentially losing logs +bind-key -n -N 'confirm INTR key' -- C-c \ + confirm-before -p 'Send ^C? (y/N)' -- 'send-keys -- C-c' + +# one-click version of default `C-b w' (shows preview windows) +bind-key -n -N 'pick chat network' -- F1 choose-tree -Z + +# catgirl(1) might run in `-R'/`restrict'ed mode, i.e. `/help' is disabled +bind-key -n -N 'read catgirl help' -- F2 \ + new-window -S -n help -- man -s 1 -- catgirl + +# intuitive refresh, just don't spam it ;-) +bind-key -n -N 'reconnect network' -- F5 \ + confirm-before -p 'reconnect network? (y/N)' -- 'respawn-pane -k' + +# immersive mode ;-) +bind-key -n -N 'toggle fullscreen' -- F11 set status + + +# this configuration is idempotent, i.e. reloading it only changes settings +# and never duplicates already existing windows +bind-key -N 'reload configuration' -- R { + source-file -- ./chat.tmux.conf + display-message -- 'configuration reloaded' +} ## do not double-quote commands to avoid running through "sh -c" # IRC -new-window -n efnet -- catgirl efnet -new-window -n freenode -- catgirl freenode -new-window -n hackint -- catgirl hackint +new-window -d -S -n hackint -- catgirl -- defaults hackint +new-window -d -S -n freenode -- catgirl -- defaults freenode +new-window -d -S -n efnet -- catgirl -- defaults efnet diff --git a/ui.c b/ui.c index 5d3f070..d18ea74 100644 --- a/ui.c +++ b/ui.c @@ -53,10 +53,6 @@ #undef lines #undef tab -#ifndef A_ITALIC -#define A_ITALIC A_NORMAL -#endif - enum { StatusLines = 1, MarkerLines = 1, @@ -78,6 +74,7 @@ struct Window { int scroll; bool mark; bool mute; + bool time; enum Heat thresh; enum Heat heat; uint unreadSoft; @@ -129,15 +126,13 @@ 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; window->thresh = Cold; window->buffer = bufferAlloc(); - return windowPush(window); } @@ -199,6 +194,7 @@ static short colorPair(short fg, short bg) { X(KeyMetaN, "\33n", NULL) \ X(KeyMetaP, "\33p", NULL) \ X(KeyMetaQ, "\33q", NULL) \ + X(KeyMetaT, "\33t", NULL) \ X(KeyMetaU, "\33u", NULL) \ X(KeyMetaV, "\33v", NULL) \ X(KeyMetaEnter, "\33\r", "\33\n") \ @@ -211,7 +207,8 @@ static short colorPair(short fg, short bg) { X(KeyFocusIn, "\33[I", NULL) \ X(KeyFocusOut, "\33[O", NULL) \ X(KeyPasteOn, "\33[200~", NULL) \ - X(KeyPasteOff, "\33[201~", NULL) + X(KeyPasteOff, "\33[201~", NULL) \ + X(KeyPasteManual, "\32p", "\32\20") enum { KeyMax = KEY_MAX, @@ -222,14 +219,14 @@ enum { // XXX: Assuming terminals will be fine with these even if they're unsupported, // since they're "private" modes. -static const char *EnterFocusMode = "\33[?1004h"; -static const char *ExitFocusMode = "\33[?1004l"; -static const char *EnterPasteMode = "\33[?2004h"; -static const char *ExitPasteMode = "\33[?2004l"; +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(ExitFocusMode); - putp(ExitPasteMode); + putp(FocusMode[false]); + putp(PasteMode[false]); reset_shell_mode(); } @@ -240,6 +237,13 @@ void uiInitEarly(void) { colorInit(); atexit(errExit); +#ifndef A_ITALIC +#define A_ITALIC A_BLINK + // Force ncurses to use individual enter_attr_mode strings: + set_attributes = NULL; + enter_blink_mode = enter_italics_mode; +#endif + if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; from_status_line = "\7"; @@ -255,6 +259,16 @@ void uiInitEarly(void) { main = newwin(MAIN_LINES, COLS, StatusLines, 0); if (!main) err(EX_OSERR, "newwin"); + int y; + char buf[TimeCap]; + struct tm *time = localtime(&(time_t) { -22100400 }); + size_t len = strftime(buf, sizeof(buf), uiTime.format, time); + if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", uiTime.format); + waddstr(main, buf); + waddch(main, ' '); + getyx(main, y, uiTime.width); + (void)y; + input = newpad(InputLines, InputCols); if (!input) err(EX_OSERR, "newpad"); keypad(input, true); @@ -386,14 +400,11 @@ static void statusUpdate(void) { char buf[256] = ""; struct Cat cat = { buf, sizeof(buf), 0 }; catf( - &cat, "\3%d%s %u ", - idColors[window->id], (num == windows.show ? "\26" : ""), num + &cat, "\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->thresh != Cold || window->mute) { - const char *thresh[] = { "-", "", "+", "++" }; - catf(&cat, "%s%s ", thresh[window->thresh], &"="[!window->mute]); - } - catf(&cat, "%s ", idNames[window->id]); if (window->mark && window->unreadWarm) { catf( &cat, "\3%d+%d\3%d%s", @@ -438,8 +449,8 @@ static void unmark(struct Window *window) { void uiShow(void) { if (!hidden) return; prevTitle[0] = '\0'; - putp(EnterFocusMode); - putp(EnterPasteMode); + putp(FocusMode[true]); + putp(PasteMode[true]); fflush(stdout); hidden = false; unmark(windows.ptrs[windows.show]); @@ -449,8 +460,8 @@ void uiHide(void) { if (hidden) return; mark(windows.ptrs[windows.show]); hidden = true; - putp(ExitFocusMode); - putp(ExitPasteMode); + putp(FocusMode[false]); + putp(PasteMode[false]); endwin(); } @@ -466,12 +477,35 @@ static size_t windowBottom(const struct Window *window) { return bottom; } -static void mainAdd(int y, const char *str) { +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); - styleAdd(main, str); + if (!line || !line->str[0]) { + wclrtoeol(main); + return; + } + if (time && line->time) { + char buf[TimeCap]; + strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time)); + wattr_set( + main, + colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), + NULL + ); + waddstr(main, buf); + waddch(main, ' '); + } else if (time) { + whline(main, ' ', uiTime.width); + wmove(main, y, uiTime.width); + } + styleAdd(main, line->str); getyx(main, ny, nx); - if (ny == y) wclrtoeol(main); + if (ny != y) return; + wclrtoeol(main); (void)nx; } @@ -481,16 +515,14 @@ static void mainUpdate(void) { int y = 0; int marker = MAIN_LINES - SplitLines - MarkerLines; for (size_t i = windowTop(window); i < BufferCap; ++i) { - const struct Line *line = bufferHard(window->buffer, i); - mainAdd(y++, (line ? line->str : "")); + 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) { - const struct Line *line = bufferHard(window->buffer, i); - mainAdd(y++, (line ? line->str : "")); + mainAdd(y++, window->time, bufferHard(window->buffer, i)); } wattr_set(main, A_NORMAL, 0, NULL); mvwhline(main, marker, 0, ACS_BULLET, COLS); @@ -540,7 +572,10 @@ void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) { } if (window->mark && heat > Cold) { if (!window->unreadWarm++) { - int lines = bufferPush(window->buffer, COLS, false, Warm, ts, ""); + int lines = bufferPush( + window->buffer, windowCols(window), + window->thresh, Warm, ts, "" + ); if (window->scroll) windowScroll(window, lines); if (window->unreadSoft > 1) { window->unreadSoft++; @@ -550,7 +585,10 @@ void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) { if (heat > window->heat) window->heat = heat; statusUpdate(); } - int lines = bufferPush(window->buffer, COLS, window->thresh, heat, ts, str); + 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(); @@ -583,7 +621,8 @@ static void windowReflow(struct Window *window) { const struct Line *line = bufferHard(window->buffer, windowTop(window)); if (line) num = line->num; window->unreadHard = bufferReflow( - window->buffer, COLS, window->thresh, window->unreadSoft + window->buffer, windowCols(window), + window->thresh, window->unreadSoft ); if (!window->scroll || !num) return; for (size_t i = 0; i < BufferCap; ++i) { @@ -595,12 +634,12 @@ static void windowReflow(struct Window *window) { } static void resize(void) { - statusUpdate(); wclear(main); wresize(main, MAIN_LINES, COLS); for (uint num = 0; num < windows.len; ++num) { windowReflow(windows.ptrs[num]); } + statusUpdate(); mainUpdate(); } @@ -620,13 +659,10 @@ static void windowList(const struct Window *window) { continue; } - struct tm *tm = localtime(&line->time); - if (!tm) err(EX_OSERR, "localtime"); - - char buf[sizeof("00:00:00")]; - strftime(buf, sizeof(buf), "%T", tm); + 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); + printf("%s ", buf); bool align = false; struct Style style = StyleDefault; @@ -679,7 +715,7 @@ static void inputAdd(struct Style *style, const char *str) { static void inputUpdate(void) { size_t pos; char *buf = editBuffer(&pos); - uint id = windows.ptrs[windows.show]->id; + struct Window *window = windows.ptrs[windows.show]; const char *prefix = ""; const char *prompt = self.nick; @@ -688,9 +724,9 @@ static void inputUpdate(void) { struct Style stylePrompt = { .fg = self.color, .bg = Default }; struct Style styleInput = StyleDefault; - const char *privmsg = commandIsPrivmsg(id, buf); - const char *notice = commandIsNotice(id, buf); - const char *action = commandIsAction(id, buf); + const char *privmsg = commandIsPrivmsg(window->id, buf); + const char *notice = commandIsNotice(window->id, buf); + const char *action = commandIsAction(window->id, buf); if (privmsg) { prefix = "<"; suffix = "> "; skip = privmsg; @@ -703,7 +739,7 @@ static void inputUpdate(void) { stylePrompt.attr |= Italic; styleInput.attr |= Italic; skip = action; - } else if (id == Debug && buf[0] != '/') { + } else if (window->id == Debug && buf[0] != '/') { prompt = "<< "; stylePrompt.fg = Gray; } else { @@ -716,6 +752,10 @@ 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); + } wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), NULL); waddstr(input, prefix); waddstr(input, prompt); @@ -732,11 +772,12 @@ static void inputUpdate(void) { } static void windowShow(uint num) { - windows.user = num; - if (windows.show == num) return; - windows.swap = windows.show; + if (num != windows.show) { + windows.swap = windows.show; + mark(windows.ptrs[windows.swap]); + } windows.show = num; - mark(windows.ptrs[windows.swap]); + windows.user = num; unmark(windows.ptrs[windows.show]); mainUpdate(); inputUpdate(); @@ -787,6 +828,14 @@ 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); @@ -807,6 +856,14 @@ static void scrollSearch(struct Window *window, const char *str, int dir) { } } +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) { @@ -815,6 +872,7 @@ static void incThresh(struct Window *window, int n) { window->thresh += n; } windowReflow(window); + statusUpdate(); mainUpdate(); statusUpdate(); } @@ -862,7 +920,7 @@ static void keyCode(int code) { break; case KeyMetaSlash: windowShow(windows.swap); break; case KeyMetaGt: scrollTo(window, 0); - break; case KeyMetaLt: scrollTo(window, BufferCap); + break; case KeyMetaLt: scrollTop(window); break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0); break; case KeyMetaA: showAuto(); @@ -874,6 +932,7 @@ static void keyCode(int code) { break; case KeyMetaN: scrollHot(window, +1); break; case KeyMetaP: scrollHot(window, -1); break; case KeyMetaQ: edit(id, EditCollapse, 0); + break; case KeyMetaT: toggleTime(window); break; case KeyMetaU: scrollTo(window, window->unreadHard); break; case KeyMetaV: scrollPage(window, +1); @@ -946,19 +1005,24 @@ void uiRead(void) { } wint_t ch; - static bool paste, style; + static bool paste, style, literal; for (int ret; ERR != (ret = wget_wch(input, &ch));) { if (ret == KEY_CODE_YES && ch == KeyPasteOn) { paste = true; } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) { paste = false; - } else if (paste) { + } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { + paste ^= true; + } else if (paste || literal) { edit(windows.ptrs[windows.show]->id, EditInsert, ch); } else if (ret == KEY_CODE_YES) { keyCode(ch); } else if (ch == (L'Z' ^ L'@')) { style = true; continue; + } else if (style && ch == (L'V' ^ L'@')) { + literal = true; + continue; } else if (style) { keyStyle(ch); } else if (iswcntrl(ch)) { @@ -967,6 +1031,7 @@ void uiRead(void) { edit(windows.ptrs[windows.show]->id, EditInsert, ch); } style = false; + literal = false; } inputUpdate(); } @@ -978,7 +1043,8 @@ static const time_t Signatures[] = { 0x6C72696774616304, // no mute 0x6C72696774616305, // no URLs 0x6C72696774616306, // no thresh - 0x6C72696774616307, + 0x6C72696774616307, // no window time + 0x6C72696774616308, }; static size_t signatureVersion(time_t signature) { @@ -1000,7 +1066,7 @@ int uiSave(const char *name) { if (!file) return -1; int error = 0 - || writeTime(file, Signatures[6]) + || writeTime(file, Signatures[7]) || writeTime(file, self.pos); if (error) return error; for (uint num = 0; num < windows.len; ++num) { @@ -1008,6 +1074,7 @@ int uiSave(const char *name) { 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) @@ -1072,6 +1139,7 @@ void uiLoad(const char *name) { while (0 < readString(file, &buf, &cap) && buf[0]) { struct Window *window = windows.ptrs[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); |