hfc.c (52542B)
1 /* hfc.c - host file client 2 * 3 * headers and macros */ 4 #define _POSIX_C_SOURCE 200809L 5 6 #include <ctype.h> 7 #include <curl/curl.h> 8 #include <errno.h> 9 #include <locale.h> 10 #include <ncurses.h> 11 #include <netdb.h> 12 #include <signal.h> 13 #include <stdbool.h> 14 #include <stdio.h> 15 #include <stdlib.h> 16 #include <string.h> 17 #include <time.h> 18 #include <unistd.h> 19 20 #include "config.h" 21 #include "get.h" 22 #include "hfc.h" 23 #include "update.h" 24 #include <sys/types.h> 25 #include <sys/file.h> 26 #include <sys/wait.h> 27 28 #define MAX_ENTRIES 100 29 #define MAX_KEYBINDINGS 64 30 #define MAX_LINE_LEN 256 31 #define MAX_HEADER_LEN 256 32 #define MAX_NAME_LEN 220 33 34 /* functions */ 35 /* 01.00 */ static int check_write_hosts(bool silent); 36 /* 02.00 */ static void load_entries(void); 37 /* 03.00 */ static void reorder_entry(int from, int to); 38 /* 04.00 */ static int fetch_lock_exists(void); 39 /* 05.00 */ static int is_fetch_lock_empty(void); 40 /* 06.00 */ static void create_fetch_lock(int index); 41 /* 07.00 */ static void remove_index_from_lock(int index_to_remove); 42 /* 08.00 */ static void start_update_in_background(void); 43 /* 09.00 */ static void save_count_for_index(int index); 44 /* 10.00 */ static void save_counts(void); 45 /* 11.00 */ static void load_counts(void); 46 /* 12.00 */ static void update_all_sources(void); 47 /* 13.00 */ static void update_selected_sources(void); 48 /* 14.00 */ static void update_domain_count(int index); 49 /* 15.00 */ static int count_hosts_in_section(int section_index); 50 /* 16.00 */ static void save_entries(void); 51 /* 17.00 */ static void free_entries(void); 52 /* 18.00 */ static void display_entries(int highlight); 53 /* 19.00 */ static void init_footer(void); 54 /* 20.00 */ static void print_footer(const char *fmt, ...); 55 /* 21.00 */ static void display_help(void); 56 /* 22.00 */ static void edit_entry(int index); 57 /* 23.00 */ static void add_entry(void); 58 /* 24.00 */ static void background_fetch_entry(int index); 59 /* 25.00 */ static void remove_entry(int index); 60 /* 26.00 */ static void remove_selected_entries(void); 61 /* 27.00 */ static void rebuild_hostfile_headers_from_urls(void); 62 /* 28.00 */ static void merge_entry(int index); 63 /* 29.00 */ static void merge_selected_entries(void); 64 /* 30.00 */ static void rename_entry(int index); 65 /* 31.00 */ const char *get_action_for_key(int key); 66 /* 32.00 */ static int has_network_connection(void); 67 /* 33.00 */ int main(int argc, char *argv[]); 68 69 /* variables */ 70 int domain_count = 0; 71 int footer_busy = 0; 72 int in_help_mode = 0; 73 int highlight = 0; 74 int updates_count = 0; 75 int entry_count = 0; 76 int keybinding_count = 0; 77 int is_checking = 1; 78 volatile int update_progress = -1; 79 int is_updating[MAX_ENTRIES] = {0}; 80 int selected[MAX_ENTRIES] = {0}; 81 int update_pipe[2]; 82 int updates_counts[MAX_ENTRIES]; 83 int domains_counts[MAX_ENTRIES]; 84 Domain domains[MAX_ENTRIES]; 85 WINDOW *footer_win = NULL; 86 KeyBinding keybindings[MAX_KEYBINDINGS]; 87 char *entries[MAX_ENTRIES]; 88 char urls_path[256]; 89 char counts_path[256]; 90 char fetch_lock_path[256]; 91 char path_keybindings[256]; 92 long remote_sizes[MAX_ENTRIES]; 93 const char *editor = NULL; 94 const char *hosts_path = "/etc/hosts"; 95 int is_local_entry(const char *entry) { 96 if (!entry) return 0; 97 return !(strstr(entry, "http://") || strstr(entry, "https://")); 98 } 99 100 /* 01.00 */ static int 101 check_write_hosts(bool silent) 102 { 103 if (access(hosts_path, W_OK) != 0) { 104 if (!silent) { 105 int is_tui = (stdscr != NULL || isatty(fileno(stdout))); 106 107 if (is_tui) { 108 print_footer("Error: Can't open hosts for writing: permission denied."); 109 refresh(); 110 napms(1500); 111 } else { 112 fprintf(stderr, "Error: Can't open hosts for writing: permission denied.\n"); 113 } 114 } 115 return 0; 116 } 117 return 1; 118 } 119 120 /* 02.00 */ static void 121 load_entries(void) 122 { 123 FILE *fp; 124 char line[MAX_LINE_LEN]; 125 char *url_start; 126 127 /* entries from urls_path */ 128 fp = fopen(urls_path, "r"); 129 if (fp) { 130 entry_count = 0; 131 while (fgets(line, sizeof(line), fp) && entry_count < MAX_ENTRIES) { 132 line[strcspn(line, "\n")] = '\0'; 133 url_start = line; 134 entries[entry_count] = strdup(url_start); 135 domains[entry_count].url = strdup(entries[entry_count]); 136 domains[entry_count].hosts = NULL; 137 entry_count++; 138 } 139 fclose(fp); 140 } 141 142 /* temporary entries from fetch.lock (not yet in urls_path!) */ 143 if (fetch_lock_exists()) { 144 FILE *lock_fp = fopen(fetch_lock_path, "r"); 145 if (lock_fp) { 146 while (fgets(line, sizeof(line), lock_fp) && entry_count < MAX_ENTRIES) { 147 line[strcspn(line, "\n")] = '\0'; 148 149 /* check: already in entries[]? */ 150 int exists = 0; 151 for (int i = 0; i < entry_count; i++) { 152 if (entries[i] && strcmp(entries[i], line) == 0) { 153 exists = 1; 154 break; 155 } 156 } 157 158 if (!exists) { 159 entries[entry_count] = strdup(line); 160 domains[entry_count].url = strdup(entries[entry_count]); 161 domains[entry_count].hosts = NULL; 162 domains_counts[entry_count] = -1; 163 updates_counts[entry_count] = -1; 164 remote_sizes[entry_count] = 0; 165 entry_count++; 166 } 167 } 168 fclose(lock_fp); 169 } 170 } 171 } 172 173 /* 03.00 */ static void 174 reorder_entry(int from, int to) 175 { 176 if (from == to || from < 0 || to < 0 || from >= entry_count || to >= entry_count) 177 return; 178 179 if (!check_write_hosts(true)) { 180 return; 181 } 182 183 /* read hosts file into blocks */ 184 FILE *fp = fopen(hosts_path, "r"); 185 if (!fp) return; 186 187 char **blocks = calloc(entry_count, sizeof(char *)); 188 size_t *block_sizes = calloc(entry_count, sizeof(size_t)); 189 int current = -1; 190 size_t bufsize = 0; 191 char *block_buf = NULL; 192 char line[MAX_LINE_LEN]; 193 194 while (fgets(line, sizeof(line), fp)) { 195 if (strncmp(line, "# ", 2) == 0) { 196 if (current >= 0) { 197 blocks[current] = block_buf; 198 block_sizes[current] = bufsize; 199 } 200 current++; 201 block_buf = NULL; 202 bufsize = 0; 203 } 204 size_t len = strlen(line); 205 block_buf = realloc(block_buf, bufsize + len + 1); 206 memcpy(block_buf + bufsize, line, len); 207 bufsize += len; 208 block_buf[bufsize] = '\0'; 209 } 210 if (current >= 0) { 211 blocks[current] = block_buf; 212 block_sizes[current] = bufsize; 213 } 214 fclose(fp); 215 216 /* back up metadata */ 217 char *entry_tmp = entries[from]; 218 int domain_tmp = domains_counts[from]; 219 int update_tmp = updates_counts[from]; 220 long size_tmp = remote_sizes[from]; 221 int sel_tmp = selected[from]; 222 Domain domain_struct_tmp = domains[from]; 223 char *block_tmp = blocks[from]; 224 size_t block_size_tmp = block_sizes[from]; 225 226 /* move arrays */ 227 if (from < to) { 228 for (int i = from; i < to; i++) { 229 entries[i] = entries[i + 1]; 230 domains_counts[i] = domains_counts[i + 1]; 231 updates_counts[i] = updates_counts[i + 1]; 232 remote_sizes[i] = remote_sizes[i + 1]; 233 selected[i] = selected[i + 1]; 234 domains[i] = domains[i + 1]; 235 blocks[i] = blocks[i + 1]; 236 block_sizes[i] = block_sizes[i + 1]; 237 } 238 } else { 239 for (int i = from; i > to; i--) { 240 entries[i] = entries[i - 1]; 241 domains_counts[i] = domains_counts[i - 1]; 242 updates_counts[i] = updates_counts[i - 1]; 243 remote_sizes[i] = remote_sizes[i - 1]; 244 selected[i] = selected[i - 1]; 245 domains[i] = domains[i - 1]; 246 blocks[i] = blocks[i - 1]; 247 block_sizes[i] = block_sizes[i - 1]; 248 } 249 } 250 251 /* use secured position */ 252 entries[to] = entry_tmp; 253 domains_counts[to] = domain_tmp; 254 updates_counts[to] = update_tmp; 255 remote_sizes[to] = size_tmp; 256 selected[to] = sel_tmp; 257 domains[to] = domain_struct_tmp; 258 blocks[to] = block_tmp; 259 block_sizes[to] = block_size_tmp; 260 261 /* rewrite hosts file with new numbers */ 262 FILE *out = fopen(hosts_path, "w"); 263 if (out) { 264 for (int i = 0; i < entry_count; i++) { 265 fprintf(out, "# %d. %s\n", i + 1, entries[i]); 266 char *block_content = strstr(blocks[i], "\n"); 267 if (block_content) { 268 fputs(block_content + 1, out); 269 } 270 } 271 fclose(out); 272 } 273 274 for (int i = 0; i < entry_count; i++) { 275 free(blocks[i]); 276 } 277 free(blocks); 278 free(block_sizes); 279 280 save_entries(); 281 save_counts(); 282 } 283 284 /* 04.00 */ static int 285 fetch_lock_exists(void) 286 { 287 FILE *fp = fopen(fetch_lock_path, "r"); 288 if (fp) { 289 fclose(fp); 290 return 1; 291 } 292 return 0; 293 } 294 295 /* 05.00 */ static int 296 is_fetch_lock_empty(void) 297 { 298 FILE *fp = fopen(fetch_lock_path, "r"); 299 if (!fp) return 1; 300 301 int dummy; 302 int result = (fscanf(fp, "%d", &dummy) != 1); 303 fclose(fp); 304 return result; 305 } 306 307 /* 06.00 */ static void 308 create_fetch_lock(int index) 309 { 310 if (fetch_lock_path[0] == '\0') 311 return; 312 313 /* check if index is already in lock */ 314 FILE *check = fopen(fetch_lock_path, "r"); 315 if (check) { 316 char line[1024]; 317 while (fgets(line, sizeof(line), check)) { 318 line[strcspn(line, "\n")] = '\0'; 319 if (strcmp(line, entries[index]) == 0) { 320 fclose(check); 321 return; 322 } 323 } 324 fclose(check); 325 } 326 327 /* if lock does not exist – add */ 328 FILE *fp = fopen(fetch_lock_path, "a"); 329 if (!fp) { 330 perror("fopen fetch_lock"); 331 return; 332 } 333 334 flock(fileno(fp), LOCK_EX); 335 fprintf(fp, "%s\n", entries[index]); 336 fflush(fp); 337 flock(fileno(fp), LOCK_UN); 338 fclose(fp); 339 } 340 341 /* 07.00 */ static void 342 remove_index_from_lock(int index_to_remove) 343 { 344 if (!entries[index_to_remove]) return; 345 346 int fd = open(fetch_lock_path, O_RDWR); 347 if (fd == -1) return; 348 349 flock(fd, LOCK_EX); 350 351 FILE *in = fdopen(fd, "r"); 352 if (!in) { 353 close(fd); 354 return; 355 } 356 357 FILE *out = fopen("/tmp/fetch_lock_temp", "w"); 358 if (!out) { 359 fclose(in); 360 return; 361 } 362 363 char line[1024]; 364 int is_empty = 1; 365 366 while (fgets(line, sizeof(line), in)) { 367 line[strcspn(line, "\n")] = '\0'; 368 369 if (strcmp(line, entries[index_to_remove]) != 0) { 370 fprintf(out, "%s\n", line); 371 is_empty = 0; 372 } 373 } 374 375 fclose(in); 376 fclose(out); 377 rename("/tmp/fetch_lock_temp", fetch_lock_path); 378 379 if (is_empty) { 380 remove(fetch_lock_path); 381 } 382 } 383 384 /* 08.00 */ static void 385 start_update_in_background() 386 { 387 pid_t pid = fork(); 388 389 if (pid < 0) { 390 perror("fork"); 391 return; 392 } 393 394 if (pid == 0) { 395 /* check permission */ 396 if (geteuid() != 0 || !check_write_hosts(true)) { 397 _exit(0); 398 } 399 /* handle updates */ 400 for (int idx = 0; idx < entry_count; idx++) { 401 if (is_local_entry(entries[idx])) 402 continue; 403 404 long new_size = get_remote_content_length(entries[idx]); 405 406 if (new_size <= 0 || new_size == remote_sizes[idx]) { 407 /* no change in remote content */ 408 if (update_pipe[1] != -1) { 409 int dummy = -2; 410 write(update_pipe[1], &dummy, sizeof(int)); 411 } 412 continue; 413 } 414 415 pid_t subpid = fork(); 416 417 if (subpid == 0) { 418 /* only update updates_counts skip domains_counts */ 419 char error_msg[CURL_ERROR_SIZE] = {0}; 420 char *content = download_url(entries[idx], error_msg, sizeof(error_msg)); 421 422 if (content && contains_valid_hosts_entry(content)) { 423 updates_counts[idx] = count_hosts_in_content(content); 424 remote_sizes[idx] = new_size; 425 save_counts(); 426 } 427 428 if (content) 429 free(content); 430 431 if (update_pipe[1] != -1) { 432 write(update_pipe[1], &idx, sizeof(int)); 433 } 434 _exit(0); 435 } 436 } 437 438 /* wait for child processes */ 439 while (wait(NULL) > 0); 440 441 /* signal to tui: all updates completed */ 442 if (update_pipe[1] != -1) { 443 int end = -1; 444 write(update_pipe[1], &end, sizeof(int)); 445 close(update_pipe[1]); 446 } 447 _exit(0); 448 } 449 } 450 451 /* 09.00 */ static void 452 save_count_for_index(int index) 453 { 454 if (index < 0 || index >= entry_count) return; 455 456 char lines[MAX_ENTRIES][64] = {0}; 457 int existing_lines = 0; 458 459 FILE *in = fopen(counts_path, "r"); 460 if (in) { 461 while (fgets(lines[existing_lines], sizeof(lines[0]), in) && existing_lines < MAX_ENTRIES) { 462 lines[existing_lines][strcspn(lines[existing_lines], "\n")] = '\0'; 463 existing_lines++; 464 } 465 fclose(in); 466 } 467 468 if (index >= existing_lines) { 469 for (int i = existing_lines; i <= index; i++) { 470 strcpy(lines[i], ""); /* blank line as placeholder */ 471 } 472 } 473 474 snprintf(lines[index], sizeof(lines[index]), "%d %ld", updates_counts[index], remote_sizes[index]); 475 476 FILE *out = fopen(counts_path, "w"); 477 if (!out) return; 478 479 for (int i = 0; i <= index; i++) { 480 if (strlen(lines[i]) == 0) continue; 481 fprintf(out, "%s\n", lines[i]); 482 } 483 484 fclose(out); 485 } 486 487 /* 10.00 */ static void 488 save_counts(void) 489 { 490 FILE *f; 491 int i; 492 493 f = fopen(counts_path, "w"); 494 if (!f) 495 return; 496 497 for (i = 0; i < entry_count; i++) 498 fprintf(f, "%d %ld\n", updates_counts[i], remote_sizes[i]); 499 500 fclose(f); 501 } 502 503 /* 11.00 */ static void 504 load_counts(void) 505 { 506 FILE *fp = fopen(counts_path, "r"); 507 if (!fp) 508 return; 509 510 for (int i = 0; i < entry_count; i++) { 511 long updates = 0; 512 long remote_size = 0; 513 if (fscanf(fp, "%ld %ld", &updates, &remote_size) != 2) 514 break; 515 516 if (is_local_entry(entries[i])) { 517 domains_counts[i] = count_hosts_in_section(i); 518 updates_counts[i] = 0; 519 remote_sizes[i] = 0; 520 } else if (updates <= 0) { 521 domains_counts[i] = -1; 522 updates_counts[i] = -1; 523 remote_sizes[i] = 0; 524 } else { 525 domains_counts[i] = updates; 526 updates_counts[i] = updates; 527 remote_sizes[i] = remote_size; 528 } 529 } 530 531 fclose(fp); 532 533 /* fetch lock control */ 534 if (fetch_lock_exists()) { 535 FILE *fp = fopen(fetch_lock_path, "r"); 536 if (fp) { 537 char line[1024]; 538 while (fgets(line, sizeof(line), fp)) { 539 line[strcspn(line, "\n")] = '\0'; 540 541 for (int i = 0; i < entry_count; i++) { 542 if (entries[i] && strcmp(entries[i], line) == 0) { 543 if (domains_counts[i] <= 0 || updates_counts[i] <= 0) { 544 domains_counts[i] = -1; 545 updates_counts[i] = -1; 546 } 547 break; 548 } 549 } 550 } 551 fclose(fp); 552 } 553 } 554 } 555 556 static void 557 background_count_entry(int index) 558 { 559 is_updating[index] = 1; 560 561 pid_t pid = fork(); 562 if (pid == 0) { 563 setsid(); 564 signal(SIGHUP, SIG_IGN); 565 signal(SIGPIPE, SIG_IGN); 566 567 fclose(stdin); 568 fclose(stdout); 569 fclose(stderr); 570 571 create_fetch_lock(index); 572 573 long new_size = get_remote_content_length(entries[index]); 574 if (new_size <= 0) { 575 remove_index_from_lock(index); 576 _exit(1); 577 } 578 579 if (new_size == remote_sizes[index]) { 580 domains_counts[index] = updates_counts[index]; 581 updates_counts[index] = domains_counts[index]; 582 } else { 583 char error_msg[CURL_ERROR_SIZE] = {0}; 584 char *content = download_url(entries[index], error_msg, sizeof(error_msg)); 585 if (!content) { 586 remove_index_from_lock(index); 587 _exit(1); 588 } 589 590 int count = count_hosts_in_content(content); 591 domains_counts[index] = count; 592 updates_counts[index] = count; 593 free(content); 594 } 595 596 remote_sizes[index] = new_size; 597 save_count_for_index(index); 598 599 remove_index_from_lock(index); 600 601 if (update_pipe[1] != -1) { 602 write(update_pipe[1], &index, sizeof(int)); 603 } 604 605 _exit(0); 606 } 607 } 608 609 /* 12.00 */ static void 610 update_all_sources(void) 611 { 612 int i, total = 0, current = 0; 613 614 if (!has_network_connection()) { 615 print_footer("Error: No network connection."); 616 napms(2000); 617 return; 618 } 619 620 if (!check_write_hosts(true)) { 621 return; 622 } 623 624 for (i = 0; i < entry_count; i++) { 625 if (!is_local_entry(entries[i]) && updates_counts[i] != -1) 626 total++; 627 } 628 629 int old_highlight = highlight; 630 631 for (i = 0; i < entry_count; i++) { 632 if (is_local_entry(entries[i])) 633 continue; 634 635 if (domains_counts[i] == -1 || updates_counts[i] == -1) 636 continue; 637 638 current++; 639 update_progress = i; 640 641 display_entries(highlight); 642 print_footer("(%d/%d) loading %s", current, total, entries[i]); 643 refresh(); 644 645 background_count_entry(i); 646 } 647 648 update_progress = -1; 649 highlight = old_highlight; 650 651 save_counts(); 652 display_entries(highlight); 653 refresh(); 654 } 655 656 /* 13.00 */ static void 657 update_selected_sources(void) 658 { 659 int i, total = 0, current = 0; 660 661 if (!has_network_connection()) { 662 print_footer("Error: No network connection."); 663 napms(2000); 664 return; 665 } 666 667 for (i = 0; i < entry_count; i++) { 668 if (selected[i] && !is_local_entry(entries[i])) 669 total++; 670 } 671 672 int old_highlight = highlight; 673 674 for (i = 0; i < entry_count; i++) { 675 if (!selected[i] || is_local_entry(entries[i])) 676 continue; 677 678 current++; 679 update_progress = i; 680 681 display_entries(highlight); 682 print_footer("(%d/%d) loading %s", current, total, entries[i]); 683 refresh(); 684 685 background_count_entry(i); 686 } 687 688 update_progress = -1; 689 highlight = old_highlight; 690 691 display_entries(highlight); 692 refresh(); 693 } 694 695 /* 14.00 */ static void 696 update_domain_count(int index) 697 { 698 long new_size; 699 char error_msg[256]; 700 char *content; 701 702 if (index < 0 || index >= entry_count || !entries[index]) { 703 updates_counts[index] = 0; 704 return; 705 } 706 707 new_size = get_remote_content_length(entries[index]); 708 if (new_size <= 0) 709 return; 710 711 if (new_size != remote_sizes[index]) { 712 content = download_url(entries[index], error_msg, sizeof(error_msg)); 713 if (!content) 714 return; 715 716 int count = count_hosts_in_content(content); 717 updates_counts[index] = count; 718 domains_counts[index] = count; 719 remote_sizes[index] = new_size; 720 721 free(content); 722 save_counts(); 723 } 724 } 725 726 /* 15.00 */ static int 727 count_hosts_in_section(int section_index) 728 { 729 FILE *fp; 730 char line[512]; 731 char section_header[256]; 732 int current_section = -1; 733 int count = 0; 734 735 fp = fopen(hosts_path, "r"); 736 if (!fp) 737 return 0; 738 739 snprintf(section_header, sizeof(section_header), "# %d.", section_index + 1); 740 741 while (fgets(line, sizeof(line), fp)) { 742 if (strncmp(line, "# ", 2) == 0) { 743 if (strncmp(line, section_header, strlen(section_header)) == 0) { 744 current_section = section_index; 745 count = 0; 746 } else if (current_section == section_index) { 747 break; 748 } else { 749 current_section = -1; 750 } 751 } else if (current_section == section_index) { 752 if (strncmp(line, "0.0.0.0 ", 8) == 0) 753 count++; 754 } 755 } 756 757 fclose(fp); 758 return count; 759 } 760 761 /* 16.00 */ static void 762 save_entries(void) 763 { 764 FILE *fp = fopen(urls_path, "w"); 765 if (!fp) 766 return; 767 768 for (int i = 0; i < entry_count; i++) { 769 if (entries[i] && strlen(entries[i]) > 0) { 770 fprintf(fp, "%s\n", entries[i]); 771 } 772 } 773 774 fclose(fp); 775 } 776 777 /* 17.00 */ static void 778 free_entries(void) 779 { 780 int i; 781 782 for (i = 0; i < entry_count; i++) 783 free(entries[i]); 784 } 785 786 /* 18.00 */ static void 787 display_entries(int highlight) 788 { 789 int i, line, y, cols, url_len, lines_needed; 790 int local_source_width = 13; 791 792 int id_width = 5; 793 int indent = 7; 794 int local_source_start; 795 int max_url_width; 796 char *text, buf[32]; 797 798 clear(); 799 cols = getmaxx(stdscr); 800 local_source_start = cols - local_source_width - 4; 801 802 int selected_count = 0; 803 for (int si = 0; si < entry_count; si++) { 804 if (selected[si]) 805 selected_count++; 806 } 807 int right_col = cols - 10; 808 809 /* header */ 810 { 811 short fg_code = (*config.header.fg) ? get_color_code(config.header.fg) : -1; 812 if (fg_code != -1) 813 apply_color(&config.header, 1); 814 else 815 attron(COLOR_PAIR(0)); 816 817 mvhline(0, 0, ' ', cols); 818 mvprintw(0, 0, "hfc %s |", HFC_VERSION); 819 mvprintw(0, 12, "%-1s:%-3s", get_keys_for_action("quit"), "quit"); 820 mvprintw(0, 20, "%-1s:%-3s", get_keys_for_action("add"), "add"); 821 mvprintw(0, 27, "%-1s:%-3s", get_keys_for_action("remove"), "remove"); 822 mvprintw(0, 37, "%-1s:%-3s", get_keys_for_action("merge"), "merge"); 823 mvprintw(0, 46, "%-1s:%-3s", get_keys_for_action("edit"), "edit"); 824 mvprintw(0, 54, "%-1s:%-3s", get_keys_for_action("help"), "help"); 825 826 if (selected_count > 0) { 827 mvprintw(0, right_col, "*%d | %d/%d", 828 selected_count, 829 (entry_count == 0) ? 0 : highlight + 1, 830 entry_count); 831 } else { 832 mvprintw(0, right_col, " | %d/%d", 833 (entry_count == 0) ? 0 : highlight + 1, 834 entry_count); 835 } 836 837 if (fg_code != -1) 838 remove_color(&config.header, 1); 839 else 840 attroff(COLOR_PAIR(0)); 841 842 /* line below header */ 843 short line_code = get_color_code(config.header.bg); 844 if (line_code != -1) { 845 init_pair(10, line_code, -1); 846 attron(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0)); 847 mvhline(1, 0, ACS_HLINE, cols); 848 attroff(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0)); 849 } else { 850 mvhline(1, 0, ACS_HLINE, cols); 851 } 852 } 853 854 /* table header */ 855 if (*config.table_header.fg) { 856 short fg_code = get_color_code(config.table_header.fg); 857 if (fg_code != -1) { 858 init_pair(21, fg_code, -1); 859 attron(COLOR_PAIR(21) | (config.table_header.bold ? A_BOLD : 0)); 860 } 861 } else { 862 attron(A_BOLD); 863 } 864 865 mvprintw(2, 2, "ID"); 866 mvprintw(2, indent, "List"); 867 mvprintw(2, local_source_start, "Local/Source"); 868 869 if (*config.table_header.fg) { 870 short fg_code = get_color_code(config.table_header.fg); 871 if (fg_code != -1) { 872 attroff(COLOR_PAIR(21) | (config.table_header.bold ? A_BOLD : 0)); 873 } 874 } else { 875 attroff(A_BOLD); 876 } 877 878 /* entries */ 879 if (entry_count > 0) { 880 max_url_width = local_source_start - indent - 6; 881 y = 3; 882 883 for (i = 0; i < entry_count; i++) { 884 text = entries[i]; 885 url_len = strlen(text); 886 lines_needed = (url_len + max_url_width - 1) / max_url_width; 887 int is_highlight = (i == highlight); 888 889 for (line = 0; line < lines_needed; line++) { 890 if (is_highlight) { 891 short fg_code = get_color_code(config.entry_highlight.fg); 892 short bg_code = get_color_code(config.entry_highlight.bg); 893 894 if (fg_code != -1 || bg_code != -1) { 895 int attrs = COLOR_PAIR(4); 896 if (config.entry_highlight.bold) 897 attrs |= A_BOLD; 898 attron(attrs); 899 mvhline(y, 0, ' ', cols); 900 } else { 901 attron(A_REVERSE); 902 mvhline(y, 0, ' ', cols); 903 } 904 } 905 906 if (!is_highlight && *config.entry_default.fg) { 907 short fg_code = get_color_code(config.entry_default.fg); 908 short bg_code = get_color_code(config.entry_default.bg); 909 if (fg_code != -1 || bg_code != -1) { 910 init_pair(20, 911 (fg_code != -1 ? fg_code : -1), 912 (bg_code != -1 ? bg_code : -1)); 913 attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 914 } 915 } 916 917 if (line == 0) { 918 mvprintw(y, 1, "%c", selected[i] ? '*' : ' '); 919 mvprintw(y, 2, "%-*d", id_width, i + 1); 920 mvprintw(y, indent, "%.*s", max_url_width, text + line * max_url_width); 921 922 if (is_updating[i]) { 923 if (domains_counts[i] > 0) { 924 snprintf(buf, sizeof(buf), "(%d/...)", domains_counts[i]); 925 } else { 926 snprintf(buf, sizeof(buf), "(...)"); 927 } 928 mvprintw(y, local_source_start, "%s", buf); 929 } else if (domains_counts[i] == -1 || updates_counts[i] == -1) { 930 mvprintw(y, local_source_start, "(...)"); 931 } else if (is_local_entry(entries[i])) { 932 snprintf(buf, sizeof(buf), "(%d)", domains_counts[i]); 933 mvprintw(y, local_source_start, "%s", buf); 934 } else { 935 snprintf(buf, sizeof(buf), "(%d/", domains_counts[i]); 936 mvprintw(y, local_source_start, "%s", buf); 937 mvprintw(y, local_source_start + strlen(buf), "%d)", updates_counts[i]); 938 } 939 } else { 940 mvprintw(y, 1, " "); 941 mvprintw(y, 2, " "); 942 mvprintw(y, indent, "%.*s", max_url_width, text + line * max_url_width); 943 } 944 945 if (!is_highlight && *config.entry_default.fg) { 946 short fg_code = get_color_code(config.entry_default.fg); 947 short bg_code = get_color_code(config.entry_default.bg); 948 if (fg_code != -1 || bg_code != -1) { 949 attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 950 } 951 } 952 953 if (is_highlight) { 954 short fg_code = get_color_code(config.entry_highlight.fg); 955 short bg_code = get_color_code(config.entry_highlight.bg); 956 957 if (fg_code != -1 || bg_code != -1) { 958 int attrs = COLOR_PAIR(4); 959 if (config.entry_highlight.bold) 960 attrs |= A_BOLD; 961 attroff(attrs); 962 } else { 963 attroff(A_REVERSE); 964 } 965 } 966 y++; 967 } 968 } 969 } 970 971 short footer_line_fg = get_color_code(config.footer.bg); 972 if (footer_line_fg != -1) { 973 init_pair(12, footer_line_fg, -1); 974 attron(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0)); 975 mvhline(LINES - 2, 0, ACS_HLINE, cols); 976 attroff(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0)); 977 } else { 978 mvhline(LINES - 2, 0, ACS_HLINE, cols); 979 } 980 } 981 982 /* 19.00 */ static void 983 init_footer() 984 { 985 if (footer_win) { 986 delwin(footer_win); 987 } 988 int cols = getmaxx(stdscr); 989 footer_win = newwin(1, cols, LINES - 1, 0); 990 } 991 992 993 /* 20.00 */ static void 994 print_footer(const char *fmt, ...) 995 { 996 int cols = getmaxx(stdscr); 997 998 if (*config.footer.fg) { 999 apply_color(&config.footer, 2); 1000 } 1001 1002 mvhline(LINES - 1, 0, ' ', cols); 1003 1004 char buf[512]; 1005 va_list args; 1006 va_start(args, fmt); 1007 vsnprintf(buf, sizeof(buf), fmt, args); 1008 va_end(args); 1009 1010 mvprintw(LINES - 1, 0, "%s", buf); 1011 1012 if (*config.footer.fg) { 1013 remove_color(&config.footer, 2); 1014 } 1015 1016 refresh(); 1017 } 1018 1019 /* 21.00 */ static void 1020 display_help(void) 1021 { 1022 typedef struct { 1023 const char *action; 1024 const char *description; 1025 } HelpEntry; 1026 1027 static HelpEntry help_entries[] = { 1028 { "help", "help" }, 1029 { "quit", "quit" }, 1030 { "refresh", "refresh screen" }, 1031 { NULL, NULL }, 1032 { "down", "down" }, 1033 { "up", "up" }, 1034 { "edit", "edit ($EDITOR)" }, 1035 { "add", "add item" }, 1036 { "remove", "remove selected" }, 1037 { "merge", "merge selected" }, 1038 { NULL, NULL }, 1039 { "select", "select" }, 1040 { "unselect_all", "unselect all" }, 1041 { "select_all", "select all" }, 1042 { NULL, NULL }, 1043 { "rename", "rename" }, 1044 { "update", "update selected" }, 1045 { "update_all", "update all" }, 1046 { "order", "change order" } 1047 }; 1048 1049 static const char *fallback_help[] = { 1050 "? - help", 1051 "q - quit", 1052 "L - refresh screen", 1053 "", 1054 "arrows / j,k - scroll", 1055 "e - edit ($EDITOR)", 1056 "a - add item", 1057 "r - remove selected", 1058 "m - merge selected", 1059 "", 1060 "space - select", 1061 "- - unselect all", 1062 "+ - select all", 1063 "", 1064 "R - rename", 1065 "u - update selected", 1066 "U - update all", 1067 "o - change order", 1068 }; 1069 1070 int lines = sizeof(help_entries) / sizeof(help_entries[0]); 1071 int fallback_lines = sizeof(fallback_help) / sizeof(fallback_help[0]); 1072 1073 clear(); 1074 int cols = getmaxx(stdscr); 1075 1076 /* header like in display_entries() */ 1077 if (*config.header.fg) { 1078 apply_color(&config.header, 1); 1079 mvhline(0, 0, ' ', cols); 1080 mvprintw(0, 0, "hfc %s | help", HFC_VERSION); 1081 remove_color(&config.header, 1); 1082 } else { 1083 mvhline(0, 0, ' ', cols); 1084 if (config.header.bold) attron(A_BOLD); 1085 mvprintw(0, 0, "hfc %s | help", HFC_VERSION); 1086 if (config.header.bold) attroff(A_BOLD); 1087 } 1088 1089 /* line below header */ 1090 short fg_code = get_color_code(config.header.bg); 1091 if (fg_code != -1) { 1092 init_pair(10, fg_code, -1); 1093 attron(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0)); 1094 mvhline(1, 0, ACS_HLINE, cols); 1095 attroff(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0)); 1096 } else { 1097 mvhline(1, 0, ACS_HLINE, cols); 1098 } 1099 1100 /* check if bindings present */ 1101 int any_binding_found = 0; 1102 for (int i = 0; i < lines; i++) { 1103 if (help_entries[i].action && 1104 strcmp(get_keys_for_action(help_entries[i].action), "?") != 0) { 1105 any_binding_found = 1; 1106 break; 1107 } 1108 } 1109 1110 int row = 3; 1111 if (any_binding_found) { 1112 for (int i = 0; i < lines; i++) { 1113 if (help_entries[i].action == NULL) { 1114 row++; 1115 continue; 1116 } 1117 const char *keys = get_keys_for_action(help_entries[i].action); 1118 1119 /* only font color like entry_default, without background */ 1120 if (*config.entry_default.fg) { 1121 short fg_code = get_color_code(config.entry_default.fg); 1122 if (fg_code != -1) { 1123 init_pair(20, fg_code, -1); 1124 attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 1125 } 1126 } 1127 1128 mvprintw(row++, 2, "%-18s - %s", keys, help_entries[i].description); 1129 1130 if (*config.entry_default.fg) { 1131 short fg_code = get_color_code(config.entry_default.fg); 1132 if (fg_code != -1) { 1133 attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 1134 } 1135 } 1136 } 1137 } else { 1138 for (int i = 0; i < fallback_lines; i++) { 1139 if (strcmp(fallback_help[i], "") == 0) { 1140 row++; 1141 continue; 1142 } 1143 if (*config.entry_default.fg) { 1144 short fg_code = get_color_code(config.entry_default.fg); 1145 if (fg_code != -1) { 1146 init_pair(20, fg_code, -1); 1147 attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 1148 } 1149 } 1150 mvprintw(row++, 2, "%s", fallback_help[i]); 1151 if (*config.entry_default.fg) { 1152 short fg_code = get_color_code(config.entry_default.fg); 1153 if (fg_code != -1) { 1154 attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0)); 1155 } 1156 } 1157 } 1158 } 1159 1160 /* footer line as in display_entries() */ 1161 short footer_line_fg = get_color_code(config.footer.bg); 1162 if (footer_line_fg != -1) { 1163 init_pair(12, footer_line_fg, -1); 1164 attron(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0)); 1165 mvhline(LINES - 2, 0, ACS_HLINE, cols); 1166 attroff(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0)); 1167 } else { 1168 mvhline(LINES - 2, 0, ACS_HLINE, cols); 1169 } 1170 1171 /* footer text as normal in footer color */ 1172 print_footer("Press any key to continue..."); 1173 refresh(); 1174 } 1175 1176 /* 22.00 */ static void 1177 edit_entry(int index) 1178 { 1179 const char *editor = getenv("EDITOR"); 1180 FILE *fp; 1181 char line[1024]; 1182 char search_tag[64]; 1183 char cmd[1024]; 1184 int line_num, target_line, ret; 1185 1186 if (!editor) { 1187 const char *env = getenv("EDITOR"); 1188 editor = (env && *env) ? env : "vim"; 1189 } 1190 1191 if (index < 0 || index >= entry_count) 1192 return; 1193 1194 fp = fopen(hosts_path, "r"); 1195 if (!fp) { 1196 mvprintw(LINES - 1, 2, "Error: Couldn't open hosts."); 1197 refresh(); 1198 napms(1500); 1199 return; 1200 } 1201 1202 line_num = 0; 1203 target_line = -1; 1204 1205 snprintf(search_tag, sizeof(search_tag), "# %d. %s", index + 1, entries[index]); 1206 1207 while (fgets(line, sizeof(line), fp)) { 1208 line_num++; 1209 if (strstr(line, search_tag)) { 1210 target_line = line_num; 1211 break; 1212 } 1213 } 1214 fclose(fp); 1215 1216 if (target_line == -1) { 1217 mvprintw(LINES - 1, 2, "No entry found."); 1218 refresh(); 1219 napms(1500); 1220 return; 1221 } 1222 is_checking = 0; 1223 1224 snprintf(cmd, sizeof(cmd), "%s +%d %s", editor, target_line, hosts_path); 1225 1226 endwin(); 1227 1228 ret = system(cmd); 1229 1230 refresh(); 1231 clear(); 1232 1233 if (ret == 0) { 1234 /* full recount for all sections */ 1235 for (int i = 0; i < entry_count; i++) { 1236 domains_counts[i] = count_hosts_in_section(i); 1237 updates_counts[i] = domains_counts[i]; 1238 } 1239 save_counts(); 1240 } 1241 } 1242 1243 /* 23.00 */ static void 1244 add_entry(void) 1245 { 1246 char input[MAX_LINE_LEN] = {0}; 1247 int pos = 0, ch, next; 1248 1249 echo(); 1250 curs_set(1); 1251 1252 const char *prompt = "Url/List: "; 1253 print_footer("%s", prompt); 1254 1255 int input_start_x = (int)strlen(prompt); 1256 apply_color(&config.footer, 2); 1257 mvhline(LINES - 1, input_start_x, ' ', MAX_LINE_LEN - 1); 1258 move(LINES - 1, input_start_x); 1259 1260 while ((ch = getch()) != '\n' && ch != ERR) { 1261 if (ch == 27) { 1262 nodelay(stdscr, 1); 1263 next = getch(); 1264 nodelay(stdscr, 0); 1265 if (next == ERR) goto cancel; 1266 else ungetch(next); 1267 } else if (ch == KEY_BACKSPACE || ch == 127) { 1268 if (pos > 0) { 1269 pos--; 1270 input[pos] = '\0'; 1271 mvaddch(LINES - 1, input_start_x + pos, ' '); 1272 } 1273 move(LINES - 1, input_start_x + pos); 1274 } else if (pos < MAX_LINE_LEN - 1 && isprint(ch)) { 1275 input[pos++] = ch; 1276 input[pos] = '\0'; 1277 mvaddch(LINES - 1, input_start_x + pos - 1, ch); 1278 move(LINES - 1, input_start_x + pos); 1279 } 1280 refresh(); 1281 } 1282 1283 remove_color(&config.footer, 2); 1284 1285 if (pos == 0) 1286 goto cancel; 1287 1288 noecho(); 1289 curs_set(0); 1290 1291 /* add metadata entry */ 1292 int index = entry_count; 1293 1294 if (!strchr(input, '.')) { 1295 /* local list → check write access */ 1296 if (access(hosts_path, W_OK) != 0) { 1297 print_footer("Error: Can't open hosts for writing: permission denied."); 1298 refresh(); 1299 napms(1500); 1300 goto cancel; 1301 } 1302 if (!save_to_hosts_file("# (manually added entry)\n", input, index + 1)) { 1303 print_footer("Error: Could not write to hosts file."); 1304 refresh(); 1305 napms(1500); 1306 goto cancel; 1307 } 1308 1309 entries[index] = strdup(input); 1310 domains_counts[index] = count_hosts_in_section(index); 1311 updates_counts[index] = 0; 1312 remote_sizes[index] = 0; 1313 entry_count++; 1314 save_entries(); 1315 save_counts(); 1316 } else { 1317 /* remote URL → check write access */ 1318 if (!check_write_hosts(true)) { 1319 refresh(); 1320 napms(1500); 1321 goto cancel; 1322 } 1323 1324 /* add metadata entry */ 1325 entries[index] = strdup(input); 1326 domains_counts[index] = -1; 1327 updates_counts[index] = -1; 1328 remote_sizes[index] = 0; 1329 entry_count++; 1330 1331 background_fetch_entry(index); 1332 } 1333 1334 display_entries(index); 1335 highlight = index; 1336 move(LINES - 1, 0); 1337 clrtoeol(); 1338 refresh(); 1339 return; 1340 1341 cancel: 1342 remove_color(&config.footer, 2); 1343 noecho(); 1344 curs_set(0); 1345 move(LINES - 1, 0); 1346 clrtoeol(); 1347 refresh(); 1348 display_entries(highlight); 1349 return; 1350 } 1351 1352 /* 24.00 */ static void 1353 background_fetch_entry(int index) 1354 { 1355 pid_t pid = fork(); 1356 if (pid == 0) { 1357 setsid(); 1358 signal(SIGHUP, SIG_IGN); 1359 signal(SIGPIPE, SIG_IGN); 1360 1361 fclose(stdin); 1362 fclose(stdout); 1363 fclose(stderr); 1364 1365 create_fetch_lock(index); 1366 1367 char error_msg[CURL_ERROR_SIZE] = {0}; 1368 char *content = download_url(entries[index], error_msg, sizeof(error_msg)); 1369 1370 if (!content || !contains_valid_hosts_entry(content)) { 1371 if (content) free(content); 1372 remove_index_from_lock(index); 1373 _exit(1); 1374 } 1375 1376 if (!check_write_hosts(true)) { 1377 free(content); 1378 remove_index_from_lock(index); 1379 _exit(1); 1380 } 1381 1382 /* count + save */ 1383 int count = count_hosts_in_content(content); 1384 domains_counts[index] = count; 1385 updates_counts[index] = count; 1386 remote_sizes[index] = get_remote_content_length(entries[index]); 1387 1388 /* now write to hosts + urls */ 1389 save_to_hosts_file(content, entries[index], index + 1); 1390 save_entries(); 1391 save_count_for_index(index); 1392 1393 free(content); 1394 remove_index_from_lock(index); 1395 1396 if (update_pipe[1] != -1) { 1397 int idx = index; 1398 write(update_pipe[1], &idx, sizeof(int)); 1399 } 1400 1401 _exit(0); 1402 } 1403 } 1404 1405 /* 25.00 */ static void 1406 remove_entry(int index) 1407 { 1408 if (index < 0 || index >= entry_count || !entries[index]) 1409 return; 1410 1411 if (!check_write_hosts(true)) 1412 return; 1413 1414 const char *target_url = entries[index]; 1415 FILE *in = fopen(hosts_path, "r"); 1416 FILE *out = fopen("/tmp/hosts_temp", "w"); 1417 1418 if (!in || !out) { 1419 if (in) fclose(in); 1420 if (out) fclose(out); 1421 return; 1422 } 1423 1424 char line[MAX_LINE_LEN]; 1425 int skip_block = 0; 1426 1427 while (fgets(line, sizeof(line), in)) { 1428 if (strncmp(line, "# ", 2) == 0) { 1429 /* is that a block with the url? */ 1430 if (strstr(line, target_url)) { 1431 skip_block = 1; 1432 continue; 1433 } else { 1434 skip_block = 0; 1435 } 1436 } 1437 1438 if (!skip_block) 1439 fputs(line, out); 1440 } 1441 1442 fclose(in); 1443 fclose(out); 1444 1445 rename("/tmp/hosts_temp", hosts_path); 1446 1447 /* move array entries */ 1448 free(entries[index]); 1449 for (int i = index; i < entry_count - 1; i++) { 1450 entries[i] = entries[i + 1]; 1451 updates_counts[i] = updates_counts[i + 1]; 1452 domains_counts[i] = domains_counts[i + 1]; 1453 remote_sizes[i] = remote_sizes[i + 1]; 1454 selected[i] = selected[i + 1]; 1455 } 1456 entries[entry_count - 1] = NULL; 1457 updates_counts[entry_count - 1] = 0; 1458 domains_counts[entry_count - 1] = 0; 1459 remote_sizes[entry_count - 1] = 0; 1460 selected[entry_count - 1] = 0; 1461 1462 entry_count--; 1463 1464 save_entries(); 1465 save_counts(); 1466 rebuild_hostfile_headers_from_urls(); 1467 } 1468 1469 /* 26.00 */ static void 1470 remove_selected_entries(void) 1471 { 1472 for (int i = entry_count - 1; i >= 0; i--) { 1473 if (selected[i]) { 1474 remove_entry(i); 1475 } 1476 } 1477 1478 /* reset selection */ 1479 for (int i = 0; i < MAX_ENTRIES; i++) 1480 selected[i] = 0; 1481 } 1482 1483 /* 27.00 */ static void 1484 rebuild_hostfile_headers_from_urls(void) 1485 { 1486 FILE *in = fopen(hosts_path, "r"); 1487 FILE *out = fopen("/tmp/hosts_reindexed", "w"); 1488 char line[MAX_LINE_LEN]; 1489 int entry_idx = 0; 1490 int inside_block = 0; 1491 1492 if (!in || !out) { 1493 if (in) fclose(in); 1494 if (out) fclose(out); 1495 return; 1496 } 1497 1498 while (fgets(line, sizeof(line), in)) { 1499 if (strncmp(line, "# ", 2) == 0) { 1500 if (entry_idx < entry_count) { 1501 fprintf(out, "# %d. %s\n", entry_idx + 1, entries[entry_idx]); 1502 inside_block = 1; 1503 entry_idx++; 1504 } else { 1505 inside_block = 0; 1506 } 1507 continue; 1508 } 1509 1510 if (inside_block || line[0] == '\n') { 1511 fputs(line, out); 1512 } 1513 } 1514 1515 fclose(in); 1516 fclose(out); 1517 rename("/tmp/hosts_reindexed", hosts_path); 1518 } 1519 1520 /* 28.00 */ static void 1521 merge_entry(int index) 1522 { 1523 FILE *in, *out; 1524 const char *url, *p, *end; 1525 char error_msg[CURL_ERROR_SIZE] = {0}; 1526 char *new_content; 1527 char line[MAX_LINE_LEN]; 1528 char header[MAX_LINE_LEN]; 1529 char buffer[MAX_LINE_LEN]; 1530 int skip; 1531 size_t len; 1532 1533 if (index < 0 || index >= entry_count) 1534 return; 1535 1536 if (!has_network_connection()) { 1537 print_footer("Error: No network connection."); 1538 refresh(); 1539 napms(2000); 1540 return; 1541 } 1542 1543 if (is_local_entry(entries[index])) 1544 return; 1545 1546 if (updates_counts[index] == domains_counts[index]) { 1547 print_footer("Already up to date."); 1548 refresh(); 1549 napms(1500); 1550 return; 1551 } 1552 1553 print_footer("Merging %s ...", entries[index]); 1554 refresh(); 1555 1556 url = entries[index]; 1557 new_content = download_url(url, error_msg, sizeof(error_msg)); 1558 1559 if (!new_content) { 1560 print_footer("Error during download: %s", error_msg); 1561 refresh(); 1562 napms(2000); 1563 return; 1564 } 1565 1566 if (!contains_valid_hosts_entry(new_content)) { 1567 print_footer("Error: No valid hosts line."); 1568 refresh(); 1569 free(new_content); 1570 napms(2000); 1571 return; 1572 } 1573 1574 if (!check_write_hosts(true)) { 1575 free(new_content); 1576 return; 1577 } 1578 1579 in = fopen(hosts_path, "r"); 1580 out = fopen("/tmp/hosts_merge_temp", "w"); 1581 1582 if (!in || !out) { 1583 if (in) fclose(in); 1584 if (out) fclose(out); 1585 free(new_content); 1586 return; 1587 } 1588 1589 skip = 0; 1590 snprintf(header, sizeof(header), "# %d. %s", index + 1, url); 1591 1592 while (fgets(line, sizeof(line), in)) { 1593 if (strncmp(line, "# ", 2) == 0) { 1594 if (strncmp(line, header, strlen(header)) == 0) { 1595 skip = 1; 1596 continue; 1597 } else { 1598 skip = 0; 1599 } 1600 } 1601 if (!skip) 1602 fputs(line, out); 1603 } 1604 1605 fclose(in); 1606 1607 fprintf(out, "%s\n", header); 1608 p = new_content; 1609 1610 while (*p) { 1611 end = strchr(p, '\n'); 1612 len = end ? (size_t)(end - p) : strlen(p); 1613 1614 if (len > 0 && len < sizeof(buffer)) { 1615 memcpy(buffer, p, len); 1616 buffer[len] = '\0'; 1617 if (strncmp(buffer, "0.0.0.0 ", 8) == 0) 1618 fprintf(out, "%s\n", buffer); 1619 } 1620 1621 if (!end) 1622 break; 1623 p = end + 1; 1624 } 1625 1626 fclose(out); 1627 rename("/tmp/hosts_merge_temp", hosts_path); 1628 free(new_content); 1629 1630 domains_counts[index] = count_hosts_in_section(index); 1631 updates_counts[index] = domains_counts[index]; 1632 remote_sizes[index] = get_remote_content_length(entries[index]); 1633 save_counts(); 1634 } 1635 1636 /* 29.00 */ static void 1637 merge_selected_entries(void) 1638 { 1639 int i; 1640 1641 if (!check_write_hosts(true)) 1642 return; 1643 1644 for (i = 0; i < entry_count; i++) { 1645 if (selected[i] && !is_local_entry(entries[i])) 1646 merge_entry(i); 1647 } 1648 1649 for (i = 0; i < MAX_ENTRIES; i++) 1650 selected[i] = 0; 1651 } 1652 1653 /* 30.00 */ static void 1654 rename_entry(int index) 1655 { 1656 FILE *in, *out; 1657 char input[MAX_LINE_LEN] = {0}; 1658 char line[MAX_LINE_LEN]; 1659 char old_header[MAX_LINE_LEN]; 1660 char new_header[MAX_LINE_LEN]; 1661 int input_start_x, pos, ch, next; 1662 1663 if (index < 0 || index >= entry_count) 1664 return; 1665 1666 if (!is_local_entry(entries[index])) 1667 return; 1668 1669 echo(); 1670 curs_set(1); 1671 1672 /* footer-prompt */ 1673 const char *prompt = "Rename to: "; 1674 print_footer("%s", prompt); 1675 1676 input_start_x = (int)strlen(prompt); 1677 apply_color(&config.footer, 2); 1678 mvhline(LINES - 1, input_start_x, ' ', MAX_LINE_LEN - 1); 1679 1680 strncpy(input, entries[index], MAX_LINE_LEN - 1); 1681 pos = strlen(input); 1682 1683 mvprintw(LINES - 1, input_start_x, "%s", input); 1684 move(LINES - 1, input_start_x + pos); 1685 refresh(); 1686 1687 while ((ch = getch()) != '\n' && ch != ERR) { 1688 if (ch == 27) { 1689 nodelay(stdscr, 1); 1690 next = getch(); 1691 nodelay(stdscr, 0); 1692 if (next == ERR) { 1693 remove_color(&config.footer, 2); 1694 noecho(); 1695 curs_set(0); 1696 move(LINES - 1, 0); 1697 clrtoeol(); 1698 refresh(); 1699 return; 1700 } else { 1701 ungetch(next); 1702 } 1703 } else if (ch == KEY_BACKSPACE || ch == 127) { 1704 if (pos > 0) { 1705 pos--; 1706 input[pos] = '\0'; 1707 mvaddch(LINES - 1, input_start_x + pos, ' '); 1708 } 1709 move(LINES - 1, input_start_x + pos); 1710 } else if (pos < MAX_LINE_LEN - 1 && isprint(ch)) { 1711 input[pos++] = ch; 1712 input[pos] = '\0'; 1713 mvaddch(LINES - 1, input_start_x + pos - 1, ch); 1714 move(LINES - 1, input_start_x + pos); 1715 } 1716 refresh(); 1717 } 1718 1719 remove_color(&config.footer, 2); 1720 1721 noecho(); 1722 curs_set(0); 1723 move(LINES - 1, 0); 1724 clrtoeol(); 1725 refresh(); 1726 1727 if (pos == 0 || strspn(input, " \t") == strlen(input)) 1728 return; 1729 1730 if (!check_write_hosts(true)) 1731 return; 1732 1733 snprintf(old_header, sizeof(old_header), 1734 "# %d. %s", index + 1, entries[index]); 1735 snprintf(new_header, sizeof(new_header), 1736 "# %d. %.220s", index + 1, input); 1737 1738 in = fopen(hosts_path, "r"); 1739 out = fopen("/tmp/hosts_rename_temp", "w"); 1740 1741 if (!in || !out) { 1742 if (in) fclose(in); 1743 if (out) fclose(out); 1744 return; 1745 } 1746 1747 while (fgets(line, sizeof(line), in)) { 1748 if (strncmp(line, old_header, strlen(old_header)) == 0) { 1749 fprintf(out, "%s\n", new_header); 1750 } else { 1751 fputs(line, out); 1752 } 1753 } 1754 1755 fclose(in); 1756 fclose(out); 1757 rename("/tmp/hosts_rename_temp", hosts_path); 1758 1759 free(entries[index]); 1760 entries[index] = strdup(input); 1761 save_entries(); 1762 1763 display_entries(index); 1764 } 1765 1766 /* 31.00 */ const char * 1767 get_action_for_key(int key) 1768 { 1769 for (int i = 0; i < keybinding_count; i++) { 1770 if (keybindings[i].key == key) 1771 return keybindings[i].action; 1772 } 1773 return NULL; 1774 } 1775 1776 /* 32.00 */ static int 1777 has_network_connection(void) 1778 { 1779 struct addrinfo hints = {0}, *res = NULL; 1780 int result; 1781 1782 hints.ai_family = AF_UNSPEC; 1783 hints.ai_socktype = SOCK_STREAM; 1784 1785 /* try resolving google.com */ 1786 result = getaddrinfo("google.com", NULL, &hints, &res); 1787 if (res) 1788 freeaddrinfo(res); 1789 1790 return result == 0; 1791 } 1792 1793 /* 33.00 */ int 1794 main(int argc, char *argv[]) 1795 { 1796 int ch; 1797 const char *editor = getenv("EDITOR"); 1798 1799 load_config(); 1800 1801 get_config_path("counts", counts_path, sizeof(counts_path)); 1802 get_config_path("urls", urls_path, sizeof(urls_path)); 1803 get_config_path("fetch.lock", fetch_lock_path, sizeof(fetch_lock_path)); 1804 1805 setlocale(LC_ALL, ""); 1806 curl_global_init(CURL_GLOBAL_DEFAULT); 1807 update_progress = -1; 1808 1809 load_entries(); 1810 load_counts(); 1811 1812 if (pipe(update_pipe) < 0) { 1813 perror("pipe"); 1814 } else { 1815 fcntl(update_pipe[0], F_SETFL, O_NONBLOCK); 1816 fcntl(update_pipe[1], F_SETFD, 0); 1817 } 1818 1819 if (fetch_lock_exists() && !is_fetch_lock_empty()) { 1820 is_checking = 1; 1821 start_update_in_background(); 1822 } else { 1823 is_checking = 0; 1824 } 1825 1826 if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) { 1827 printf("Usage: hfc [OPTION]\n\n"); 1828 printf("Options:\n"); 1829 printf(" -h, --help Show usage information.\n"); 1830 printf(" -a, --add <url/list> Add a remote URL or custom list.\n"); 1831 printf(" -r, --remove <url/list> Remove a remote URL (https://...) or custom list.\n"); 1832 printf(" -e, --edit <list> Edit a local entry section with \033[1m$EDITOR\033[0m\n"); 1833 printf(" -U, --update_all Update all remote sources\n"); 1834 printf("\nReport bugs via xmpp to chat@marlonivo.xyz.\n"); 1835 return 0; 1836 } 1837 1838 if (argc == 2 && ( 1839 !strcmp(argv[1], "-a") || !strcmp(argv[1], "--add") || 1840 !strcmp(argv[1], "-r") || !strcmp(argv[1], "--remove") || 1841 !strcmp(argv[1], "-e") || !strcmp(argv[1], "--edit") 1842 )) { 1843 fprintf(stderr, 1844 "hosts file client\n" 1845 "usage: hfc '%s' <url/list>\n" 1846 "\n" 1847 "Use -h to get help or, even better, run 'man hfc'\n", argv[1]); 1848 return 1; 1849 } 1850 1851 if (argc == 3 && (!strcmp(argv[1], "-a") || !strcmp(argv[1], "--add"))) { 1852 char *url = argv[2]; 1853 1854 if (!has_network_connection()) { 1855 fprintf(stderr, "Error: No network connection.\n"); 1856 return 1; 1857 } 1858 1859 char error_msg[CURL_ERROR_SIZE] = {0}; 1860 char *content = download_url(url, error_msg, sizeof(error_msg)); 1861 1862 if (!content) { 1863 fprintf(stderr, "Download failed: %s\n", error_msg); 1864 return 1; 1865 } 1866 1867 if (!contains_valid_hosts_entry(content)) { 1868 fprintf(stderr, "Error: No valid hosts entries found.\n"); 1869 free(content); 1870 return 1; 1871 } 1872 1873 if (entry_count >= MAX_ENTRIES) { 1874 fprintf(stderr, "Error: Maximum number of entries reached.\n"); 1875 free(content); 1876 return 1; 1877 } 1878 1879 if (!check_write_hosts(true)) { 1880 free(content); 1881 return 1; 1882 } 1883 1884 entries[entry_count] = strdup(url); 1885 entry_count++; 1886 save_entries(); 1887 1888 if (!save_to_hosts_file(content, url, entry_count)) { 1889 fprintf(stderr, "Error: Could not write to hosts.\n"); 1890 free(content); 1891 return 1; 1892 } 1893 1894 int local_count = count_hosts_in_section(entry_count - 1); 1895 domains_counts[entry_count - 1] = local_count; 1896 update_domain_count(entry_count - 1); 1897 save_counts(); 1898 free(content); 1899 1900 printf("Entry added successfully: %s\n", url); 1901 return 0; 1902 } 1903 1904 if (argc == 3 && (!strcmp(argv[1], "-r") || !strcmp(argv[1], "--remove"))) { 1905 char *url = argv[2]; 1906 int found = -1; 1907 1908 for (int i = 0; i < entry_count; i++) { 1909 if (strcmp(entries[i], url) == 0) { 1910 found = i; 1911 break; 1912 } 1913 } 1914 1915 if (found == -1) { 1916 fprintf(stderr, "Entry not found: %s\n", url); 1917 return 1; 1918 } 1919 1920 if (!check_write_hosts(true)) { 1921 return 1; 1922 } 1923 1924 remove_entry(found); 1925 printf("Entry removed successfully: %s\n", url); 1926 return 0; 1927 } 1928 1929 if (argc == 2 && (!strcmp(argv[1], "-U") || !strcmp(argv[1], "--update_all"))) { 1930 if (!has_network_connection()) { 1931 fprintf(stderr, "Error: No network connection.\n"); 1932 return 1; 1933 } 1934 1935 int total = 0; 1936 for (int i = 0; i < entry_count; i++) { 1937 if (!is_local_entry(entries[i])) total++; 1938 } 1939 1940 int current = 0; 1941 for (int i = 0; i < entry_count; i++) { 1942 if (is_local_entry(entries[i])) continue; 1943 current++; 1944 printf("(%d/%d) updating %s\n", current, total, entries[i]); 1945 fflush(stdout); 1946 update_domain_count(i); 1947 } 1948 1949 save_counts(); 1950 printf("All sources updated.\n"); 1951 return 0; 1952 } 1953 1954 if (argc == 3 && (!strcmp(argv[1], "-e") || !strcmp(argv[1], "--edit"))) { 1955 char *url = argv[2]; 1956 int found = -1; 1957 1958 for (int i = 0; i < entry_count; i++) { 1959 if (strcmp(entries[i], url) == 0) { 1960 found = i; 1961 break; 1962 } 1963 } 1964 1965 if (found == -1) { 1966 fprintf(stderr, "Entry not found: %s\n", url); 1967 return 1; 1968 } 1969 1970 FILE *fp = fopen(hosts_path, "r"); 1971 if (!fp) { 1972 fprintf(stderr, "Error: Could not open hosts\n"); 1973 return 1; 1974 } 1975 1976 char line[1024]; 1977 int line_num = 0; 1978 int target_line = -1; 1979 char search_tag[256]; 1980 snprintf(search_tag, sizeof(search_tag), "# %d. %s", found + 1, entries[found]); 1981 1982 while (fgets(line, sizeof(line), fp)) { 1983 line_num++; 1984 if (strstr(line, search_tag)) { 1985 target_line = line_num; 1986 break; 1987 } 1988 } 1989 fclose(fp); 1990 1991 if (target_line == -1) { 1992 fprintf(stderr, "Error: Entry header not found in hosts\n"); 1993 return 1; 1994 } 1995 1996 if (!check_write_hosts(true)) { 1997 return 1; 1998 } 1999 2000 char cmd[1024]; 2001 snprintf(cmd, sizeof(cmd), "%s +%d %s", editor, target_line, hosts_path); 2002 2003 int ret = system(cmd); 2004 return ret; 2005 } 2006 2007 /* 34.00 */ initscr(); 2008 2009 /* respect terminal themes (e.g. gruvbox) */ 2010 if (has_colors()) { 2011 start_color(); 2012 use_default_colors(); 2013 load_config(); 2014 init_colors(); 2015 } 2016 2017 init_footer(); 2018 2019 /* non-blocking input */ 2020 set_escdelay(10); 2021 cbreak(); 2022 noecho(); 2023 keypad(stdscr, TRUE); 2024 curs_set(0); 2025 timeout(100); 2026 2027 /* render initial screen */ 2028 display_entries(highlight); 2029 refresh(); 2030 update_all_sources(); 2031 2032 while (1) { 2033 /* check for background fetches */ 2034 int status; 2035 pid_t pid; 2036 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { 2037 load_counts(); 2038 display_entries(highlight); 2039 } 2040 /* check background_fetch_entry to refresh UI */ 2041 int idx; 2042 while (read(update_pipe[0], &idx, sizeof(int)) > 0) { 2043 if (idx >= 0 && idx < entry_count) { 2044 // Nur zurücksetzen, wenn es ein Count-Vorgang war: 2045 if (is_updating[idx]) { 2046 is_updating[idx] = 0; 2047 } 2048 display_entries(highlight); 2049 } 2050 } 2051 2052 /* handle user input */ 2053 ch = getch(); 2054 2055 if (ch == KEY_RESIZE) { 2056 endwin(); refresh(); clear(); 2057 display_entries(highlight); 2058 continue; 2059 } 2060 2061 if (ch == ERR) continue; 2062 2063 /* dismiss help screen */ 2064 if (in_help_mode) { 2065 in_help_mode = 0; 2066 display_entries(highlight); 2067 continue; 2068 } 2069 2070 /* all keys */ 2071 const char *action = get_action_for_key(ch); 2072 if (!action) continue; 2073 2074 /* core actions */ 2075 if (!strcmp(action, "quit")) break; 2076 else if (!strcmp(action, "select")) { 2077 selected[highlight] = !selected[highlight]; 2078 display_entries(highlight); 2079 } 2080 else if (!strcmp(action, "select_all")) { 2081 for (int i = 0; i < entry_count; i++) selected[i] = 1; 2082 display_entries(highlight); 2083 } 2084 else if (!strcmp(action, "unselect_all")) { 2085 for (int i = 0; i < entry_count; i++) selected[i] = 0; 2086 display_entries(highlight); 2087 } 2088 else if (!strcmp(action, "up")) { 2089 if (highlight > 0) highlight--; 2090 display_entries(highlight); 2091 } 2092 else if (!strcmp(action, "down")) { 2093 if (highlight < entry_count - 1) highlight++; 2094 display_entries(highlight); 2095 } 2096 else if (!strcmp(action, "rename")) { 2097 if (entry_count > 0 && is_local_entry(entries[highlight])) { 2098 timeout(-1); 2099 rename_entry(highlight); 2100 timeout(100); 2101 display_entries(highlight); 2102 } 2103 } 2104 else if (!strcmp(action, "refresh")) { 2105 load_config(); init_colors(); 2106 clear(); load_entries(); load_counts(); 2107 if (highlight >= entry_count) highlight = entry_count - 1; 2108 if (highlight < 0) highlight = 0; 2109 display_entries(highlight); 2110 } 2111 else if (!strcmp(action, "order")) { 2112 if (entry_count > 1) { 2113 timeout(-1); 2114 2115 char prompt[64]; 2116 snprintf(prompt, sizeof(prompt), "Move to position (1-%d): ", entry_count); 2117 print_footer(" %s", prompt); 2118 2119 int input_col = 2 + (int)strlen(prompt); 2120 char input[8] = {0}; 2121 2122 apply_color(&config.footer, 2); 2123 mvhline(LINES - 1, input_col, ' ', sizeof(input)); 2124 2125 echo(); 2126 curs_set(1); 2127 int ch = getch(); 2128 if (ch == 27) { 2129 noecho(); 2130 curs_set(0); 2131 remove_color(&config.footer, 2); 2132 display_entries(highlight); 2133 timeout(100); 2134 continue; 2135 } 2136 ungetch(ch); 2137 mvgetnstr(LINES - 1, input_col, input, sizeof(input) - 1); 2138 clrtoeol(); 2139 2140 remove_color(&config.footer, 2); 2141 noecho(); 2142 curs_set(0); 2143 2144 int new_pos = atoi(input) - 1; 2145 if (new_pos >= 0 && new_pos < entry_count && new_pos != highlight) { 2146 reorder_entry(highlight, new_pos); 2147 highlight = new_pos; 2148 display_entries(highlight); 2149 } else { 2150 print_footer("Invalid position."); 2151 refresh(); 2152 napms(1000); 2153 display_entries(highlight); 2154 } 2155 2156 timeout(100); 2157 } 2158 } 2159 else if (!strcmp(action, "remove")) { 2160 if (entry_count > 0) { 2161 int selected_any = 0; 2162 for (int i = 0; i < entry_count; i++) { 2163 if (selected[i]) { selected_any = 1; break; } 2164 } 2165 timeout(-1); 2166 if (selected_any) { 2167 print_footer("Delete selected entries? (y/n): "); 2168 } else { 2169 print_footer("Remove %s? (y/n): ", entries[highlight]); 2170 } 2171 int confirm = getch(); 2172 timeout(100); 2173 2174 if (confirm == 'y' || confirm == 'Y') { 2175 if (selected_any) { 2176 print_footer("Deleting selected entries..."); 2177 refresh(); 2178 napms(100); 2179 remove_selected_entries(); 2180 for (int i = 0; i < MAX_ENTRIES; i++) selected[i] = 0; 2181 highlight = 0; 2182 } else { 2183 print_footer("Deleting..."); 2184 refresh(); 2185 napms(100); 2186 remove_entry(highlight); 2187 if (highlight >= entry_count && highlight > 0) highlight--; 2188 } 2189 display_entries(highlight); 2190 } else { 2191 display_entries(highlight); 2192 } 2193 } 2194 } 2195 else if (!strcmp(action, "help")) { 2196 in_help_mode = 1; 2197 display_help(); 2198 } 2199 else if (!strcmp(action, "add")) { 2200 timeout(-1); 2201 add_entry(); 2202 timeout(100); 2203 display_entries(highlight); 2204 } 2205 else if (!strcmp(action, "edit")) { 2206 if (entry_count > 0) { 2207 edit_entry(highlight); 2208 display_entries(highlight); 2209 } 2210 } 2211 else if (!strcmp(action, "update")) { 2212 int any = 0; 2213 for (int i = 0; i < entry_count; i++) { 2214 if (selected[i]) { any = 1; break; } 2215 } 2216 if (!has_network_connection()) { 2217 print_footer("Error: No network connection."); 2218 display_entries(highlight); 2219 continue; 2220 } 2221 if (any) { 2222 update_selected_sources(); 2223 } else { 2224 if (is_local_entry(entries[highlight])) { 2225 display_entries(highlight); 2226 continue; 2227 } 2228 print_footer("Checking for updates..."); 2229 refresh(); 2230 napms(100); 2231 long before = remote_sizes[highlight]; 2232 long after = get_remote_content_length(entries[highlight]); 2233 if (after > 0 && after != before) { 2234 print_footer("Updating %s...", entries[highlight]); 2235 background_count_entry(highlight); 2236 domains_counts[highlight] = count_hosts_in_section(highlight); 2237 updates_counts[highlight] = domains_counts[highlight]; 2238 save_counts(); 2239 } else { 2240 print_footer("No updates available."); 2241 napms(1500); 2242 } 2243 } 2244 display_entries(highlight); 2245 } 2246 else if (!strcmp(action, "update_all")) { 2247 if (!has_network_connection()) { 2248 print_footer("Error: No network connection."); 2249 napms(2000); 2250 display_entries(highlight); 2251 continue; 2252 } 2253 update_all_sources(); 2254 display_entries(highlight); 2255 } 2256 else if (!strcmp(action, "merge")) { 2257 if (entry_count > 0 && !is_local_entry(entries[highlight])) { 2258 int any = 0; 2259 for (int i = 0; i < entry_count; i++) { 2260 if (selected[i]) { any = 1; break; } 2261 } 2262 if (any) { 2263 print_footer("Merging selected entries..."); 2264 merge_selected_entries(); 2265 } else { 2266 merge_entry(highlight); 2267 } 2268 display_entries(highlight); 2269 } 2270 } 2271 } 2272 2273 free_entries(); 2274 endwin(); 2275 curl_global_cleanup(); 2276 return 0; 2277 }