hfc

Hosts file client
git clone git://git.marlonivo.com/hfc
Log | Files | Refs | LICENSE

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 }