zygo.c (25123B)
1 /* 2 * zygo/zygo.c 3 * 4 * Copyright (c) 2022 hhvn <dev@hhvn.uk> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 * 18 */ 19 20 #define _XOPEN_SOURCE_EXTENDED /* ncurses wchar wants this sometimes */ 21 #include <ncurses.h> 22 #include <stdlib.h> 23 #include <string.h> 24 #include <stdarg.h> 25 #include <libgen.h> 26 #include <assert.h> 27 #include <locale.h> 28 #include <signal.h> 29 #include <unistd.h> 30 #include <limits.h> 31 #include <regex.h> 32 #include <ctype.h> 33 #include <stdio.h> 34 #include <wchar.h> 35 #include <sys/wait.h> 36 #include "zygo.h" 37 #include "config.h" 38 39 Elem *history = NULL; 40 Elem *page = NULL; 41 Elem *current = NULL; 42 int insecure = 0; 43 44 #define TLSOPTS "ku" 45 46 struct { 47 int scroll; 48 int wantinput; /* 0 - no 49 * 1 - yes (with cmd) 50 * 2 - yes (id) */ 51 wchar_t input[BUFLEN]; 52 char cmd; 53 char arg[BUFLEN * 4]; /* UTF8 max char size: 4 bytes. 4x sizeof(input) */ 54 int search; 55 regex_t regex; 56 int error; 57 char errorbuf[BUFLEN]; 58 } ui = {.scroll = 0, 59 .wantinput = 0, 60 .search = 0, 61 .error = 0}; 62 63 /* 64 * Memory functions 65 */ 66 void * 67 emalloc(size_t size) { 68 void *mem; 69 70 if ((mem = malloc(size)) == NULL) { 71 perror("malloc()"); 72 exit(EXIT_FAILURE); 73 } 74 75 return mem; 76 } 77 78 void * 79 erealloc(void *ptr, size_t size) { 80 void *mem; 81 82 if ((mem = realloc(ptr, size)) == NULL) { 83 perror("realloc()"); 84 exit(EXIT_FAILURE); 85 } 86 87 return mem; 88 } 89 90 char * 91 estrdup(const char *str) { 92 char *ret; 93 94 if ((ret = strdup(str)) == NULL) { 95 perror("strdup()"); 96 exit(EXIT_FAILURE); 97 } 98 99 return ret; 100 } 101 102 /* 103 * Elem functions 104 */ 105 void 106 elem_free(Elem *e) { 107 if (e) { 108 free(e->desc); 109 free(e->selector); 110 free(e->server); 111 free(e->port); 112 free(e); 113 } 114 } 115 116 Elem * 117 elem_create(int tls, char type, char *desc, char *selector, char *server, char *port) { 118 Elem *ret = emalloc(sizeof(Elem)); 119 ret->tls = tls; 120 #define DUP(str) str ? estrdup(str) : NULL 121 ret->type = type; 122 ret->desc = DUP(desc); 123 ret->selector = DUP(selector); 124 ret->server = DUP(server); 125 ret->port = DUP(port); 126 ret->id = ret->len = ret->lastid = 0; 127 ret->next = NULL; 128 #undef DUP 129 return ret; 130 } 131 132 Elem * 133 elem_dup(Elem *e) { 134 return (e ? elem_create(e->tls, e->type, e->desc, e->selector, e->server, e->port) : NULL); 135 } 136 137 char * 138 elemtouri(Elem *e) { 139 static char ret[BUFLEN]; 140 char type[2] = {0, 0}; 141 142 ret[0] = '\0'; 143 144 switch (e->type) { 145 case 'T': 146 case '8': 147 strlcat(ret, "teln://", sizeof(ret)); 148 break; 149 case 'h': 150 if (strncmp(e->selector, "URL:", strlen("URL:")) == 0) { 151 strlcat(ret, e->selector + strlen("URL:"), sizeof(ret)); 152 return ret; 153 } 154 else if (strncmp(e->selector, "/URL:", strlen("/URL:")) == 0) { 155 strlcat(ret, e->selector + strlen("URL:"), sizeof(ret)); 156 return ret; 157 } 158 /* fallthrough */ 159 default: 160 strlcat(ret, e->tls ? "gophers://" : "gopher://", sizeof(ret)); 161 break; 162 } 163 164 zygo_assert(e->server); 165 zygo_assert(e->port); 166 167 strlcat(ret, e->server, sizeof(ret)); 168 if (strcmp(e->port, "70") != 0) { 169 strlcat(ret, ":", sizeof(ret)); 170 strlcat(ret, e->port, sizeof(ret)); 171 } 172 173 type[0] = e->type; 174 strlcat(ret, "/", sizeof(ret)); 175 strlcat(ret, type, sizeof(ret)); 176 177 if (e->selector && *e->selector && strcmp(e->selector, "/") != 0) 178 strlcat(ret, e->selector, sizeof(ret)); 179 180 return ret; 181 } 182 183 Elem * 184 uritoelem(const char *uri) { 185 Elem *ret; 186 char *dup = estrdup(uri); 187 char *tmp = dup; 188 char *serv = NULL; 189 char *p; 190 enum {SEGSERVER, SEGTYPE, SEGSELECTOR} seg; 191 192 ret = elem_create(0, '1', NULL, NULL, NULL, NULL); 193 194 if (strncmp(tmp, "gopher://", strlen("gopher://")) == 0) { 195 tmp += strlen("gopher://"); 196 } else if (strncmp(tmp, "gophers://", strlen("gophers://")) == 0) { 197 #ifdef TLS 198 ret->tls = 1; 199 tmp += strlen("gophers://"); 200 #else 201 error("TLS support not compiled"); 202 free(ret); 203 ret = NULL; 204 goto end; 205 #endif /* TLS */ 206 } else if (strstr(tmp, "://")) { 207 error("non-gopher protocol entered"); 208 free(ret); 209 ret = NULL; 210 goto end; 211 } 212 213 for (p = tmp, seg = SEGSERVER; *p; p++) { 214 if (seg == SEGSELECTOR || *p == '\t') { 215 ret->selector = estrdup(p); 216 if (seg == SEGSERVER) { 217 *p = '\0'; 218 serv = tmp; 219 } 220 break; 221 } else if (seg == SEGSERVER && *p == '/') { 222 *p = '\0'; 223 serv = tmp; 224 tmp = p + 1; 225 seg = SEGTYPE; 226 } else if (seg == SEGTYPE) { 227 ret->type = *p; 228 tmp = p + 1; 229 break; 230 } 231 } 232 233 if (!serv && seg == SEGSERVER) { 234 serv = tmp; 235 tmp += strlen(tmp); 236 } 237 238 if (*serv == '[' && (p = strstr(serv + 1, "]:"))) { /* ipv6 + port */ 239 *p = '\0'; 240 ret->server = estrdup(serv + 1); 241 ret->port = estrdup(p + 2); 242 } else if ((p = strchr(serv, ':')) == strrchr(serv, ':') && p) { /* only one : == ipv4 + port */ 243 *p = '\0'; 244 ret->server = estrdup(serv); 245 ret->port = estrdup(p + 1); 246 } else { /* no port */ 247 ret->server = estrdup(serv); 248 ret->port = estrdup("70"); 249 } 250 251 if (!ret->selector) 252 ret->selector = estrdup(tmp); 253 254 end: 255 free(dup); 256 return ret; 257 } 258 259 Elem * 260 gophertoelem(Elem *from, const char *line) { 261 Elem *ret; 262 char *dup = estrdup(line); 263 char *tmp = dup; 264 char *p; 265 enum {SEGDESC, SEGSELECTOR, SEGSERVER, SEGPORT} seg; 266 267 ret = elem_create(0, *(tmp++), NULL, NULL, NULL, NULL); 268 269 for (p = tmp, seg = SEGDESC; *p; p++) { 270 if (*p == '\t') { 271 *p = '\0'; 272 switch (seg) { 273 case SEGDESC: ret->desc = estrdup(tmp); break; 274 case SEGSELECTOR: ret->selector = estrdup(tmp); break; 275 case SEGSERVER: ret->server = estrdup(tmp); break; 276 case SEGPORT: ret->port = estrdup(tmp); break; 277 } 278 tmp = p + 1; 279 seg++; 280 } 281 } 282 283 /* ret->port will only be set on gopher+ menus with 284 * the above loop, set it here for non-gopher+ */ 285 if (!ret->port) 286 ret->port = estrdup(tmp); 287 if (from && from->tls && ret->server && ret->port && 288 strcmp(ret->server, from->server) == 0 && 289 strcmp(ret->port, from->port) == 0) 290 ret->tls = 1; 291 else 292 ret->tls = 0; 293 294 free(dup); 295 296 if (ret->desc != NULL && 297 ret->server != NULL && 298 ret->port != NULL) 299 return ret; 300 301 free(ret->desc); 302 free(ret->selector); 303 free(ret->server); 304 free(ret->port); 305 ret->type = '3'; 306 ret->desc = estrdup("invalid gopher menu element"); 307 ret->selector = estrdup("Err"); 308 ret->server = estrdup("Err"); 309 ret->port = estrdup("Err"); 310 return ret; 311 } 312 313 /* 314 * List functions 315 */ 316 void 317 list_free(Elem **l) { 318 Elem *prev, *p; 319 320 if (!l || !*l) 321 return; 322 for (prev = *l, p = prev->next; p; p = p->next) { 323 elem_free(prev); 324 prev = p; 325 } 326 *l = NULL; 327 } 328 329 void 330 list_append(Elem **l, Elem *e) { 331 Elem *elem, *p; 332 333 zygo_assert(l); 334 elem = elem_dup(e); 335 336 if (!*l) { 337 (*l) = elem; 338 (*l)->len = 1; 339 (*l)->lastid = 0; /* incremented later */ 340 } else { 341 for (p = *l; p && p->next; p = p->next); 342 p->next = elem; 343 (*l)->len++; 344 } 345 346 if (elem->type != 'i' && elem->type != '3') 347 elem->id = ++(*l)->lastid; 348 } 349 350 Elem * 351 list_get(Elem **l, size_t elem) { 352 Elem *p; 353 if (!l || !(*l) || (*l)->len == 0 || elem >= (*l)->len) 354 return NULL; 355 for (p = *l; p && elem; elem--, p = p->next); 356 return p; 357 } 358 359 Elem * 360 list_idget(Elem **l, size_t id) { 361 Elem *p; 362 if (!l || !(*l) || (*l)->len == 0 || id > (*l)->lastid) 363 return NULL; 364 for (p = *l; p && id; p = p->next) 365 if (p->type != 'i' && p->type != '3') 366 if (!--id) 367 break; 368 return p; 369 } 370 371 size_t 372 list_len(Elem **l) { 373 if (!l || !(*l)) 374 return 0; 375 return (*l)->len; 376 } 377 378 void 379 list_rev(Elem **l) { 380 Elem *p, *prev, *next; 381 size_t len, lastid; 382 383 if (!l || !*l) 384 return; 385 386 len = (*l)->len; 387 lastid = (*l)->lastid; 388 389 for (p = *l, prev = NULL; p; p = next) { 390 next = p->next; 391 p->next = prev; 392 prev = p; 393 if (p->id) 394 p->id = lastid - p->id + 1; 395 } 396 397 prev->len = len; 398 prev->lastid = lastid; 399 400 *l = prev; 401 } 402 403 /* 404 * Misc functions 405 */ 406 int 407 readline(char *buf, size_t count) { 408 size_t i = 0; 409 char c = 0; 410 411 while (i < count && c != '\n') { 412 if (net_read(&c, sizeof(char)) < 1) 413 return 0; 414 buf[i++] = c; 415 } 416 417 buf[i - 1] = '\0'; 418 return 1; 419 } 420 421 int 422 go(Elem *e, int mhist, int notls) { 423 char line[BUFLEN]; 424 char *uri; 425 char *pstr; 426 Elem *elem; 427 Elem *dup = elem_dup(e); /* elem may be part of page */ 428 Elem missing = {0, '3', "Full contents not received."}; 429 int ret; 430 int gotall = 0; 431 pid_t pid; 432 433 if (!e) return -1; 434 435 if (dup->type != '0' && dup->type != '1' && dup->type != '7' && dup->type != '+') { 436 /* call mario */ 437 uri = elemtouri(e); 438 439 if (!parallelplumb) 440 endwin(); 441 442 if ((pid = fork()) == 0) { 443 if (parallelplumb) { 444 close(1); 445 close(2); 446 } 447 execlp(plumber, plumber, uri, NULL); 448 } 449 zygo_assert(pid != -1); 450 451 if (!parallelplumb) { 452 waitpid(pid, NULL, 0); 453 fprintf(stderr, "Press enter..."); 454 fread(&line, sizeof(char), 1, stdin); 455 initscr(); 456 } 457 return -1; 458 } 459 460 if (dup->type == '7' && !strchr(dup->selector, '\t')) { 461 if ((pstr = prompt("Query: ", 0)) == NULL) { 462 elem_free(dup); 463 return -1; 464 } 465 466 free(dup->selector); 467 dup->selector = emalloc(strlen(e->selector) + strlen(pstr) + 2); 468 snprintf(dup->selector, strlen(e->selector) + strlen(pstr) + 2, 469 "%s\t%s", e->selector, pstr); 470 } 471 472 move(LINES - 1, 0); 473 clrtoeol(); 474 #ifdef TLS 475 if (!dup->tls && autotls && !notls && 476 (!current || strcmp(current->server, dup->server) != 0)) { 477 dup->tls = 1; 478 printw("Attempting a TLS connection with %s:%s", dup->server, dup->port); 479 } else { 480 #endif /* TLS */ 481 printw("Connecting to %s:%s", dup->server, dup->port); 482 #ifdef TLS 483 } 484 #endif /* TLS */ 485 refresh(); 486 487 if ((ret = net_connect(dup, e->tls != dup->tls)) == -1) { 488 if (dup->tls && dup->tls == e->tls) { 489 timeout(stimeout * 1000); 490 pstr = prompt("TLS failed. Retry in cleartext (y/n)? ", 1); 491 if (pstr && tolower(*pstr) == 'y') { 492 dup->tls = 0; 493 ui.error = 0; /* hide the TLS error */ 494 ret = go(dup, mhist, 1); 495 } 496 timeout(-1); 497 } else if (dup->tls) { 498 dup->tls = 0; 499 ret = go(dup, mhist, 1); 500 } 501 502 elem_free(dup); 503 return ret; 504 } 505 506 net_write(dup->selector, strlen(dup->selector)); 507 net_write("\r\n", 2); 508 509 list_free(&page); 510 while (readline(line, sizeof(line))) { 511 if (strcmp(line, ".\r") == 0) { 512 gotall = 1; 513 } else { 514 if (line[strlen(line) - 1] == '\r') 515 line[strlen(line) - 1] = '\0'; 516 if (dup->type == '0') 517 elem = elem_create(0, 'i', line, NULL, NULL, NULL); 518 else 519 elem = gophertoelem(dup, line); 520 list_append(&page, elem); 521 elem_free(elem); 522 } 523 } 524 525 if (!gotall && dup->type != '0') 526 list_append(&page, &missing); 527 528 elem_free(current); 529 current = dup; 530 if (mhist) 531 list_append(&history, current); 532 533 ui.scroll = 0; 534 if (ui.search) { 535 regfree(&ui.regex); 536 ui.search = 0; 537 } 538 539 return 0; 540 } 541 542 int 543 digits(int i) { 544 int ret = 0; 545 546 do { 547 ret++; 548 i /= 10; 549 } while (i != 0); 550 551 return ret; 552 } 553 554 /* 555 * UI functions 556 */ 557 void 558 error(char *format, ...) { 559 va_list ap; 560 561 ui.error = 1; 562 563 va_start(ap, format); 564 vsnprintf(ui.errorbuf, sizeof(ui.errorbuf), format, ap); 565 va_end(ap); 566 567 draw_bar(); 568 } 569 570 Scheme * 571 getscheme(Elem *e) { 572 char type; 573 int i; 574 575 type = e->type; 576 if (type == 'h' && strstr(e->selector, "URL:")) 577 type = EXTR; 578 579 /* Try to get scheme from markdown header */ 580 if (type == 'i' && mdhilight) { 581 /* 4+ matches MDH4 */ 582 if (strncmp(e->desc, "####", 4) == 0) 583 type = MDH4; 584 else if (strncmp(e->desc, "###", 3) == 0) 585 type = MDH3; 586 else if (strncmp(e->desc, "##", 2) == 0) 587 type = MDH2; 588 else if (strncmp(e->desc, "#", 1) == 0) 589 type = MDH1; 590 } 591 592 for (i = 0; ; i++) 593 if (scheme[i].type == type || scheme[i].type == DEFL) 594 return &scheme[i]; 595 } 596 597 void 598 find(int backward) { 599 enum {mfirst, mclose, mlast}; 600 struct { 601 size_t pos; 602 int found; 603 } matches[] = { 604 [mfirst] = {.found = 0}, 605 [mclose] = {.found = 0}, 606 [mlast] = {.found = 0}, 607 }; 608 size_t i; 609 size_t want; 610 Elem *e; 611 612 if (!ui.search) { 613 error("no search"); 614 return; 615 } 616 617 for (i = 0, e = page; i < list_len(&page); i++, e = e->next) { 618 if (regexec(&ui.regex, e->desc, 0, NULL, 0) == 0) { 619 matches[mlast].found = 1; 620 matches[mlast].pos = i; 621 if (!matches[mfirst].found) { 622 matches[mfirst].found = 1; 623 matches[mfirst].pos = i; 624 } 625 if (!matches[mclose].found && ((backward && i < ui.scroll) || (!backward && i > ui.scroll))) { 626 matches[mclose].found = 1; 627 matches[mclose].pos = i; 628 } 629 } 630 } 631 632 if (matches[mfirst].found == 0 && 633 matches[mclose].found == 0 && 634 matches[mlast].found == 0) { 635 error("no match"); 636 return; 637 } 638 639 if (matches[mclose].found) 640 want = matches[mclose].pos; 641 else if (backward && matches[mlast].found) 642 want = matches[mlast].pos; 643 else 644 want = matches[mfirst].pos; 645 646 ui.scroll = want; 647 } 648 649 int 650 draw_line(Elem *e, int nwidth) { 651 int y, x, len; 652 wchar_t *mbdesc, *p; 653 654 if (nwidth) 655 attron(COLOR_PAIR(PAIR_EID)); 656 657 if (e->type != 'i' && e->type != '3') 658 printw("%1$ *2$ld ", e->id, nwidth + 1); 659 else if (nwidth) 660 printw("%1$ *2$s ", "", nwidth + 1); 661 662 if (nwidth) { 663 attroff(A_COLOR); 664 attron(COLOR_PAIR(getscheme(e)->pair)); 665 printw("%s ", getscheme(e)->name); 666 attroff(A_COLOR); 667 printw("%s ", normsep); 668 } else { 669 attroff(A_COLOR); 670 } 671 672 if (ui.search && regexec(&ui.regex, e->desc, 0, NULL, 0) == 0) 673 attron(A_REVERSE); 674 675 if (mdhilight && strncmp(e->desc, "#", 1) == 0) { 676 attron(A_BOLD); 677 attron(COLOR_PAIR(getscheme(e)->pair)); 678 attroff(A_BOLD); 679 } 680 681 len = mbstowcs(NULL, e->desc, 0) + 1; 682 mbdesc = emalloc(len * sizeof(wchar_t*)); 683 mbstowcs(mbdesc, e->desc, len); 684 685 getyx(stdscr, y, x); 686 for (p = mbdesc; *p; p++) { 687 if (*p == L'\t') 688 x += 8; 689 else 690 x++; 691 if (x >= COLS) { 692 attron(A_REVERSE); 693 printw("%s", toolong); 694 goto end; 695 } 696 addnwstr(p, 1); 697 } 698 699 printw("\n"); 700 end: 701 free(mbdesc); 702 attroff(A_REVERSE); 703 return y + 1; 704 } 705 706 void 707 draw_page(void) { 708 int y = 0, i; 709 int nwidth; 710 Elem *e; 711 712 attroff(A_COLOR); 713 if (page) { 714 if (!current || current->type != '0') 715 nwidth = digits(page->lastid); 716 else 717 nwidth = 0; 718 move(0, 0); 719 if (ui.scroll > list_len(&page)) 720 ui.scroll = 0; 721 for (i = ui.scroll, e = list_get(&page, i); i <= list_len(&page) - 1 && y != LINES - 1; i++, e = e->next) 722 y = draw_line(e, nwidth); 723 for (; y < LINES - 1; y++) { 724 move(y, 0); 725 clrtoeol(); 726 } 727 } 728 } 729 730 void 731 draw_bar(void) { 732 int savey, savex, x; 733 734 move(LINES - 1, 0); 735 clrtoeol(); 736 if (current) { 737 attron(COLOR_PAIR(PAIR_URI)); 738 printw(" %s ", elemtouri(current)); 739 } 740 attron(COLOR_PAIR(PAIR_BAR)); 741 printw(" "); 742 if (ui.error) { 743 curs_set(0); 744 attron(COLOR_PAIR(PAIR_ERR)); 745 printw("%s", ui.errorbuf); 746 } else if (ui.wantinput) { 747 curs_set(1); 748 if (ui.cmd) { 749 attron(COLOR_PAIR(PAIR_CMD)); 750 printw("%c", ui.cmd); 751 } 752 attron(COLOR_PAIR(PAIR_ARG)); 753 printw("%s", ui.arg); 754 } else curs_set(0); 755 756 attron(COLOR_PAIR(PAIR_BAR)); 757 getyx(stdscr, savey, savex); 758 for (x = savex; x < COLS; x++) 759 addch(' '); 760 move(savey, savex); 761 } 762 763 void 764 manpage(void) { 765 pid_t pid; 766 int status; 767 char buf; 768 769 endwin(); 770 771 pid = fork(); 772 if (pid == 0) 773 execlp("man", "man", "zygo", NULL); 774 assert(pid != -1); 775 waitpid(pid, &status, 0); 776 if (WEXITSTATUS(status) != 0) { 777 fprintf(stderr, "%s", "could not find manpage, press enter to continue..."); 778 fread(&buf, sizeof(char), 1, stdin); 779 } 780 781 initscr(); 782 draw_page(); 783 draw_bar(); 784 } 785 786 /* 0 - clear 787 * KEY_BACKSPACE - remove character 788 * other - append character */ 789 void 790 input(int c) { 791 static size_t il = 0; 792 793 if (!c) { 794 ui.input[il = 0] = '\0'; 795 ui.arg[0] = '\0'; 796 return; 797 } else if (c == KEY_BACKSPACE) { 798 ui.input[--il] = '\0'; 799 } else if (il == sizeof(ui.input)) { 800 return; 801 } else { 802 ui.input[il++] = c; 803 ui.input[il] = '\0'; 804 } 805 806 wcstombs(ui.arg, ui.input, sizeof(ui.arg)); 807 } 808 809 char * 810 prompt(char *prompt, size_t count) { 811 wint_t c; 812 int ret; 813 int x, y; 814 815 attrset(A_NORMAL); 816 input(0); 817 curs_set(1); 818 goto start; 819 while ((ret = get_wch(&c)) != ERR) { 820 if (c == KEY_RESIZE) { 821 start: 822 draw_page(); 823 move(LINES - 1, 0); 824 clrtoeol(); 825 printw("%s%s", prompt, ui.arg); 826 } else if (c == 27 /* escape */) { 827 return NULL; 828 } else if (c == '\n') { 829 goto end; 830 } else if (c == KEY_BACKSPACE || c == 127) { 831 if (ui.input[0]) { 832 getyx(stdscr, y, x); 833 move(LINES - 1, x - 1); 834 addch(' '); 835 move(LINES - 1, x - 1); 836 refresh(); 837 input(KEY_BACKSPACE); 838 } 839 } else if (c >= 32 && c < KEY_CODE_YES) { 840 addnwstr((wchar_t *)&c, 1); 841 input(c); 842 } 843 844 if (count && wcslen(ui.input) == count) 845 goto end; 846 } 847 848 end: 849 return ui.arg; 850 } 851 852 Elem * 853 strtolink(char *str) { 854 if (atoi(str) > page->lastid || atoi(str) < 0) { 855 error("no such link: %s", str); 856 return NULL; 857 } 858 859 return list_idget(&page, atoi(str)); 860 } 861 862 void 863 yank(Elem *e) { 864 char *uri, *sh; 865 int pfd[2]; 866 int status; 867 pid_t pid; 868 869 uri = elemtouri(e); 870 871 zygo_assert(pipe(pfd) != -1); 872 zygo_assert((pid = fork()) != -1); 873 874 if (pid == 0) { 875 close(0); 876 close(1); 877 close(2); 878 close(pfd[1]); 879 dup2(pfd[0], 0); 880 execlp(yanker, yanker, NULL); 881 } 882 883 close(pfd[0]); 884 write(pfd[1], uri, strlen(uri)); 885 close(pfd[1]); 886 887 waitpid(pid, &status, 0); 888 if (WEXITSTATUS(status) != 0) 889 error("could not execute '%s' for yanking", yanker); 890 891 free(uri); 892 } 893 894 void 895 pagescroll(int lines) { 896 if (lines > 0 && list_len(&page) > LINES - 1) { 897 ui.scroll += lines; 898 if (ui.scroll > list_len(&page) - LINES) 899 ui.scroll = list_len(&page) - LINES + 1; 900 } else if (lines < 0) { 901 ui.scroll += lines; 902 if (ui.scroll < 0) 903 ui.scroll = 0; 904 } /* else intentionally left blank */ 905 draw_page(); 906 } 907 908 void 909 idgo(size_t id) { 910 if (id > page->lastid || id < 1) 911 error("no such link: %d", id); 912 else 913 go(list_idget(&page, id), 1, 0); 914 } 915 916 int 917 wantnum(char cmd) { 918 return (!ui.cmd || ui.cmd == BIND_DISPLAY || ui.cmd == BIND_YANK); 919 } 920 921 int 922 acceptkey(char cmd, int key) { 923 if (wantnum(cmd)) 924 return isdigit(key); 925 return key >= 32 && key < KEY_CODE_YES; 926 } 927 928 /* 929 * Main loop 930 */ 931 void 932 run(void) { 933 wint_t c; 934 int ret; 935 Elem *e; 936 char tmperror[BUFLEN]; 937 938 draw_page(); 939 draw_bar(); 940 941 /* get_wch does refresh() for us */ 942 while ((ret = get_wch(&c)) != ERR) { 943 if (ui.error && c != KEY_RESIZE) 944 ui.error = 0; 945 946 if (c == KEY_RESIZE) { 947 draw_page(); 948 draw_bar(); 949 } else if (ui.wantinput) { 950 if (c == 27 /* escape */) { 951 ui.wantinput = 0; 952 } else if (c == '\n') { 953 submit: 954 switch (ui.cmd) { 955 case BIND_URI: 956 e = uritoelem(ui.arg); 957 go(e, 1, 0); 958 elem_free(e); 959 break; 960 case BIND_DISPLAY: 961 if ((e = strtolink(ui.arg))) { 962 move(LINES - 1, 0); 963 attroff(A_COLOR); 964 clrtoeol(); 965 printw("%s", elemtouri(list_idget(&page, atoi(ui.arg)))); 966 curs_set(0); 967 getch(); /* wait */ 968 } 969 break; 970 case BIND_SEARCH: 971 case BIND_SEARCH_BACK: 972 if (ui.search) { 973 regfree(&ui.regex); 974 ui.search = 0; 975 } 976 977 if (ui.input[0] != '\0') { 978 if ((ret = regcomp(&ui.regex, ui.arg, regexflags)) != 0) { 979 regerror(ret, &ui.regex, (char *)&tmperror, sizeof(tmperror)); 980 error("could not compile regex '%s': %s", ui.arg, tmperror); 981 } else { 982 ui.search = 1; 983 find(ui.cmd == BIND_SEARCH_BACK ? 1 : 0); 984 } 985 } 986 break; 987 case BIND_APPEND: 988 e = elem_dup(current); 989 e->selector = erealloc(e->selector, strlen(e->selector) + strlen(ui.arg) + 1); 990 /* should be safe.. I think */ 991 strcat(e->selector, ui.arg); 992 go(e, 1, 0); 993 elem_free(e); 994 break; 995 case BIND_YANK: 996 if ((e = strtolink(ui.arg))) 997 yank(e); 998 break; 999 case '\0': /* links */ 1000 idgo(atoi(ui.arg)); 1001 } 1002 ui.wantinput = 0; 1003 draw_page(); 1004 } else if (c == KEY_BACKSPACE || c == 127) { 1005 if ((ui.cmd && !ui.input[0]) || (!ui.cmd && !ui.input[1])) 1006 ui.wantinput = 0; 1007 else 1008 input(KEY_BACKSPACE); 1009 } else if (ui.cmd == BIND_YANK && c == BIND_YANK && !ui.input[0]) { 1010 ui.wantinput = 0; 1011 yank(current); 1012 } else if (acceptkey(ui.cmd, c)) { 1013 input(c); 1014 if (wantnum(ui.cmd) && atoi(ui.arg) * 10 > page->lastid) 1015 goto submit; 1016 } 1017 draw_bar(); 1018 } else { 1019 if ((c == BIND_RELOAD || c == BIND_ROOT || c == BIND_APPEND || c == BIND_YANK) && 1020 (!current || !current->server || !current->port)) { 1021 error("%c command can only be used on remote gopher menus", c); 1022 continue; 1023 } 1024 1025 switch (c) { 1026 case KEY_DOWN: 1027 case BIND_DOWN: 1028 pagescroll(1); 1029 break; 1030 case 4: /* ^D */ 1031 case 6: /* ^F */ 1032 pagescroll((int)(LINES / 2)); 1033 break; 1034 case KEY_UP: 1035 case BIND_UP: 1036 pagescroll(-1); 1037 break; 1038 case 21: /* ^U */ 1039 case 2: /* ^B */ 1040 pagescroll(-(int)(LINES / 2)); 1041 break; 1042 case 3: /* ^C */ 1043 case BIND_QUIT: 1044 endwin(); 1045 exit(EXIT_SUCCESS); 1046 case BIND_BACK: 1047 if (history && history->next) { 1048 for (e = history; e; e = e->next) 1049 if (e->next && !e->next->next) 1050 break; 1051 go(e, 0, 0); 1052 free(e->next); 1053 e->next = NULL; 1054 draw_page(); 1055 draw_bar(); 1056 } else { 1057 error("no previous history"); 1058 } 1059 break; 1060 case BIND_RELOAD: 1061 go(current, 0, 0); 1062 draw_page(); 1063 draw_bar(); 1064 break; 1065 case BIND_TOP: 1066 pagescroll(INT_MIN); 1067 break; 1068 case BIND_BOTTOM: 1069 pagescroll(INT_MAX); 1070 break; 1071 case BIND_SEARCH_NEXT: 1072 case BIND_SEARCH_PREV: 1073 find(c == BIND_SEARCH_PREV ? 1 : 0); 1074 draw_page(); 1075 break; 1076 case BIND_ROOT: 1077 e = elem_dup(current); 1078 free(e->selector); 1079 e->selector = strdup(""); 1080 go(e, 1, 0); 1081 elem_free(e); 1082 draw_page(); 1083 draw_bar(); 1084 break; 1085 case BIND_HELP: 1086 manpage(); 1087 break; 1088 case BIND_HISTORY: 1089 if (history) { 1090 elem_free(current); 1091 current = NULL; 1092 list_free(&page); 1093 for (e = history; e; e = e->next) { 1094 free(e->desc); 1095 e->desc = elemtouri(e); 1096 list_append(&page, e); 1097 } 1098 list_rev(&page); 1099 draw_bar(); 1100 draw_page(); 1101 } else { 1102 error("no history"); 1103 } 1104 break; 1105 /* link numbers */ 1106 case '0': case '1': case '2': case '3': case '4': 1107 case '5': case '6': case '7': case '8': case '9': 1108 ui.wantinput = 1; 1109 ui.cmd = '\0'; 1110 input(0); 1111 input(c); 1112 if (atoi(ui.arg) * 10 > page->lastid) { 1113 idgo(atoi(ui.arg)); 1114 ui.wantinput = 0; 1115 draw_page(); 1116 } 1117 draw_bar(); 1118 break; 1119 /* commands with arg */ 1120 case BIND_URI: 1121 case BIND_DISPLAY: 1122 case BIND_SEARCH: 1123 case BIND_SEARCH_BACK: 1124 case BIND_APPEND: 1125 case BIND_YANK: 1126 ui.cmd = (char)c; 1127 ui.wantinput = 1; 1128 input(0); 1129 draw_bar(); 1130 break; 1131 case '\n': 1132 case 27: /* escape */ 1133 case KEY_BACKSPACE: 1134 break; 1135 default: 1136 error("not bound"); 1137 break; 1138 } 1139 } 1140 } 1141 } 1142 1143 void 1144 sighandler(int signal) { 1145 switch (signal) { 1146 case SIGCHLD: 1147 while (waitpid(-1, NULL, WNOHANG) == 0); 1148 break; 1149 } 1150 } 1151 1152 void 1153 usage(char *argv0) { 1154 #ifdef TLS 1155 #define OPTS "-Pv" TLSOPTS 1156 #else 1157 #define OPTS "-Pv" 1158 #endif /* TLS */ 1159 fprintf(stderr, "usage: %s [%s] [-p plumber] [-y yanker] [uri]\n", basename(argv0), OPTS); 1160 exit(EXIT_FAILURE); 1161 #undef OPTS 1162 } 1163 1164 int 1165 main(int argc, char *argv[]) { 1166 Elem *target = NULL; 1167 Elem err = {0, 0, NULL, NULL, NULL, NULL, 0}; 1168 char *s; 1169 int i; 1170 1171 for (i = 1; i < argc; i++) { 1172 if ((*argv[i] == '-' && *(argv[i]+1) == '\0') || 1173 (*argv[i] != '-' && target)) { 1174 usage(argv[0]); 1175 } else if (*argv[i] == '-') { 1176 for (s = argv[i]+1; *s; s++) { 1177 #ifndef TLS 1178 if (strchr(TLSOPTS, *s)) { 1179 fprintf(stderr, "-%c: TLS support not compiled\n", *s); 1180 exit(EXIT_FAILURE); 1181 } 1182 #endif /* TLS */ 1183 switch (*s) { 1184 case 'k': 1185 insecure = 1; 1186 break; 1187 case 'p': 1188 if (*(s+1)) { 1189 plumber = s + 1; 1190 s += strlen(s) - 1; 1191 } else if (i + 1 != argc) { 1192 plumber = argv[++i]; 1193 } else { 1194 usage(argv[0]); 1195 } 1196 break; 1197 case 'y': 1198 if (*(s+1)) { 1199 yanker = s + 1; 1200 s += strlen(s) - 1; 1201 } else if (i + 1 != argc) { 1202 yanker = argv[++i]; 1203 } else { 1204 usage(argv[0]); 1205 } 1206 break; 1207 case 'P': 1208 parallelplumb = 1; 1209 break; 1210 case 'u': 1211 autotls = 1; 1212 break; 1213 case 'v': 1214 fprintf(stderr, "zygo %s\n", COMMIT); 1215 exit(EXIT_SUCCESS); 1216 default: 1217 usage(argv[0]); 1218 } 1219 } 1220 } else { 1221 target = uritoelem(argv[argc-1]); 1222 } 1223 } 1224 1225 if (!page) { 1226 if (ui.error) { 1227 err.type = '3'; 1228 err.desc = ui.errorbuf; 1229 list_append(&page, &err); 1230 err.type = 'i'; 1231 err.desc = ""; 1232 list_append(&page, &err); 1233 } 1234 1235 for (i = 0; i < sizeof(start_page) / sizeof(start_page[0]); i++) 1236 list_append(&page, &start_page[i]); 1237 } 1238 1239 setlocale(LC_ALL, ""); 1240 initscr(); 1241 noecho(); 1242 start_color(); 1243 use_default_colors(); 1244 keypad(stdscr, TRUE); 1245 set_escdelay(10); 1246 1247 signal(SIGALRM, sighandler); 1248 signal(SIGCHLD, sighandler); 1249 1250 init_pair(PAIR_BAR, bar_pair[0], bar_pair[1]); 1251 init_pair(PAIR_URI, uri_pair[0], uri_pair[1]); 1252 init_pair(PAIR_CMD, cmd_pair[0], cmd_pair[1]); 1253 init_pair(PAIR_ARG, arg_pair[0], arg_pair[1]); 1254 init_pair(PAIR_ERR, err_pair[0], err_pair[1]); 1255 init_pair(PAIR_EID, eid_pair[0], eid_pair[1]); 1256 for (i = 0; i == 0 || scheme[i - 1].type; i++) { 1257 scheme[i].pair = i + PAIR_SCHEME; 1258 init_pair(scheme[i].pair, scheme[i].fg, -1); 1259 } 1260 1261 if (target) 1262 go(target, 1, 0); 1263 1264 run(); 1265 1266 endwin(); 1267 }