commit 18ddf9d6a3cbfd4806ec6a361cf893524e44122b
parent 19372596c16cea5b42b47e821075ade0ab66a40e
Author: Marlon Ivo <email@marlonivo.xyz>
Date: Mon, 18 Aug 2025 08:42:13 +0000
its alive
Diffstat:
A | LICENSE | | | 21 | +++++++++++++++++++++ |
A | Makefile | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
M | PKGBUILD | | | 2 | +- |
A | config.c | | | 385 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.h | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
A | get.c | | | 205 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | get.h | | | 24 | ++++++++++++++++++++++++ |
A | hfc.1 | | | 131 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | hfc.c | | | 2195 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | hfc.h | | | 32 | ++++++++++++++++++++++++++++++++ |
A | update.c | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | update.h | | | 10 | ++++++++++ |
12 files changed, 3147 insertions(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT/X Consortium License
+
+© 2024-2025 Marlon Ivo <email@marlonivo.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,45 @@
+# hfc - hosts file client
+# See LICENSE file for copyright and license details.
+
+CC = gcc
+CFLAGS = -Wall -Wextra -std=c11 -D_POSIX_C_SOURCE=200809L
+LDFLAGS = -lcurl -lncurses
+
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/share/man
+VERSION = 0.0.1
+
+SRCS = hfc.c get.c update.c config.c
+OBJS = hfc.o get.o update.o config.o
+TARGET = hfc
+
+all: prepare $(TARGET)
+prepare:
+ mkdir -p $(HOME)/.config/hfc
+ mkdir -p $(HOME)/.local/state
+ touch $(HOME)/.config/hfc/urls
+ touch $(HOME)/.config/hfc/conf
+ touch $(HOME)/.local/state/counts
+
+$(TARGET): $(OBJS)
+ $(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+install:
+ mkdir -p ${DESTDIR}${PREFIX}/bin
+ install -Dm755 ./$(TARGET) ${DESTDIR}${PREFIX}/bin/$(TARGET)
+ mkdir -p ${DESTDIR}${MANPREFIX}/man1
+ sed "s/VERSION/${VERSION}/g" < hfc.1 > ${DESTDIR}${MANPREFIX}/man1/hfc.1
+ chmod 644 ${DESTDIR}${MANPREFIX}/man1/hfc.1
+
+uninstall:
+ rm -f ${DESTDIR}${PREFIX}/bin/$(TARGET)
+ rm -f ${DESTDIR}${MANPREFIX}/man1/hfc.1
+
+clean:
+ rm -f $(OBJS) $(TARGET)
+
+.PHONY: all clean install uninstall prepare
+
diff --git a/PKGBUILD b/PKGBUILD
@@ -7,7 +7,7 @@ url="https://git.marlonivo.xyz/hfc"
license=('MIT')
depends=('curl' 'ncurses')
makedepends=('gcc' 'make')
-source=("hfc::git+file://$PWD")
+source=("hfc::git+https://git.marlonivo.xyz/hfc")
md5sums=('SKIP')
build() {
diff --git a/config.c b/config.c
@@ -0,0 +1,385 @@
+/* config.c - hfc config file
+*
+* headers and macros */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <ncurses.h>
+#include <unistd.h>
+#include "config.h"
+
+/* functions */
+/* 01.00 */ static void trim(char *str);
+/* 02.00 */ static int get_special_key_code(const char *name);
+/* 03.00 */ static void extract_color(const char *input, char *output);
+/* 04.00 */ void remove_keybindings_for_action(const char *action);
+/* 05.00 */ void load_config(void);
+/* 06.00 */ short get_color_code(const char *name);
+/* 07.00 */ void init_colors(void);
+/* 08.00 */ void apply_color(ColorSetting *setting, short pair_id);
+/* 09.00 */ void remove_color(ColorSetting *setting, short pair_id);
+/* 10.00 */ const char *get_config_path(const char *filename, char *buffer, size_t size);
+/* 11.00 */ const char *get_keys_for_action(const char *action);
+
+HFCConfig config;
+
+/* mapping table */
+typedef struct {
+ const char *name;
+ int code;
+} KeyName;
+
+static KeyName special_keys[] = {
+ { "key_up", KEY_UP },
+ { "key_down", KEY_DOWN },
+ { "key_left", KEY_LEFT },
+ { "key_right", KEY_RIGHT },
+ { "key_home", KEY_HOME },
+ { "key_end", KEY_END },
+ { "key_npage", KEY_NPAGE },
+ { "key_ppage", KEY_PPAGE },
+ { "key_enter", 10 },
+ { "space", ' ' },
+};
+
+/* 01.00 */ static void
+trim(char *str)
+{
+ char *end;
+ while (isspace((unsigned char)*str)) str++;
+ end = str + strlen(str) - 1;
+ while (end > str && isspace((unsigned char)*end)) end--;
+ end[1] = '\0';
+}
+
+/* 02.00 */ static int
+get_special_key_code(const char *name)
+{
+ for (size_t i = 0; i < sizeof(special_keys) / sizeof(special_keys[0]); i++) {
+ if (strcmp(name, special_keys[i].name) == 0)
+ return special_keys[i].code;
+ }
+ return -1;
+}
+
+/* 03.00 */ static void
+extract_color(const char *input, char *output)
+{
+ if (!input || !*input) {
+ *output = '\0';
+ return;
+ }
+
+ while (isspace((unsigned char)*input)) input++;
+
+ if (!*input) {
+ *output = '\0';
+ return;
+ }
+
+ sscanf(input, "%15s", output);
+}
+
+/* 04.00 */ void remove_keybindings_for_action(const char *action) {
+ for (int i = 0; i < keybinding_count; ) {
+ if (strcmp(keybindings[i].action, action) == 0) {
+ for (int j = i; j < keybinding_count - 1; j++) {
+ keybindings[j] = keybindings[j + 1];
+ }
+ keybinding_count--;
+ } else {
+ i++;
+ }
+ }
+}
+
+/* 05.00 */ void load_config(void)
+{
+ /* init config to "no colors" and default keybindings count */
+ memset(&config, 0, sizeof(HFCConfig));
+ keybinding_count = 0;
+
+ ColorSetting *all_colors[] = {
+ &config.header, &config.footer, &config.entry_selected,
+ &config.entry_highlight, &config.entry_default, &config.table_header
+ };
+ for (size_t i = 0; i < sizeof(all_colors)/sizeof(all_colors[0]); i++) {
+ all_colors[i]->fg[0] = '\0';
+ all_colors[i]->bg[0] = '\0';
+ all_colors[i]->bold = false;
+ }
+
+ char path[512];
+ get_config_path("conf", path, sizeof(path));
+
+ /* default keybindings */
+ static struct {
+ const char *key;
+ const char *action;
+ } default_keybindings[] = {
+ { "j", "down" }, { "k", "up" }, { "q", "quit" }, { "a", "add" },
+ { "r", "remove" }, { "e", "edit" }, { "u", "update" },
+ { "U", "update_all" }, { "o", "order" }, { "m", "merge" },
+ { "L", "refresh" }, { "+", "select_all" }, { "-", "unselect_all" },
+ { "space", "select" }, { "?", "help" }
+ };
+
+ FILE *fp = fopen(path, "r");
+ if (!fp) return;
+
+ char line[512];
+ char current_section[32] = {0};
+
+ /* ui color config mapping */
+ typedef struct {
+ const char *key;
+ ColorSetting *setting;
+ } ColorConfigMap;
+
+ ColorConfigMap color_map[] = {
+ { "header", &config.header },
+ { "footer", &config.footer },
+ { "entry_selected", &config.entry_selected },
+ { "entry_highlight",&config.entry_highlight },
+ { "entry_default", &config.entry_default },
+ { "table_header", &config.table_header }
+ };
+
+ while (fgets(line, sizeof(line), fp)) {
+ trim(line);
+
+ if (line[0] == '#' || line[0] == '\0') continue;
+
+ if (line[0] == '[') {
+ sscanf(line, "[%31[^]]", current_section);
+ continue;
+ }
+
+ if (strcmp(current_section, "ui") == 0) {
+ char word1[64] = {0}, word2[64] = {0}, word3[64] = {0};
+ char word4[64] = {0}, word5[64] = {0};
+
+ if (sscanf(line, "%63s %63s %63s %63s %63s", word1, word2, word3, word4, word5) >= 3
+ && strcmp(word1, "color") == 0) {
+
+ for (size_t i = 0; i < sizeof(color_map)/sizeof(color_map[0]); ++i) {
+ if (strcmp(word2, color_map[i].key) == 0) {
+ ColorSetting *set = color_map[i].setting;
+
+ strncpy(set->fg, word3, sizeof(set->fg));
+
+ if (strcmp(word4, "bold") == 0) {
+ set->bg[0] = '\0';
+ set->bold = true;
+ } else if (*word4) {
+ strncpy(set->bg, word4, sizeof(set->bg));
+ if (strcmp(word5, "bold") == 0)
+ set->bold = true;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ if (strcmp(current_section, "keys") == 0) {
+ if (strncmp(line, "bind ", 5) == 0 && keybinding_count < MAX_KEYBINDINGS) {
+ char key_str[32], action[32];
+ if (sscanf(line + 5, "%31s %31s", key_str, action) == 2) {
+ int key = get_special_key_code(key_str);
+ if (key == -1 && strlen(key_str) == 1)
+ key = key_str[0];
+
+ if (key != -1) {
+ remove_keybindings_for_action(action);
+ keybindings[keybinding_count].key = key;
+ strncpy(keybindings[keybinding_count].action, action,
+ sizeof(keybindings[keybinding_count].action));
+ keybinding_count++;
+ }
+ }
+ }
+ }
+ }
+
+ fclose(fp);
+
+ /* add fallback keybindings if missing */
+ for (size_t i = 0; i < sizeof(default_keybindings) / sizeof(default_keybindings[0]); i++) {
+ const char *action = default_keybindings[i].action;
+ int already_bound = 0;
+
+ for (int j = 0; j < keybinding_count; j++) {
+ if (strcmp(keybindings[j].action, action) == 0) {
+ already_bound = 1;
+ break;
+ }
+ }
+
+ if (!already_bound && keybinding_count < MAX_KEYBINDINGS) {
+ int key = get_special_key_code(default_keybindings[i].key);
+ if (key == -1 && strlen(default_keybindings[i].key) == 1)
+ key = default_keybindings[i].key[0];
+
+ keybindings[keybinding_count].key = key;
+ strncpy(keybindings[keybinding_count].action,
+ action,
+ sizeof(keybindings[keybinding_count].action));
+ keybinding_count++;
+ }
+ }
+}
+
+/* 06.00 */ short
+get_color_code(const char *name)
+{
+ if (!name || !*name) return -1;
+
+ struct {
+ const char *name;
+ short code;
+ } colors[] = {
+ { "black", COLOR_BLACK },
+ { "red", COLOR_RED },
+ { "green", COLOR_GREEN },
+ { "yellow", COLOR_YELLOW },
+ { "blue", COLOR_BLUE },
+ { "magenta", COLOR_MAGENTA },
+ { "cyan", COLOR_CYAN },
+ { "white", COLOR_WHITE },
+ { "gray", 8 },
+ };
+
+ for (size_t i = 0; i < sizeof(colors)/sizeof(colors[0]); i++) {
+ if (strcmp(name, colors[i].name) == 0)
+ return colors[i].code;
+ }
+
+ return -1;
+}
+
+/* 07.00 */ void
+init_colors(void)
+{
+ if (!has_colors()) return;
+
+ start_color();
+ use_default_colors();
+
+ struct {
+ ColorSetting *setting;
+ short pair_id;
+ } pairs[] = {
+ { &config.header, 1 },
+ { &config.footer, 2 },
+ { &config.entry_selected, 3 },
+ { &config.entry_highlight,4 },
+ { &config.table_header, 5 },
+ { &config.entry_default, 6 }
+ };
+
+ char fg_buf[16], bg_buf[16];
+ short fg_code, bg_code;
+
+ for (size_t i = 0; i < sizeof(pairs)/sizeof(pairs[0]); i++) {
+ ColorSetting *set = pairs[i].setting;
+
+ /* extract color */
+ extract_color(set->fg, fg_buf);
+ extract_color(set->bg, bg_buf);
+
+ fg_code = get_color_code(fg_buf);
+ bg_code = get_color_code(bg_buf);
+
+ /* only initialize if at least one valid color */
+ if (fg_code != -1 || bg_code != -1) {
+ init_pair(pairs[i].pair_id,
+ (bg_code != -1 ? bg_code : -1),
+ (fg_code != -1 ? fg_code : -1));
+ }
+ }
+}
+
+/* 08.00 */ void
+apply_color(ColorSetting *setting, short pair_id)
+{
+ if (!*setting->fg && !*setting->bg)
+ return;
+
+ int attrs = COLOR_PAIR(pair_id);
+ if (setting->bold)
+ attrs |= A_BOLD;
+ attron(attrs);
+}
+
+/* 09.00 */ void
+remove_color(ColorSetting *setting, short pair_id)
+{
+ if (!*setting->fg && !*setting->bg)
+ return;
+
+ int attrs = COLOR_PAIR(pair_id);
+ if (setting->bold)
+ attrs |= A_BOLD;
+ attroff(attrs);
+}
+
+/* 10.00 */ const char *
+get_config_path(const char *filename, char *buffer, size_t size)
+{
+ const char *home;
+
+ home = getenv("HOME");
+ if (!home)
+ return NULL;
+
+ if (strcmp(filename, "counts") == 0) {
+ snprintf(buffer, size, "%s/.local/state/hfc/%s", home,filename);
+ } else {
+ const char *config_home = getenv("XDG_CONFIG_HOME");
+
+ if (!config_home || strlen(config_home) == 0) {
+ snprintf(buffer, size, "%s/.config/hfc/%s", home, filename);
+ } else {
+ snprintf(buffer, size, "%s/hfc/%s", config_home, filename);
+ }
+ }
+
+ return buffer;
+}
+
+/* 11.00 */ const char *
+get_keys_for_action(const char *action) {
+ static char buf[128];
+ buf[0] = '\0';
+
+ for (int i = 0; i < keybinding_count; i++) {
+ if (strcmp(keybindings[i].action, action) == 0) {
+ char keyname[32];
+
+ /* check special keys */
+ int found_special = 0;
+ for (size_t j = 0; j < sizeof(special_keys)/sizeof(special_keys[0]); j++) {
+ if (keybindings[i].key == special_keys[j].code) {
+ snprintf(keyname, sizeof(keyname), "%s", special_keys[j].name);
+ found_special = 1;
+ break;
+ }
+ }
+ if (!found_special) {
+ snprintf(keyname, sizeof(keyname), "%c", keybindings[i].key);
+ }
+
+ if (strstr(buf, keyname) == NULL) {
+ if (buf[0] != '\0') {
+ strncat(buf, "/", sizeof(buf) - strlen(buf) - 1);
+ }
+ strncat(buf, keyname, sizeof(buf) - strlen(buf) - 1);
+ }
+ }
+ }
+
+ return (buf[0] != '\0') ? buf : "?";
+}
diff --git a/config.h b/config.h
@@ -0,0 +1,46 @@
+/* hfc config file */
+
+#ifndef HFC_CONFIG_H
+#define HFC_CONFIG_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#define MAX_KEYBINDINGS 64
+
+typedef struct {
+ char fg[16];
+ char bg[16];
+ bool bold;
+} ColorSetting;
+
+typedef struct {
+ ColorSetting header;
+ ColorSetting footer;
+ ColorSetting entry_selected;
+ ColorSetting entry_highlight;
+ ColorSetting entry_default;
+ ColorSetting table_header;
+
+ char path_urls[256];
+} HFCConfig;
+
+typedef struct {
+ int key;
+ char action[32];
+} KeyBinding;
+
+extern HFCConfig config;
+extern KeyBinding keybindings[MAX_KEYBINDINGS];
+extern int keybinding_count;
+
+void init_colors(void);
+void load_config(void);
+const char *get_config_path(const char *filename, char *buffer, size_t size);
+void apply_color(ColorSetting *setting, short pair_id);
+void remove_color(ColorSetting *setting, short pair_id);
+const char *get_keys_for_action(const char *action);
+short get_color_code(const char *name);
+
+#endif
+
diff --git a/get.c b/get.c
@@ -0,0 +1,205 @@
+/* get.c - download and save hosts entries from URL sources
+*
+* headers and macros */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <curl/curl.h>
+#include <ctype.h>
+
+#include "get.h"
+#include "hfc.h"
+
+#define HOSTS_FILE_PATH "/etc/hosts"
+
+struct MemoryStruct {
+ char *memory;
+ size_t size;
+};
+
+static size_t
+WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
+{
+ size_t realsize;
+ char *ptr;
+ struct MemoryStruct *mem;
+
+ realsize = size * nmemb;
+ mem = (struct MemoryStruct *)userp;
+
+ ptr = realloc(mem->memory, mem->size + realsize + 1);
+ if (!ptr)
+ return 0;
+
+ mem->memory = ptr;
+ memcpy(&(mem->memory[mem->size]), contents, realsize);
+ mem->size += realsize;
+ mem->memory[mem->size] = '\0';
+
+ return realsize;
+}
+
+char *
+download_url(const char *url, char *error_msg, size_t err_size)
+{
+ CURL *curl_handle;
+ CURLcode res;
+ struct MemoryStruct chunk;
+
+ chunk.memory = malloc(1);
+ chunk.size = 0;
+
+ curl_handle = curl_easy_init();
+ if (!curl_handle) {
+ snprintf(error_msg, err_size, "Could not initialize CURL");
+ free(chunk.memory);
+ return NULL;
+ }
+
+ char user_agent[64];
+ snprintf(user_agent, sizeof(user_agent), "hfc/%s", HFC_VERSION);
+
+ curl_easy_setopt(curl_handle, CURLOPT_URL, url);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
+ curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, user_agent);
+ curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_msg);
+ curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 0L);
+
+ res = curl_easy_perform(curl_handle);
+ if (res != CURLE_OK) {
+ free(chunk.memory);
+ chunk.memory = NULL;
+ } else {
+ error_msg[0] = '\0';
+ }
+
+ curl_easy_cleanup(curl_handle);
+ return chunk.memory;
+}
+
+/* 02.01 */ long
+get_remote_content_length(const char *url)
+{
+ CURL *curl;
+ CURLcode res;
+ curl_off_t clength = -1;
+
+ curl = curl_easy_init();
+ if (!curl)
+ return -1;
+
+ char user_agent[64];
+ snprintf(user_agent, sizeof(user_agent), "hfc/%s", HFC_VERSION);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+ curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
+
+ res = curl_easy_perform(curl);
+ if (res == CURLE_OK) {
+ if (curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &clength) != CURLE_OK)
+ clength = -1;
+ }
+
+ curl_easy_cleanup(curl);
+ return (clength >= 0) ? (long)clength : -1;
+}
+
+int
+contains_valid_hosts_entry(const char *content)
+{
+ const char *line, *next_line;
+ size_t len;
+ char *cr;
+ char buffer[512];
+
+ if (!content)
+ return 0;
+
+ line = content;
+ while (*line) {
+ next_line = strchr(line, '\n');
+ len = next_line ? (size_t)(next_line - line) : strlen(line);
+
+ if (len >= sizeof(buffer))
+ len = sizeof(buffer) - 1;
+
+ if (len > 0) {
+ memcpy(buffer, line, len);
+ buffer[len] = '\0';
+
+ cr = strchr(buffer, '\r');
+ if (cr)
+ *cr = '\0';
+
+ while (*buffer == ' ' || *buffer == '\t')
+ memmove(buffer, buffer + 1, strlen(buffer));
+
+ if (strncmp(buffer, "0.0.0.0 ", 8) == 0 ||
+ strncmp(buffer, "127.0.0.1 ", 10) == 0)
+ return 1;
+ }
+
+ if (!next_line)
+ break;
+ line = next_line + 1;
+ }
+
+ return 0;
+}
+
+int
+save_to_hosts_file(const char *content, const char *url, int number)
+{
+ FILE *fp;
+ const char *line, *next_line;
+ size_t len;
+ char *cr;
+ char buffer[512], ip[16], domain[256];
+
+ if (!content)
+ return 0;
+
+ fp = fopen(HOSTS_FILE_PATH, "a");
+ if (!fp)
+ return 0;
+
+ fprintf(fp, "# %d. %s\n", number, url);
+
+ line = content;
+ while (*line) {
+ next_line = strchr(line, '\n');
+ len = next_line ? (size_t)(next_line - line) : strlen(line);
+
+ if (len >= sizeof(buffer))
+ len = sizeof(buffer) - 1;
+
+ if (len > 0) {
+ memcpy(buffer, line, len);
+ buffer[len] = '\0';
+
+ cr = strchr(buffer, '\r');
+ if (cr)
+ *cr = '\0';
+
+ while (*buffer == ' ' || *buffer == '\t')
+ memmove(buffer, buffer + 1, strlen(buffer));
+
+ if (sscanf(buffer, "%15s %255s", ip, domain) == 2) {
+ if (!strcmp(ip, "127.0.0.1") || !strcmp(ip, "0.0.0.0"))
+ fprintf(fp, "0.0.0.0 %s\n", domain);
+ }
+ }
+
+ if (!next_line)
+ break;
+ line = next_line + 1;
+ }
+
+ fclose(fp);
+ return 1;
+}
diff --git a/get.h b/get.h
@@ -0,0 +1,24 @@
+/* get.h */
+
+#ifndef GET_H
+#define GET_H
+
+#include <stddef.h>
+
+typedef struct Host {
+ char *name;
+ struct Host *next;
+} Host;
+
+typedef struct Domain {
+ char *url;
+ Host *hosts;
+} Domain;
+
+char *download_url(const char *url, char *error_msg, size_t err_size);
+long get_remote_content_length(const char *url);
+int contains_valid_hosts_entry(const char *content);
+int save_to_hosts_file(const char *content, const char *url, int number);
+
+#endif
+
diff --git a/hfc.1 b/hfc.1
@@ -0,0 +1,131 @@
+.TH HFC 1 hfc\-VERSION
+.SH NAME
+hfc \- hosts file client
+.SH SYNOPSIS
+.B hfc
+[ \fIOPTION\fR ]
+.SH DESCRIPTION
+\fBhfc\fR is a TUI/CLI program for managing entries in the linux hosts file.
+It's primarely usecase is to view entries by custom or remote (https://...) lists as well as having update indicators which one could merge.
+.P
+When adding remote hosts entries make sure to run the software with root. Then, while downloading into the hosts file, all empty lines and full-line comments are removed, whereas those after domains:
+.br
+.B 0.0.0.0 domain.tld # comment
+are preserved as they may contain useful metadata.
+.P
+To automate updates of your added hosts sources, use
+.B hfc \-U
+in your crontab or systemd timer, merging though must be done manually from the TUI.
+.SH OPTIONS
+.TP
+\fB\-h, \-\-help\fR
+Show usage information.
+.TP
+\fB\-a, \-\-add\fR \fI<url/list>\fR
+Add a remote url (https://...) or custom list.
+.TP
+\fB\-l, \-\-list\fR
+List all entries.
+.TP
+\fB\-r, \-\-remove\fR \fI<url/list>\fR
+Remove a remote url or custom list.
+.TP
+\fB\-e, \-\-edit\fR \fI<list>\fR
+Edit a custom list section using your \fB$EDITOR\fR.
+.br
+! Changes to remote urls will be overwritten
+.TP
+\fB\-U, \-\-update_all\fR
+Check all remote sources for available updates.
+.SH FILES
+.TP
+.B /etc/hosts
+The system hosts file where domain entries are stored.
+.br
+This file is read, copied and modified by the program and therefore needs root execution.
+.TP
+.B /tmp/hosts_temp, /tmp/hosts_merge_temp
+Temporary files used to store a modified version of the hosts file.
+.TP
+.B $HOME/.config/hfc/urls
+Data file for storing list names.
+.TP
+.B $HOME/.config/hfc/fetch.lock
+Lock file used for cross-session downloads.
+.TP
+.B $HOME/.config/hfc/conf
+Config file to change key bindings and color of the TUI.
+.TP
+.B $HOME/.local/state/hfc/counts
+Data file for storing entry counters, which is used to recognize and inform about updates.
+.SH CONFIGURATION
+Configuration is done in the common format seperated by spaces:
+.br
+.B <config-command> <arg1> <arg2>...
+with self-explanatory arguments.
+.br
+Actions can be bound to multiple keys.
+.TP
+.B Default
+.nf
+[keys]
+bind j down
+bind k up
+bind q quit
+bind a add
+bind r remove
+bind e edit
+bind u update_selected
+bind U update_all
+bind o order
+bind m merge
+bind L refresh
+bind + select_all
+bind - unselect_all
+bind space select
+bind ? help
+
+[ui]
+color header white
+color footer white
+color entry_highlight white
+color table_header white
+color entry_default white
+.fi
+.TP
+.B Available [keys]
+.TS
+tab(|);
+l l l 1.
+bind|<key>|<action>
+\&|key_up |down
+\&|key_down |up
+\&|key_left |quit
+\&|key_right |add
+\&|key_home |remove
+\&|key_end |edit
+\&|key_npage |update_selected
+\&|key_ppage |update_all
+\&|key_enter |order
+\&|space |merge
+\&|a–z, A–Z, 0–9 |refresh
+\&|!#$%&'()*+,-./:;<=>?@[]^_`{}~) |help
+.TE
+.TP
+.B Available [ui]
+.TS
+tab(|);
+l l l l l.
+color|<element>|<foreground>|<background>|bold
+\&|header |black |white |\&
+\&|footer |red |blue |\&
+\&|entry_highlight|gray |yellow |\&
+\&|table_header |green |cyan |\&
+\&|entry_default |magenta |black |\&
+.TE
+.TP
+The optional \fBbold\fR attribute makes the text bold.
+.SH COMMANDS DURING USE
+Press '\fB?\fP' during use to get a list of commands.
+.SH BUGS
+Send all bug reports with patch trough xmpp to chat@marlonivo.xyz.
diff --git a/hfc.c b/hfc.c
@@ -0,0 +1,2195 @@
+/* hfc.c - host file client
+*
+* headers and macros */
+#define _POSIX_C_SOURCE 200809L
+
+#include <ctype.h>
+#include <curl/curl.h>
+#include <errno.h>
+#include <locale.h>
+#include <ncurses.h>
+#include <netdb.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "get.h"
+#include "hfc.h"
+#include "update.h"
+#include <sys/types.h>
+#include <sys/file.h>
+#include <sys/wait.h>
+
+#define MAX_ENTRIES 100
+#define MAX_KEYBINDINGS 64
+#define MAX_LINE_LEN 256
+#define MAX_HEADER_LEN 256
+#define MAX_NAME_LEN 220
+
+/* functions */
+/* 01.00 */ static int check_write_hosts(bool silent);
+/* 02.00 */ static void load_entries(void);
+/* 03.00 */ static void reorder_entry(int from, int to);
+/* 04.00 */ static int fetch_lock_exists(void);
+/* 05.00 */ static int is_fetch_lock_empty(void);
+/* 06.00 */ static void create_fetch_lock(int index);
+/* 07.00 */ static void remove_index_from_lock(int index_to_remove);
+/* 08.00 */ static void start_update_in_background(void);
+/* 09.00 */ static void save_count_for_index(int index);
+/* 10.00 */ static void save_counts(void);
+/* 11.00 */ static void load_counts(void);
+/* 12.00 */ static void update_all_sources(void);
+/* 13.00 */ static void update_selected_sources(void);
+/* 14.00 */ static void update_domain_count(int index);
+/* 15.00 */ static int count_hosts_in_section(int section_index);
+/* 16.00 */ static void save_entries(void);
+/* 17.00 */ static void free_entries(void);
+/* 18.00 */ static void display_entries(int highlight);
+/* 19.00 */ static void init_footer(void);
+/* 20.00 */ static void print_footer(const char *fmt, ...);
+/* 21.00 */ static void display_help(void);
+/* 22.00 */ static void edit_entry(int index);
+/* 23.00 */ static void add_entry(void);
+/* 24.00 */ static void background_fetch_entry(int index);
+/* 25.00 */ static void remove_entry(int index);
+/* 26.00 */ static void remove_selected_entries(void);
+/* 27.00 */ static void rebuild_hostfile_headers_from_urls(void);
+/* 28.00 */ static void merge_entry(int index);
+/* 29.00 */ static void merge_selected_entries(void);
+/* 30.00 */ static void rename_entry(int index);
+/* 31.00 */ const char *get_action_for_key(int key);
+/* 32.00 */ static int has_network_connection(void);
+/* 33.00 */ int main(int argc, char *argv[]);
+
+/* variables */
+int domain_count = 0;
+int footer_busy = 0;
+int in_help_mode = 0;
+int highlight = 0;
+int updates_count = 0;
+int entry_count = 0;
+int keybinding_count = 0;
+int is_checking = 1;
+volatile int update_progress = -1;
+int selected[MAX_ENTRIES] = {0};
+int update_pipe[2];
+int updates_counts[MAX_ENTRIES];
+int domains_counts[MAX_ENTRIES];
+Domain domains[MAX_ENTRIES];
+WINDOW *footer_win = NULL;
+KeyBinding keybindings[MAX_KEYBINDINGS];
+char *entries[MAX_ENTRIES];
+char urls_path[256];
+char counts_path[256];
+char fetch_lock_path[256];
+char path_keybindings[256];
+long remote_sizes[MAX_ENTRIES];
+const char *editor = NULL;
+const char *hosts_path = "/etc/hosts";
+int is_local_entry(const char *entry) {
+ if (!entry) return 0;
+ return !(strstr(entry, "http://") || strstr(entry, "https://"));
+}
+
+/* 01.00 */ static int
+check_write_hosts(bool silent)
+{
+ if (access(hosts_path, W_OK) != 0) {
+ if (!silent) {
+ int is_tui = (stdscr != NULL || isatty(fileno(stdout)));
+
+ if (is_tui) {
+ print_footer("Error: Can't open hosts for writing: permission denied.");
+ refresh();
+ napms(1500);
+ } else {
+ fprintf(stderr, "Error: Can't open hosts for writing: permission denied.\n");
+ }
+ }
+ return 0;
+ }
+ return 1;
+}
+
+/* 02.00 */ static void
+load_entries(void)
+{
+ FILE *fp;
+ char line[MAX_LINE_LEN];
+ char *url_start;
+
+ /* entries from urls_path */
+ fp = fopen(urls_path, "r");
+ if (fp) {
+ entry_count = 0;
+ while (fgets(line, sizeof(line), fp) && entry_count < MAX_ENTRIES) {
+ line[strcspn(line, "\n")] = '\0';
+ url_start = line;
+ entries[entry_count] = strdup(url_start);
+ domains[entry_count].url = strdup(entries[entry_count]);
+ domains[entry_count].hosts = NULL;
+ entry_count++;
+ }
+ fclose(fp);
+ }
+
+ /* temporary entries from fetch.lock (not yet in urls_path!) */
+ if (fetch_lock_exists()) {
+ FILE *lock_fp = fopen(fetch_lock_path, "r");
+ if (lock_fp) {
+ while (fgets(line, sizeof(line), lock_fp) && entry_count < MAX_ENTRIES) {
+ line[strcspn(line, "\n")] = '\0';
+
+ /* check: already in entries[]? */
+ int exists = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (entries[i] && strcmp(entries[i], line) == 0) {
+ exists = 1;
+ break;
+ }
+ }
+
+ if (!exists) {
+ entries[entry_count] = strdup(line);
+ domains[entry_count].url = strdup(entries[entry_count]);
+ domains[entry_count].hosts = NULL;
+ domains_counts[entry_count] = -1;
+ updates_counts[entry_count] = -1;
+ remote_sizes[entry_count] = 0;
+ entry_count++;
+ }
+ }
+ fclose(lock_fp);
+ }
+ }
+}
+
+/* 03.00 */ static void
+reorder_entry(int from, int to)
+{
+ if (from == to || from < 0 || to < 0 || from >= entry_count || to >= entry_count)
+ return;
+
+ if (!check_write_hosts(true)) {
+ return;
+ }
+
+ /* read hosts file into blocks */
+ FILE *fp = fopen(hosts_path, "r");
+ if (!fp) return;
+
+ char **blocks = calloc(entry_count, sizeof(char *));
+ size_t *block_sizes = calloc(entry_count, sizeof(size_t));
+ int current = -1;
+ size_t bufsize = 0;
+ char *block_buf = NULL;
+ char line[MAX_LINE_LEN];
+
+ while (fgets(line, sizeof(line), fp)) {
+ if (strncmp(line, "# ", 2) == 0) {
+ if (current >= 0) {
+ blocks[current] = block_buf;
+ block_sizes[current] = bufsize;
+ }
+ current++;
+ block_buf = NULL;
+ bufsize = 0;
+ }
+ size_t len = strlen(line);
+ block_buf = realloc(block_buf, bufsize + len + 1);
+ memcpy(block_buf + bufsize, line, len);
+ bufsize += len;
+ block_buf[bufsize] = '\0';
+ }
+ if (current >= 0) {
+ blocks[current] = block_buf;
+ block_sizes[current] = bufsize;
+ }
+ fclose(fp);
+
+ /* back up metadata */
+ char *entry_tmp = entries[from];
+ int domain_tmp = domains_counts[from];
+ int update_tmp = updates_counts[from];
+ long size_tmp = remote_sizes[from];
+ int sel_tmp = selected[from];
+ Domain domain_struct_tmp = domains[from];
+ char *block_tmp = blocks[from];
+ size_t block_size_tmp = block_sizes[from];
+
+ /* move arrays */
+ if (from < to) {
+ for (int i = from; i < to; i++) {
+ entries[i] = entries[i + 1];
+ domains_counts[i] = domains_counts[i + 1];
+ updates_counts[i] = updates_counts[i + 1];
+ remote_sizes[i] = remote_sizes[i + 1];
+ selected[i] = selected[i + 1];
+ domains[i] = domains[i + 1];
+ blocks[i] = blocks[i + 1];
+ block_sizes[i] = block_sizes[i + 1];
+ }
+ } else {
+ for (int i = from; i > to; i--) {
+ entries[i] = entries[i - 1];
+ domains_counts[i] = domains_counts[i - 1];
+ updates_counts[i] = updates_counts[i - 1];
+ remote_sizes[i] = remote_sizes[i - 1];
+ selected[i] = selected[i - 1];
+ domains[i] = domains[i - 1];
+ blocks[i] = blocks[i - 1];
+ block_sizes[i] = block_sizes[i - 1];
+ }
+ }
+
+ /* use secured position */
+ entries[to] = entry_tmp;
+ domains_counts[to] = domain_tmp;
+ updates_counts[to] = update_tmp;
+ remote_sizes[to] = size_tmp;
+ selected[to] = sel_tmp;
+ domains[to] = domain_struct_tmp;
+ blocks[to] = block_tmp;
+ block_sizes[to] = block_size_tmp;
+
+ /* rewrite hosts file with new numbers */
+ FILE *out = fopen(hosts_path, "w");
+ if (out) {
+ for (int i = 0; i < entry_count; i++) {
+ fprintf(out, "# %d. %s\n", i + 1, entries[i]);
+ char *block_content = strstr(blocks[i], "\n");
+ if (block_content) {
+ fputs(block_content + 1, out);
+ }
+ }
+ fclose(out);
+ }
+
+ for (int i = 0; i < entry_count; i++) {
+ free(blocks[i]);
+ }
+ free(blocks);
+ free(block_sizes);
+
+ save_entries();
+ save_counts();
+}
+
+/* 04.00 */ static int
+fetch_lock_exists(void)
+{
+ FILE *fp = fopen(fetch_lock_path, "r");
+ if (fp) {
+ fclose(fp);
+ return 1;
+ }
+ return 0;
+}
+
+/* 05.00 */ static int
+is_fetch_lock_empty(void)
+{
+ FILE *fp = fopen(fetch_lock_path, "r");
+ if (!fp) return 1;
+
+ int dummy;
+ int result = (fscanf(fp, "%d", &dummy) != 1);
+ fclose(fp);
+ return result;
+}
+
+/* 06.00 */ static void
+create_fetch_lock(int index)
+{
+ if (fetch_lock_path[0] == '\0')
+ return;
+
+ /* check if index is already in lock */
+ FILE *check = fopen(fetch_lock_path, "r");
+ if (check) {
+ char line[1024];
+ while (fgets(line, sizeof(line), check)) {
+ line[strcspn(line, "\n")] = '\0';
+ if (strcmp(line, entries[index]) == 0) {
+ fclose(check);
+ return;
+ }
+ }
+ fclose(check);
+ }
+
+ /* if lock does not exist – add */
+ FILE *fp = fopen(fetch_lock_path, "a");
+ if (!fp) {
+ perror("fopen fetch_lock");
+ return;
+ }
+
+ flock(fileno(fp), LOCK_EX);
+ fprintf(fp, "%s\n", entries[index]);
+ fflush(fp);
+ flock(fileno(fp), LOCK_UN);
+ fclose(fp);
+}
+
+/* 07.00 */ static void
+remove_index_from_lock(int index_to_remove)
+{
+ if (!entries[index_to_remove]) return;
+
+ int fd = open(fetch_lock_path, O_RDWR);
+ if (fd == -1) return;
+
+ flock(fd, LOCK_EX);
+
+ FILE *in = fdopen(fd, "r");
+ if (!in) {
+ close(fd);
+ return;
+ }
+
+ FILE *out = fopen("/tmp/fetch_lock_temp", "w");
+ if (!out) {
+ fclose(in);
+ return;
+ }
+
+ char line[1024];
+ int is_empty = 1;
+
+ while (fgets(line, sizeof(line), in)) {
+ line[strcspn(line, "\n")] = '\0';
+
+ if (strcmp(line, entries[index_to_remove]) != 0) {
+ fprintf(out, "%s\n", line);
+ is_empty = 0;
+ }
+ }
+
+ fclose(in);
+ fclose(out);
+ rename("/tmp/fetch_lock_temp", fetch_lock_path);
+
+ if (is_empty) {
+ remove(fetch_lock_path);
+ }
+}
+
+/* 08.00 */ static void
+start_update_in_background()
+{
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ perror("fork");
+ return;
+ }
+
+ if (pid == 0) {
+ /* check permission */
+ if (geteuid() != 0 || !check_write_hosts(true)) {
+ _exit(0);
+ }
+ /* handle updates */
+ for (int idx = 0; idx < entry_count; idx++) {
+ if (is_local_entry(entries[idx]))
+ continue;
+
+ long new_size = get_remote_content_length(entries[idx]);
+
+ if (new_size <= 0 || new_size == remote_sizes[idx]) {
+ /* no change in remote content */
+ if (update_pipe[1] != -1) {
+ int dummy = -2;
+ write(update_pipe[1], &dummy, sizeof(int));
+ }
+ continue;
+ }
+
+ pid_t subpid = fork();
+
+ if (subpid == 0) {
+ /* only update updates_counts skip domains_counts */
+ char error_msg[CURL_ERROR_SIZE] = {0};
+ char *content = download_url(entries[idx], error_msg, sizeof(error_msg));
+
+ if (content && contains_valid_hosts_entry(content)) {
+ updates_counts[idx] = count_hosts_in_content(content);
+ remote_sizes[idx] = new_size;
+ save_counts();
+ }
+
+ if (content)
+ free(content);
+
+ if (update_pipe[1] != -1) {
+ write(update_pipe[1], &idx, sizeof(int));
+ }
+ _exit(0);
+ }
+ }
+
+ /* wait for child processes */
+ while (wait(NULL) > 0);
+
+ /* signal to tui: all updates completed */
+ if (update_pipe[1] != -1) {
+ int end = -1;
+ write(update_pipe[1], &end, sizeof(int));
+ close(update_pipe[1]);
+ }
+ _exit(0);
+ }
+}
+
+/* 09.00 */ static void
+save_count_for_index(int index)
+{
+ if (index < 0 || index >= entry_count) return;
+
+ char lines[MAX_ENTRIES][64] = {0};
+ int existing_lines = 0;
+
+ FILE *in = fopen(counts_path, "r");
+ if (in) {
+ while (fgets(lines[existing_lines], sizeof(lines[0]), in) && existing_lines < MAX_ENTRIES) {
+ lines[existing_lines][strcspn(lines[existing_lines], "\n")] = '\0';
+ existing_lines++;
+ }
+ fclose(in);
+ }
+
+ if (index >= existing_lines) {
+ for (int i = existing_lines; i <= index; i++) {
+ strcpy(lines[i], ""); /* blank line as placeholder */
+ }
+ }
+
+ snprintf(lines[index], sizeof(lines[index]), "%d %ld", updates_counts[index], remote_sizes[index]);
+
+ FILE *out = fopen(counts_path, "w");
+ if (!out) return;
+
+ for (int i = 0; i <= index; i++) {
+ if (strlen(lines[i]) == 0) continue;
+ fprintf(out, "%s\n", lines[i]);
+ }
+
+ fclose(out);
+}
+
+/* 10.00 */ static void
+save_counts(void)
+{
+ FILE *f;
+ int i;
+
+ f = fopen(counts_path, "w");
+ if (!f)
+ return;
+
+ for (i = 0; i < entry_count; i++)
+ fprintf(f, "%d %ld\n", updates_counts[i], remote_sizes[i]);
+
+ fclose(f);
+}
+
+/* 11.00 */ static void
+load_counts(void)
+{
+ FILE *fp = fopen(counts_path, "r");
+ if (!fp)
+ return;
+
+ for (int i = 0; i < entry_count; i++) {
+ long updates = 0;
+ long remote_size = 0;
+ if (fscanf(fp, "%ld %ld", &updates, &remote_size) != 2)
+ break;
+
+ if (is_local_entry(entries[i])) {
+ domains_counts[i] = count_hosts_in_section(i);
+ updates_counts[i] = 0;
+ remote_sizes[i] = 0;
+ } else if (updates <= 0) {
+ domains_counts[i] = -1;
+ updates_counts[i] = -1;
+ remote_sizes[i] = 0;
+ } else {
+ domains_counts[i] = updates;
+ updates_counts[i] = updates;
+ remote_sizes[i] = remote_size;
+ }
+ }
+
+ fclose(fp);
+
+ /* fetch lock control */
+ if (fetch_lock_exists()) {
+ FILE *fp = fopen(fetch_lock_path, "r");
+ if (fp) {
+ char line[1024];
+ while (fgets(line, sizeof(line), fp)) {
+ line[strcspn(line, "\n")] = '\0';
+
+ for (int i = 0; i < entry_count; i++) {
+ if (entries[i] && strcmp(entries[i], line) == 0) {
+ if (domains_counts[i] <= 0 || updates_counts[i] <= 0) {
+ domains_counts[i] = -1;
+ updates_counts[i] = -1;
+ }
+ break;
+ }
+ }
+ }
+ fclose(fp);
+ }
+ }
+}
+
+/* 12.00 */ static void
+update_all_sources(void)
+{
+ int i, total = 0, current = 0;
+
+ if (!has_network_connection()) {
+ print_footer("Error: No network connection.");
+ napms(2000);
+ return;
+ }
+
+ if (!check_write_hosts(true)) {
+ return;
+ }
+
+ /* skips personal lists */
+ for (i = 0; i < entry_count; i++) {
+ if (!is_local_entry(entries[i])) {
+ total++;
+ }
+ }
+
+ for (i = 0; i < entry_count; i++) {
+ if (is_local_entry(entries[i]))
+ continue;
+
+ if (domains_counts[i] == -1 || updates_counts[i] == -1)
+ continue; /* skip lists in fetch.lock */
+
+ current++;
+ update_progress = i;
+ print_footer("(%d/%d) loading %s", current, total, entries[i]);
+ update_domain_count(i);
+ }
+
+ update_progress = -1;
+ save_counts();
+ display_entries(highlight);
+}
+
+/* 13.00 */ static void
+update_selected_sources(void)
+{
+ int i, total = 0, current = 0;
+
+ if (!has_network_connection()) {
+ print_footer("Error: No network connection.");
+ napms(2000);
+ return;
+ }
+
+ for (i = 0; i < entry_count; i++) {
+ if (selected[i] && !is_local_entry(entries[i]))
+ total++;
+ }
+
+ for (i = 0; i < entry_count; i++) {
+ if (!selected[i] || is_local_entry(entries[i]))
+ continue;
+
+ current++;
+ update_progress = i;
+ print_footer("(%d/%d) updating %s", current, total, entries[i]);
+ update_domain_count(i);
+ }
+
+ update_progress = -1;
+ save_counts();
+ display_entries(highlight);
+}
+
+
+/* 14.00 */ static void
+update_domain_count(int index)
+{
+ long new_size;
+ char error_msg[256];
+ char *content;
+
+ if (index < 0 || index >= entry_count || !entries[index]) {
+ updates_counts[index] = 0;
+ return;
+ }
+
+ new_size = get_remote_content_length(entries[index]);
+ if (new_size <= 0)
+ return;
+
+ if (new_size != remote_sizes[index]) {
+ content = download_url(entries[index], error_msg, sizeof(error_msg));
+ if (!content)
+ return;
+
+ updates_counts[index] = count_hosts_in_content(content);
+ remote_sizes[index] = new_size;
+
+ free(content);
+ save_counts();
+ }
+}
+
+/* 15.00 */ static int
+count_hosts_in_section(int section_index)
+{
+ FILE *fp;
+ char line[512];
+ char section_header[256];
+ int current_section = -1;
+ int count = 0;
+
+ fp = fopen(hosts_path, "r");
+ if (!fp)
+ return 0;
+
+ snprintf(section_header, sizeof(section_header), "# %d.", section_index + 1);
+
+ while (fgets(line, sizeof(line), fp)) {
+ if (strncmp(line, "# ", 2) == 0) {
+ if (strncmp(line, section_header, strlen(section_header)) == 0) {
+ current_section = section_index;
+ count = 0;
+ } else if (current_section == section_index) {
+ break;
+ } else {
+ current_section = -1;
+ }
+ } else if (current_section == section_index) {
+ if (strncmp(line, "0.0.0.0 ", 8) == 0)
+ count++;
+ }
+ }
+
+ fclose(fp);
+ return count;
+}
+
+/* 16.00 */ static void
+save_entries(void)
+{
+ FILE *fp = fopen(urls_path, "w");
+ if (!fp)
+ return;
+
+ for (int i = 0; i < entry_count; i++) {
+ if (entries[i] && strlen(entries[i]) > 0) {
+ fprintf(fp, "%s\n", entries[i]);
+ }
+ }
+
+ fclose(fp);
+}
+
+/* 17.00 */ static void
+free_entries(void)
+{
+ int i;
+
+ for (i = 0; i < entry_count; i++)
+ free(entries[i]);
+}
+
+/* 18.00 */ static void
+display_entries(int highlight)
+{
+ int i, line, y, cols, url_len, lines_needed;
+ int local_source_width = 13;
+
+ int id_width = 5;
+ int indent = 7;
+ int local_source_start;
+ int max_url_width;
+ char *text, buf[32];
+
+ clear();
+ cols = getmaxx(stdscr);
+ local_source_start = cols - local_source_width - 4;
+
+ int selected_count = 0;
+ for (int si = 0; si < entry_count; si++) {
+ if (selected[si])
+ selected_count++;
+ }
+ int right_col = cols - 10;
+
+ /* header */
+ {
+ short fg_code = (*config.header.fg) ? get_color_code(config.header.fg) : -1;
+ if (fg_code != -1)
+ apply_color(&config.header, 1);
+ else
+ attron(COLOR_PAIR(0));
+
+ mvhline(0, 0, ' ', cols);
+ mvprintw(0, 0, "hfc %s |", HFC_VERSION);
+ mvprintw(0, 12, "%-1s:%-3s", get_keys_for_action("quit"), "quit");
+ mvprintw(0, 20, "%-1s:%-3s", get_keys_for_action("add"), "add");
+ mvprintw(0, 27, "%-1s:%-3s", get_keys_for_action("remove"), "remove");
+ mvprintw(0, 37, "%-1s:%-3s", get_keys_for_action("merge"), "merge");
+ mvprintw(0, 46, "%-1s:%-3s", get_keys_for_action("edit"), "edit");
+ mvprintw(0, 54, "%-1s:%-3s", get_keys_for_action("help"), "help");
+
+ if (selected_count > 0) {
+ mvprintw(0, right_col, "*%d | %d/%d",
+ selected_count,
+ (entry_count == 0) ? 0 : highlight + 1,
+ entry_count);
+ } else {
+ mvprintw(0, right_col, " | %d/%d",
+ (entry_count == 0) ? 0 : highlight + 1,
+ entry_count);
+ }
+
+ if (fg_code != -1)
+ remove_color(&config.header, 1);
+ else
+ attroff(COLOR_PAIR(0));
+
+ /* line unter dem header */
+ short line_code = get_color_code(config.header.bg);
+ if (line_code != -1) {
+ init_pair(10, line_code, -1);
+ attron(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0));
+ mvhline(1, 0, ACS_HLINE, cols);
+ attroff(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0));
+ } else {
+ mvhline(1, 0, ACS_HLINE, cols);
+ }
+ }
+
+ /* table header */
+ if (*config.table_header.fg) {
+ short fg_code = get_color_code(config.table_header.fg);
+ if (fg_code != -1) {
+ init_pair(21, fg_code, -1);
+ attron(COLOR_PAIR(21) | (config.table_header.bold ? A_BOLD : 0));
+ }
+ } else {
+ attron(A_BOLD);
+ }
+
+ mvprintw(2, 2, "ID");
+ mvprintw(2, indent, "List");
+ mvprintw(2, local_source_start, "Local/Source");
+
+ if (*config.table_header.fg) {
+ short fg_code = get_color_code(config.table_header.fg);
+ if (fg_code != -1) {
+ attroff(COLOR_PAIR(21) | (config.table_header.bold ? A_BOLD : 0));
+ }
+ } else {
+ attroff(A_BOLD);
+ }
+
+ /* entries */
+ if (entry_count > 0) {
+ max_url_width = local_source_start - indent - 6;
+ y = 3;
+
+ for (i = 0; i < entry_count; i++) {
+ text = entries[i];
+ url_len = strlen(text);
+ lines_needed = (url_len + max_url_width - 1) / max_url_width;
+ int is_highlight = (i == highlight);
+
+ for (line = 0; line < lines_needed; line++) {
+ if (is_highlight) {
+ short fg_code = get_color_code(config.entry_highlight.fg);
+ short bg_code = get_color_code(config.entry_highlight.bg);
+
+ if (fg_code != -1 || bg_code != -1) {
+ int attrs = COLOR_PAIR(4);
+ if (config.entry_highlight.bold)
+ attrs |= A_BOLD;
+ attron(attrs);
+ mvhline(y, 0, ' ', cols);
+ } else {
+ attron(A_REVERSE);
+ mvhline(y, 0, ' ', cols);
+ }
+ }
+
+ if (!is_highlight && *config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ short bg_code = get_color_code(config.entry_default.bg);
+ if (fg_code != -1 || bg_code != -1) {
+ init_pair(20,
+ (fg_code != -1 ? fg_code : -1),
+ (bg_code != -1 ? bg_code : -1));
+ attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+
+ if (line == 0) {
+ mvprintw(y, 1, "%c", selected[i] ? '*' : ' ');
+ mvprintw(y, 2, "%-*d", id_width, i + 1);
+ mvprintw(y, indent, "%.*s", max_url_width, text + line * max_url_width);
+
+ if (domains_counts[i] == -1 || updates_counts[i] == -1) {
+ mvprintw(y, local_source_start, "(...)");
+ } else if (is_local_entry(entries[i])) {
+ snprintf(buf, sizeof(buf), "(%d)", domains_counts[i]);
+ mvprintw(y, local_source_start, "%s", buf);
+ } else {
+ snprintf(buf, sizeof(buf), "(%d/", domains_counts[i]);
+ mvprintw(y, local_source_start, "%s", buf);
+ mvprintw(y, local_source_start + strlen(buf), "%d)", updates_counts[i]);
+ }
+ } else {
+ mvprintw(y, 1, " ");
+ mvprintw(y, 2, " ");
+ mvprintw(y, indent, "%.*s", max_url_width, text + line * max_url_width);
+ }
+
+ if (!is_highlight && *config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ short bg_code = get_color_code(config.entry_default.bg);
+ if (fg_code != -1 || bg_code != -1) {
+ attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+
+ if (is_highlight) {
+ short fg_code = get_color_code(config.entry_highlight.fg);
+ short bg_code = get_color_code(config.entry_highlight.bg);
+
+ if (fg_code != -1 || bg_code != -1) {
+ int attrs = COLOR_PAIR(4);
+ if (config.entry_highlight.bold)
+ attrs |= A_BOLD;
+ attroff(attrs);
+ } else {
+ attroff(A_REVERSE);
+ }
+ }
+ y++;
+ }
+ }
+ }
+
+ short footer_line_fg = get_color_code(config.footer.bg);
+ if (footer_line_fg != -1) {
+ init_pair(12, footer_line_fg, -1);
+ attron(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0));
+ mvhline(LINES - 2, 0, ACS_HLINE, cols);
+ attroff(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0));
+ } else {
+ mvhline(LINES - 2, 0, ACS_HLINE, cols);
+ }
+}
+
+/* 19.00 */ static void
+init_footer()
+{
+ if (footer_win) {
+ delwin(footer_win);
+ }
+ int cols = getmaxx(stdscr);
+ footer_win = newwin(1, cols, LINES - 1, 0);
+}
+
+
+/* 20.00 */ static void
+print_footer(const char *fmt, ...)
+{
+ int cols = getmaxx(stdscr);
+
+ if (*config.footer.fg) {
+ apply_color(&config.footer, 2);
+ }
+
+ mvhline(LINES - 1, 0, ' ', cols);
+
+ char buf[512];
+ va_list args;
+ va_start(args, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, args);
+ va_end(args);
+
+ mvprintw(LINES - 1, 0, "%s", buf);
+
+ if (*config.footer.fg) {
+ remove_color(&config.footer, 2);
+ }
+
+ refresh();
+}
+
+/* 21.00 */ static void
+display_help(void)
+{
+ typedef struct {
+ const char *action;
+ const char *description;
+ } HelpEntry;
+
+ static HelpEntry help_entries[] = {
+ { "help", "help" },
+ { "quit", "quit" },
+ { "refresh", "refresh screen" },
+ { NULL, NULL },
+ { "down", "down" },
+ { "up", "up" },
+ { "edit", "edit ($EDITOR)" },
+ { "add", "add item" },
+ { "remove", "remove selected" },
+ { "merge", "merge selected" },
+ { NULL, NULL },
+ { "select", "select" },
+ { "unselect_all", "unselect all" },
+ { "select_all", "select all" },
+ { NULL, NULL },
+ { "rename", "rename" },
+ { "update", "update selected" },
+ { "update_all", "update all" },
+ { "order", "change order" }
+ };
+
+ static const char *fallback_help[] = {
+ "? - help",
+ "q - quit",
+ "L - refresh screen",
+ "",
+ "arrows / j,k - scroll",
+ "e - edit ($EDITOR)",
+ "a - add item",
+ "r - remove selected",
+ "m - merge selected",
+ "",
+ "space - select",
+ "- - unselect all",
+ "+ - select all",
+ "",
+ "R - rename",
+ "u - update selected",
+ "U - update all",
+ "o - change order",
+ };
+
+ int lines = sizeof(help_entries) / sizeof(help_entries[0]);
+ int fallback_lines = sizeof(fallback_help) / sizeof(fallback_help[0]);
+
+ clear();
+ int cols = getmaxx(stdscr);
+
+ /* header like in display_entries() */
+ if (*config.header.fg) {
+ apply_color(&config.header, 1);
+ mvhline(0, 0, ' ', cols);
+ mvprintw(0, 0, "hfc %s | help", HFC_VERSION);
+ remove_color(&config.header, 1);
+ } else {
+ mvhline(0, 0, ' ', cols);
+ if (config.header.bold) attron(A_BOLD);
+ mvprintw(0, 0, "hfc %s | help", HFC_VERSION);
+ if (config.header.bold) attroff(A_BOLD);
+ }
+
+ /* line below header */
+ short fg_code = get_color_code(config.header.bg);
+ if (fg_code != -1) {
+ init_pair(10, fg_code, -1);
+ attron(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0));
+ mvhline(1, 0, ACS_HLINE, cols);
+ attroff(COLOR_PAIR(10) | (config.header.bold ? A_BOLD : 0));
+ } else {
+ mvhline(1, 0, ACS_HLINE, cols);
+ }
+
+ /* check if bindings present */
+ int any_binding_found = 0;
+ for (int i = 0; i < lines; i++) {
+ if (help_entries[i].action &&
+ strcmp(get_keys_for_action(help_entries[i].action), "?") != 0) {
+ any_binding_found = 1;
+ break;
+ }
+ }
+
+ int row = 3;
+ if (any_binding_found) {
+ for (int i = 0; i < lines; i++) {
+ if (help_entries[i].action == NULL) {
+ row++;
+ continue;
+ }
+ const char *keys = get_keys_for_action(help_entries[i].action);
+
+ /* only font color like entry_default, without background */
+ if (*config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ if (fg_code != -1) {
+ init_pair(20, fg_code, -1);
+ attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+
+ mvprintw(row++, 2, "%-18s - %s", keys, help_entries[i].description);
+
+ if (*config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ if (fg_code != -1) {
+ attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+ }
+ } else {
+ for (int i = 0; i < fallback_lines; i++) {
+ if (strcmp(fallback_help[i], "") == 0) {
+ row++;
+ continue;
+ }
+ if (*config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ if (fg_code != -1) {
+ init_pair(20, fg_code, -1);
+ attron(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+ mvprintw(row++, 2, "%s", fallback_help[i]);
+ if (*config.entry_default.fg) {
+ short fg_code = get_color_code(config.entry_default.fg);
+ if (fg_code != -1) {
+ attroff(COLOR_PAIR(20) | (config.entry_default.bold ? A_BOLD : 0));
+ }
+ }
+ }
+ }
+
+ /* footer line as in display_entries() */
+ short footer_line_fg = get_color_code(config.footer.bg);
+ if (footer_line_fg != -1) {
+ init_pair(12, footer_line_fg, -1);
+ attron(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0));
+ mvhline(LINES - 2, 0, ACS_HLINE, cols);
+ attroff(COLOR_PAIR(12) | (config.footer.bold ? A_BOLD : 0));
+ } else {
+ mvhline(LINES - 2, 0, ACS_HLINE, cols);
+ }
+
+ /* footer text as normal in footer color */
+ print_footer("Press any key to continue...");
+ refresh();
+}
+
+/* 22.00 */ static void
+edit_entry(int index)
+{
+ const char *editor = getenv("EDITOR");
+ FILE *fp;
+ char line[1024];
+ char search_tag[64];
+ char cmd[1024];
+ int line_num, target_line, ret;
+
+ if (!editor) {
+ const char *env = getenv("EDITOR");
+ editor = (env && *env) ? env : "vim";
+ }
+
+ if (index < 0 || index >= entry_count)
+ return;
+
+ fp = fopen(hosts_path, "r");
+ if (!fp) {
+ mvprintw(LINES - 1, 2, "Error: Couldn't open hosts.");
+ refresh();
+ napms(1500);
+ return;
+ }
+
+ line_num = 0;
+ target_line = -1;
+
+ snprintf(search_tag, sizeof(search_tag), "# %d. %s", index + 1, entries[index]);
+
+ while (fgets(line, sizeof(line), fp)) {
+ line_num++;
+ if (strstr(line, search_tag)) {
+ target_line = line_num;
+ break;
+ }
+ }
+ fclose(fp);
+
+ if (target_line == -1) {
+ mvprintw(LINES - 1, 2, "No entry found.");
+ refresh();
+ napms(1500);
+ return;
+ }
+ is_checking = 0;
+
+ snprintf(cmd, sizeof(cmd), "%s +%d %s", editor, target_line, hosts_path);
+
+ endwin();
+
+ ret = system(cmd);
+
+ refresh();
+ clear();
+
+ if (ret == 0) {
+ /* full recount for all sections */
+ for (int i = 0; i < entry_count; i++) {
+ domains_counts[i] = count_hosts_in_section(i);
+ updates_counts[i] = domains_counts[i];
+ }
+ save_counts();
+ }
+}
+
+/* 23.00 */ static void
+add_entry(void)
+{
+ char input[MAX_LINE_LEN] = {0};
+ int pos = 0, ch, next;
+
+ echo();
+ curs_set(1);
+
+ const char *prompt = "Url/List: ";
+ print_footer("%s", prompt);
+
+ int input_start_x = (int)strlen(prompt);
+ apply_color(&config.footer, 2);
+ mvhline(LINES - 1, input_start_x, ' ', MAX_LINE_LEN - 1);
+ move(LINES - 1, input_start_x);
+
+ while ((ch = getch()) != '\n' && ch != ERR) {
+ if (ch == 27) {
+ nodelay(stdscr, 1);
+ next = getch();
+ nodelay(stdscr, 0);
+ if (next == ERR) goto cancel;
+ else ungetch(next);
+ } else if (ch == KEY_BACKSPACE || ch == 127) {
+ if (pos > 0) {
+ pos--;
+ input[pos] = '\0';
+ mvaddch(LINES - 1, input_start_x + pos, ' ');
+ }
+ move(LINES - 1, input_start_x + pos);
+ } else if (pos < MAX_LINE_LEN - 1 && isprint(ch)) {
+ input[pos++] = ch;
+ input[pos] = '\0';
+ mvaddch(LINES - 1, input_start_x + pos - 1, ch);
+ move(LINES - 1, input_start_x + pos);
+ }
+ refresh();
+ }
+
+ remove_color(&config.footer, 2);
+
+ if (pos == 0)
+ goto cancel;
+
+ noecho();
+ curs_set(0);
+
+ /* add metadata entry */
+ int index = entry_count;
+
+ if (!strchr(input, '.')) {
+ /* local list → check write access */
+ if (access(hosts_path, W_OK) != 0) {
+ print_footer("Error: Can't open hosts for writing: permission denied.");
+ refresh();
+ napms(1500);
+ goto cancel;
+ }
+ if (!save_to_hosts_file("# (manually added entry)\n", input, index + 1)) {
+ print_footer("Error: Could not write to hosts file.");
+ refresh();
+ napms(1500);
+ goto cancel;
+ }
+
+ entries[index] = strdup(input);
+ domains_counts[index] = count_hosts_in_section(index);
+ updates_counts[index] = 0;
+ remote_sizes[index] = 0;
+ entry_count++;
+ save_entries();
+ save_counts();
+ } else {
+ /* remote URL → check write access */
+ if (!check_write_hosts(true)) {
+ refresh();
+ napms(1500);
+ goto cancel;
+ }
+
+ /* add metadata entry */
+ entries[index] = strdup(input);
+ domains_counts[index] = -1;
+ updates_counts[index] = -1;
+ remote_sizes[index] = 0;
+ entry_count++;
+
+ background_fetch_entry(index);
+ }
+
+ display_entries(index);
+ highlight = index;
+ move(LINES - 1, 0);
+ clrtoeol();
+ refresh();
+ return;
+
+cancel:
+ remove_color(&config.footer, 2);
+ noecho();
+ curs_set(0);
+ move(LINES - 1, 0);
+ clrtoeol();
+ refresh();
+ display_entries(highlight);
+ return;
+}
+
+/* 24.00 */ static void
+background_fetch_entry(int index)
+{
+ pid_t pid = fork();
+ if (pid == 0) {
+ setsid();
+ signal(SIGHUP, SIG_IGN);
+ signal(SIGPIPE, SIG_IGN);
+
+ fclose(stdin);
+ fclose(stdout);
+ fclose(stderr);
+
+ create_fetch_lock(index);
+
+ char error_msg[CURL_ERROR_SIZE] = {0};
+ char *content = download_url(entries[index], error_msg, sizeof(error_msg));
+
+ if (!content || !contains_valid_hosts_entry(content)) {
+ if (content) free(content);
+ remove_index_from_lock(index);
+ _exit(1);
+ }
+
+ if (!check_write_hosts(true)) {
+ free(content);
+ remove_index_from_lock(index);
+ _exit(1);
+ }
+
+ /* count + save */
+ int count = count_hosts_in_content(content);
+ domains_counts[index] = count;
+ updates_counts[index] = count;
+ remote_sizes[index] = get_remote_content_length(entries[index]);
+
+ /* now write to hosts + urls */
+ save_to_hosts_file(content, entries[index], index + 1);
+ save_entries();
+ save_count_for_index(index);
+
+ free(content);
+ remove_index_from_lock(index);
+
+ if (update_pipe[1] != -1) {
+ int idx = index;
+ write(update_pipe[1], &idx, sizeof(int));
+ }
+
+ _exit(0);
+ }
+}
+
+/* 25.00 */ static void
+remove_entry(int index)
+{
+ if (index < 0 || index >= entry_count || !entries[index])
+ return;
+
+ if (!check_write_hosts(true))
+ return;
+
+ const char *target_url = entries[index];
+ FILE *in = fopen(hosts_path, "r");
+ FILE *out = fopen("/tmp/hosts_temp", "w");
+
+ if (!in || !out) {
+ if (in) fclose(in);
+ if (out) fclose(out);
+ return;
+ }
+
+ char line[MAX_LINE_LEN];
+ int skip_block = 0;
+
+ while (fgets(line, sizeof(line), in)) {
+ if (strncmp(line, "# ", 2) == 0) {
+ /* is that a block with the url? */
+ if (strstr(line, target_url)) {
+ skip_block = 1;
+ continue;
+ } else {
+ skip_block = 0;
+ }
+ }
+
+ if (!skip_block)
+ fputs(line, out);
+ }
+
+ fclose(in);
+ fclose(out);
+
+ rename("/tmp/hosts_temp", hosts_path);
+
+ /* move array entries */
+ free(entries[index]);
+ for (int i = index; i < entry_count - 1; i++) {
+ entries[i] = entries[i + 1];
+ updates_counts[i] = updates_counts[i + 1];
+ domains_counts[i] = domains_counts[i + 1];
+ remote_sizes[i] = remote_sizes[i + 1];
+ selected[i] = selected[i + 1];
+ }
+ entries[entry_count - 1] = NULL;
+ updates_counts[entry_count - 1] = 0;
+ domains_counts[entry_count - 1] = 0;
+ remote_sizes[entry_count - 1] = 0;
+ selected[entry_count - 1] = 0;
+
+ entry_count--;
+
+ save_entries();
+ save_counts();
+ rebuild_hostfile_headers_from_urls();
+}
+
+/* 26.00 */ static void
+remove_selected_entries(void)
+{
+ for (int i = entry_count - 1; i >= 0; i--) {
+ if (selected[i]) {
+ remove_entry(i);
+ }
+ }
+
+ /* reset selection */
+ for (int i = 0; i < MAX_ENTRIES; i++)
+ selected[i] = 0;
+}
+
+/* 27.00 */ static void
+rebuild_hostfile_headers_from_urls(void)
+{
+ FILE *in = fopen(hosts_path, "r");
+ FILE *out = fopen("/tmp/hosts_reindexed", "w");
+ char line[MAX_LINE_LEN];
+ int entry_idx = 0;
+ int inside_block = 0;
+
+ if (!in || !out) {
+ if (in) fclose(in);
+ if (out) fclose(out);
+ return;
+ }
+
+ while (fgets(line, sizeof(line), in)) {
+ if (strncmp(line, "# ", 2) == 0) {
+ if (entry_idx < entry_count) {
+ fprintf(out, "# %d. %s\n", entry_idx + 1, entries[entry_idx]);
+ inside_block = 1;
+ entry_idx++;
+ } else {
+ inside_block = 0;
+ }
+ continue;
+ }
+
+ if (inside_block || line[0] == '\n') {
+ fputs(line, out);
+ }
+ }
+
+ fclose(in);
+ fclose(out);
+ rename("/tmp/hosts_reindexed", hosts_path);
+}
+
+/* 28.00 */ static void
+merge_entry(int index)
+{
+ FILE *in, *out;
+ const char *url, *p, *end;
+ char error_msg[CURL_ERROR_SIZE] = {0};
+ char *new_content;
+ char line[MAX_LINE_LEN];
+ char header[MAX_LINE_LEN];
+ char buffer[MAX_LINE_LEN];
+ int skip;
+ size_t len;
+
+ if (index < 0 || index >= entry_count)
+ return;
+
+ if (!has_network_connection()) {
+ print_footer("Error: No network connection.");
+ refresh();
+ napms(2000);
+ return;
+ }
+
+ if (is_local_entry(entries[index]))
+ return;
+
+ if (updates_counts[index] == domains_counts[index]) {
+ print_footer("Already up to date.");
+ refresh();
+ napms(1500);
+ return;
+ }
+
+ print_footer("Merging %s ...", entries[index]);
+ refresh();
+
+ url = entries[index];
+ new_content = download_url(url, error_msg, sizeof(error_msg));
+
+ if (!new_content) {
+ print_footer("Error during download: %s", error_msg);
+ refresh();
+ napms(2000);
+ return;
+ }
+
+ if (!contains_valid_hosts_entry(new_content)) {
+ print_footer("Error: No valid hosts line.");
+ refresh();
+ free(new_content);
+ napms(2000);
+ return;
+ }
+
+ if (!check_write_hosts(true)) {
+ free(new_content);
+ return;
+ }
+
+ in = fopen(hosts_path, "r");
+ out = fopen("/tmp/hosts_merge_temp", "w");
+
+ if (!in || !out) {
+ if (in) fclose(in);
+ if (out) fclose(out);
+ free(new_content);
+ return;
+ }
+
+ skip = 0;
+ snprintf(header, sizeof(header), "# %d. %s", index + 1, url);
+
+ while (fgets(line, sizeof(line), in)) {
+ if (strncmp(line, "# ", 2) == 0) {
+ if (strncmp(line, header, strlen(header)) == 0) {
+ skip = 1;
+ continue;
+ } else {
+ skip = 0;
+ }
+ }
+ if (!skip)
+ fputs(line, out);
+ }
+
+ fclose(in);
+
+ fprintf(out, "%s\n", header);
+ p = new_content;
+
+ while (*p) {
+ end = strchr(p, '\n');
+ len = end ? (size_t)(end - p) : strlen(p);
+
+ if (len > 0 && len < sizeof(buffer)) {
+ memcpy(buffer, p, len);
+ buffer[len] = '\0';
+ if (strncmp(buffer, "0.0.0.0 ", 8) == 0)
+ fprintf(out, "%s\n", buffer);
+ }
+
+ if (!end)
+ break;
+ p = end + 1;
+ }
+
+ fclose(out);
+ rename("/tmp/hosts_merge_temp", hosts_path);
+ free(new_content);
+
+ domains_counts[index] = count_hosts_in_section(index);
+ updates_counts[index] = domains_counts[index];
+ remote_sizes[index] = get_remote_content_length(entries[index]);
+ save_counts();
+}
+
+/* 29.00 */ static void
+merge_selected_entries(void)
+{
+ int i;
+
+ if (!check_write_hosts(true))
+ return;
+
+ for (i = 0; i < entry_count; i++) {
+ if (selected[i] && !is_local_entry(entries[i]))
+ merge_entry(i);
+ }
+
+ for (i = 0; i < MAX_ENTRIES; i++)
+ selected[i] = 0;
+}
+
+/* 30.00 */ static void
+rename_entry(int index)
+{
+ FILE *in, *out;
+ char input[MAX_LINE_LEN] = {0};
+ char line[MAX_LINE_LEN];
+ char old_header[MAX_LINE_LEN];
+ char new_header[MAX_LINE_LEN];
+ int input_start_x, pos, ch, next;
+
+ if (index < 0 || index >= entry_count)
+ return;
+
+ if (!is_local_entry(entries[index]))
+ return;
+
+ echo();
+ curs_set(1);
+
+ /* footer-prompt */
+ const char *prompt = "Rename to: ";
+ print_footer("%s", prompt);
+
+ input_start_x = (int)strlen(prompt);
+ apply_color(&config.footer, 2);
+ mvhline(LINES - 1, input_start_x, ' ', MAX_LINE_LEN - 1);
+
+ strncpy(input, entries[index], MAX_LINE_LEN - 1);
+ pos = strlen(input);
+
+ mvprintw(LINES - 1, input_start_x, "%s", input);
+ move(LINES - 1, input_start_x + pos);
+ refresh();
+
+ while ((ch = getch()) != '\n' && ch != ERR) {
+ if (ch == 27) {
+ nodelay(stdscr, 1);
+ next = getch();
+ nodelay(stdscr, 0);
+ if (next == ERR) {
+ remove_color(&config.footer, 2);
+ noecho();
+ curs_set(0);
+ move(LINES - 1, 0);
+ clrtoeol();
+ refresh();
+ return;
+ } else {
+ ungetch(next);
+ }
+ } else if (ch == KEY_BACKSPACE || ch == 127) {
+ if (pos > 0) {
+ pos--;
+ input[pos] = '\0';
+ mvaddch(LINES - 1, input_start_x + pos, ' ');
+ }
+ move(LINES - 1, input_start_x + pos);
+ } else if (pos < MAX_LINE_LEN - 1 && isprint(ch)) {
+ input[pos++] = ch;
+ input[pos] = '\0';
+ mvaddch(LINES - 1, input_start_x + pos - 1, ch);
+ move(LINES - 1, input_start_x + pos);
+ }
+ refresh();
+ }
+
+ remove_color(&config.footer, 2);
+
+ noecho();
+ curs_set(0);
+ move(LINES - 1, 0);
+ clrtoeol();
+ refresh();
+
+ if (pos == 0 || strspn(input, " \t") == strlen(input))
+ return;
+
+ if (!check_write_hosts(true))
+ return;
+
+ snprintf(old_header, sizeof(old_header),
+ "# %d. %s", index + 1, entries[index]);
+ snprintf(new_header, sizeof(new_header),
+ "# %d. %.220s", index + 1, input);
+
+ in = fopen(hosts_path, "r");
+ out = fopen("/tmp/hosts_rename_temp", "w");
+
+ if (!in || !out) {
+ if (in) fclose(in);
+ if (out) fclose(out);
+ return;
+ }
+
+ while (fgets(line, sizeof(line), in)) {
+ if (strncmp(line, old_header, strlen(old_header)) == 0) {
+ fprintf(out, "%s\n", new_header);
+ } else {
+ fputs(line, out);
+ }
+ }
+
+ fclose(in);
+ fclose(out);
+ rename("/tmp/hosts_rename_temp", hosts_path);
+
+ free(entries[index]);
+ entries[index] = strdup(input);
+ save_entries();
+
+ display_entries(index);
+}
+
+/* 31.00 */ const char *
+get_action_for_key(int key)
+{
+ for (int i = 0; i < keybinding_count; i++) {
+ if (keybindings[i].key == key)
+ return keybindings[i].action;
+ }
+ return NULL;
+}
+
+/* 32.00 */ static int
+has_network_connection(void)
+{
+ struct addrinfo hints = {0}, *res = NULL;
+ int result;
+
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ /* try resolving google.com */
+ result = getaddrinfo("google.com", NULL, &hints, &res);
+ if (res)
+ freeaddrinfo(res);
+
+ return result == 0;
+}
+
+/* 33.00 */ int
+main(int argc, char *argv[])
+{
+ int ch;
+ const char *editor = getenv("EDITOR");
+
+ /* load config file */
+ load_config();
+
+ /* resolve default config paths via XDG */
+ get_config_path("counts", counts_path, sizeof(counts_path));
+ get_config_path("urls", urls_path, sizeof(urls_path));
+ get_config_path("fetch.lock", fetch_lock_path, sizeof(fetch_lock_path));
+
+ /* initialize locale and curl */
+ setlocale(LC_ALL, "");
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+ update_progress = -1;
+
+ /* load saved state */
+ load_entries();
+ load_counts();
+
+ /* check for ongoing background fetches */
+ if (pipe(update_pipe) < 0) {
+ perror("pipe");
+ } else {
+ fcntl(update_pipe[0], F_SETFL, O_NONBLOCK);
+ fcntl(update_pipe[1], F_SETFD, 0);
+ }
+
+ if (fetch_lock_exists() && !is_fetch_lock_empty()) {
+ is_checking = 1;
+ start_update_in_background();
+ } else {
+ is_checking = 0;
+ }
+
+ if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) {
+ printf("Usage: hfc [OPTION]\n\n");
+ printf("Options:\n");
+ printf(" -h, --help Show usage information.\n");
+ printf(" -a, --add <url/list> Add a remote URL or custom list.\n");
+ printf(" -r, --remove <url/list> Remove a remote URL (https://...) or custom list.\n");
+ printf(" -e, --edit <list> Edit a local entry section with \033[1m$EDITOR\033[0m\n");
+ printf(" -U, --update_all Update all remote sources\n");
+ printf("\nReport bugs via xmpp to chat@marlonivo.xyz.\n");
+ return 0;
+ }
+
+ if (argc == 2 && (
+ !strcmp(argv[1], "-a") || !strcmp(argv[1], "--add") ||
+ !strcmp(argv[1], "-r") || !strcmp(argv[1], "--remove") ||
+ !strcmp(argv[1], "-e") || !strcmp(argv[1], "--edit")
+ )) {
+ fprintf(stderr,
+ "hosts file client\n"
+ "usage: hfc '%s' <url/list>\n"
+ "\n"
+ "Use -h to get help or, even better, run 'man hfc'\n", argv[1]);
+ return 1;
+ }
+
+ if (argc == 3 && (!strcmp(argv[1], "-a") || !strcmp(argv[1], "--add"))) {
+ char *url = argv[2];
+
+ if (!has_network_connection()) {
+ fprintf(stderr, "Error: No network connection.\n");
+ return 1;
+ }
+
+ char error_msg[CURL_ERROR_SIZE] = {0};
+ char *content = download_url(url, error_msg, sizeof(error_msg));
+
+ if (!content) {
+ fprintf(stderr, "Download failed: %s\n", error_msg);
+ return 1;
+ }
+
+ if (!contains_valid_hosts_entry(content)) {
+ fprintf(stderr, "Error: No valid hosts entries found.\n");
+ free(content);
+ return 1;
+ }
+
+ if (entry_count >= MAX_ENTRIES) {
+ fprintf(stderr, "Error: Maximum number of entries reached.\n");
+ free(content);
+ return 1;
+ }
+
+ if (!check_write_hosts(true)) {
+ free(content);
+ return 1;
+ }
+
+ entries[entry_count] = strdup(url);
+ entry_count++;
+ save_entries();
+
+ if (!save_to_hosts_file(content, url, entry_count)) {
+ fprintf(stderr, "Error: Could not write to hosts.\n");
+ free(content);
+ return 1;
+ }
+
+ int local_count = count_hosts_in_section(entry_count - 1);
+ domains_counts[entry_count - 1] = local_count;
+ update_domain_count(entry_count - 1);
+ save_counts();
+ free(content);
+
+ printf("Entry added successfully: %s\n", url);
+ return 0;
+ }
+
+ if (argc == 3 && (!strcmp(argv[1], "-r") || !strcmp(argv[1], "--remove"))) {
+ char *url = argv[2];
+ int found = -1;
+
+ for (int i = 0; i < entry_count; i++) {
+ if (strcmp(entries[i], url) == 0) {
+ found = i;
+ break;
+ }
+ }
+
+ if (found == -1) {
+ fprintf(stderr, "Entry not found: %s\n", url);
+ return 1;
+ }
+
+ if (!check_write_hosts(true)) {
+ return 1;
+ }
+
+ remove_entry(found);
+ printf("Entry removed successfully: %s\n", url);
+ return 0;
+ }
+
+ if (argc == 2 && (!strcmp(argv[1], "-U") || !strcmp(argv[1], "--update_all"))) {
+ if (!has_network_connection()) {
+ fprintf(stderr, "Error: No network connection.\n");
+ return 1;
+ }
+
+ int total = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (!is_local_entry(entries[i])) total++;
+ }
+
+ int current = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (is_local_entry(entries[i])) continue;
+ current++;
+ printf("(%d/%d) updating %s\n", current, total, entries[i]);
+ fflush(stdout);
+ update_domain_count(i);
+ }
+
+ save_counts();
+ printf("All sources updated.\n");
+ return 0;
+ }
+
+ if (argc == 3 && (!strcmp(argv[1], "-e") || !strcmp(argv[1], "--edit"))) {
+ char *url = argv[2];
+ int found = -1;
+
+ for (int i = 0; i < entry_count; i++) {
+ if (strcmp(entries[i], url) == 0) {
+ found = i;
+ break;
+ }
+ }
+
+ if (found == -1) {
+ fprintf(stderr, "Entry not found: %s\n", url);
+ return 1;
+ }
+
+ FILE *fp = fopen(hosts_path, "r");
+ if (!fp) {
+ fprintf(stderr, "Error: Could not open hosts\n");
+ return 1;
+ }
+
+ char line[1024];
+ int line_num = 0;
+ int target_line = -1;
+ char search_tag[256];
+ snprintf(search_tag, sizeof(search_tag), "# %d. %s", found + 1, entries[found]);
+
+ while (fgets(line, sizeof(line), fp)) {
+ line_num++;
+ if (strstr(line, search_tag)) {
+ target_line = line_num;
+ break;
+ }
+ }
+ fclose(fp);
+
+ if (target_line == -1) {
+ fprintf(stderr, "Error: Entry header not found in hosts\n");
+ return 1;
+ }
+
+ if (!check_write_hosts(true)) {
+ return 1;
+ }
+
+ char cmd[1024];
+ snprintf(cmd, sizeof(cmd), "%s +%d %s", editor, target_line, hosts_path);
+
+ int ret = system(cmd);
+ return ret;
+ }
+
+/* 34.00 */ initscr();
+
+ /* respect terminal themes (e.g. gruvbox) */
+ if (has_colors()) {
+ start_color();
+ use_default_colors();
+ load_config();
+ init_colors();
+ }
+
+ init_footer();
+
+ /* non-blocking input */
+ set_escdelay(10);
+ cbreak();
+ noecho();
+ keypad(stdscr, TRUE);
+ curs_set(0);
+ timeout(100);
+
+ /* render initial screen */
+ display_entries(highlight);
+ refresh();
+ update_all_sources();
+
+ while (1) {
+ /* check for background fetches */
+ int status;
+ pid_t pid;
+ while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
+ load_counts();
+ display_entries(highlight);
+ }
+ /* check update_pipe to refresh UI */
+ int idx;
+ while (read(update_pipe[0], &idx, sizeof(int)) > 0) {
+ if (idx >= 0 && idx < entry_count) {
+ display_entries(highlight);
+ }
+ }
+ /* handle user input */
+ ch = getch();
+
+ if (ch == KEY_RESIZE) {
+ endwin(); refresh(); clear();
+ display_entries(highlight);
+ continue;
+ }
+
+ if (ch == ERR) continue;
+
+ /* dismiss help screen */
+ if (in_help_mode) {
+ in_help_mode = 0;
+ display_entries(highlight);
+ continue;
+ }
+
+ /* all keys */
+ const char *action = get_action_for_key(ch);
+ if (!action) continue;
+
+ /* core actions */
+ if (!strcmp(action, "quit")) break;
+ else if (!strcmp(action, "select")) {
+ selected[highlight] = !selected[highlight];
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "select_all")) {
+ for (int i = 0; i < entry_count; i++) selected[i] = 1;
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "unselect_all")) {
+ for (int i = 0; i < entry_count; i++) selected[i] = 0;
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "up")) {
+ if (highlight > 0) highlight--;
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "down")) {
+ if (highlight < entry_count - 1) highlight++;
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "rename")) {
+ if (entry_count > 0 && is_local_entry(entries[highlight])) {
+ timeout(-1);
+ rename_entry(highlight);
+ timeout(100);
+ display_entries(highlight);
+ }
+ }
+ else if (!strcmp(action, "refresh")) {
+ load_config(); init_colors();
+ clear(); load_entries(); load_counts();
+ if (highlight >= entry_count) highlight = entry_count - 1;
+ if (highlight < 0) highlight = 0;
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "order")) {
+ if (entry_count > 1) {
+ timeout(-1);
+
+ char prompt[64];
+ snprintf(prompt, sizeof(prompt), "Move to position (1-%d): ", entry_count);
+ print_footer(" %s", prompt);
+
+ int input_col = 2 + (int)strlen(prompt);
+ char input[8] = {0};
+
+ apply_color(&config.footer, 2);
+ mvhline(LINES - 1, input_col, ' ', sizeof(input));
+
+ echo();
+ curs_set(1);
+ int ch = getch();
+ if (ch == 27) {
+ noecho();
+ curs_set(0);
+ remove_color(&config.footer, 2);
+ display_entries(highlight);
+ timeout(100);
+ continue;
+ }
+ ungetch(ch);
+ mvgetnstr(LINES - 1, input_col, input, sizeof(input) - 1);
+ clrtoeol();
+
+ remove_color(&config.footer, 2);
+ noecho();
+ curs_set(0);
+
+ int new_pos = atoi(input) - 1;
+ if (new_pos >= 0 && new_pos < entry_count && new_pos != highlight) {
+ reorder_entry(highlight, new_pos);
+ highlight = new_pos;
+ display_entries(highlight);
+ } else {
+ print_footer("Invalid position.");
+ refresh();
+ napms(1000);
+ display_entries(highlight);
+ }
+
+ timeout(100);
+ }
+ }
+ else if (!strcmp(action, "remove")) {
+ if (entry_count > 0) {
+ int selected_any = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (selected[i]) { selected_any = 1; break; }
+ }
+ timeout(-1);
+ if (selected_any) {
+ print_footer("Delete selected entries? (y/n): ");
+ } else {
+ print_footer("Remove %s? (y/n): ", entries[highlight]);
+ }
+ int confirm = getch();
+ timeout(100);
+
+ if (confirm == 'y' || confirm == 'Y') {
+ if (selected_any) {
+ print_footer("Deleting selected entries...");
+ refresh();
+ napms(100);
+ remove_selected_entries();
+ for (int i = 0; i < MAX_ENTRIES; i++) selected[i] = 0;
+ highlight = 0;
+ } else {
+ print_footer("Deleting...");
+ refresh();
+ napms(100);
+ remove_entry(highlight);
+ if (highlight >= entry_count && highlight > 0) highlight--;
+ }
+ display_entries(highlight);
+ } else {
+ display_entries(highlight);
+ }
+ }
+ }
+ else if (!strcmp(action, "help")) {
+ in_help_mode = 1;
+ display_help();
+ }
+ else if (!strcmp(action, "add")) {
+ timeout(-1);
+ add_entry();
+ timeout(100);
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "edit")) {
+ if (entry_count > 0) {
+ edit_entry(highlight);
+ display_entries(highlight);
+ }
+ }
+ else if (!strcmp(action, "update")) {
+ int any = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (selected[i]) { any = 1; break; }
+ }
+ if (!has_network_connection()) {
+ print_footer("Error: No network connection.");
+ napms(2000);
+ display_entries(highlight);
+ continue;
+ }
+ if (any) {
+ print_footer("Updating selected entries...");
+ update_selected_sources();
+ } else {
+ long before = remote_sizes[highlight];
+ long after = get_remote_content_length(entries[highlight]);
+ if (after > 0 && after != before) {
+ print_footer("Updating %s...", entries[highlight]);
+ update_domain_count(highlight);
+ domains_counts[highlight] = count_hosts_in_section(highlight);
+ updates_counts[highlight] = domains_counts[highlight];
+ save_counts();
+ } else {
+ print_footer("No updates available.");
+ napms(1500);
+ }
+ }
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "update_all")) {
+ if (!has_network_connection()) {
+ print_footer("Error: No network connection.");
+ napms(2000);
+ display_entries(highlight);
+ continue;
+ }
+ update_all_sources();
+ display_entries(highlight);
+ }
+ else if (!strcmp(action, "merge")) {
+ if (entry_count > 0 && !is_local_entry(entries[highlight])) {
+ int any = 0;
+ for (int i = 0; i < entry_count; i++) {
+ if (selected[i]) { any = 1; break; }
+ }
+ if (any) {
+ print_footer("Merging selected entries...");
+ merge_selected_entries();
+ } else {
+ merge_entry(highlight);
+ }
+ display_entries(highlight);
+ }
+ }
+ }
+
+ free_entries();
+ endwin();
+ curl_global_cleanup();
+ return 0;
+}
diff --git a/hfc.h b/hfc.h
@@ -0,0 +1,32 @@
+/* hfc.h */
+
+#ifndef HFC_H
+#define HFC_H
+
+#include "get.h"
+#include "hfc.h"
+
+#define MAX_ENTRIES 100
+#define MAX_LINE_LEN 256
+#define HFC_VERSION "0.0.1"
+
+extern int domain_count;
+extern int in_help_mode;
+extern int highlight;
+extern int updates_count;
+extern int entry_count;
+extern int is_checking;
+extern volatile int update_progress;
+
+extern int selected[MAX_ENTRIES];
+extern int update_pipe[2];
+extern int updates_counts[MAX_ENTRIES];
+extern int domains_counts[MAX_ENTRIES];
+
+extern Domain domains[MAX_ENTRIES];
+extern char *entries[MAX_ENTRIES];
+extern char urls_path[256];
+extern char counts_path[256];
+extern const char *hosts_path;
+
+#endif
diff --git a/update.c b/update.c
@@ -0,0 +1,52 @@
+/* update.c - fetch remote hosts file entries to update counter
+*
+* headers and macros */
+#define MAX_ENTRIES 100
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "get.h"
+#include "update.h"
+#include "hfc.h"
+
+
+int
+count_hosts_in_content(const char *content)
+{
+ const char *line, *next;
+ size_t len;
+ int count = 0;
+ char buffer[512], *cr;
+
+ if (!content)
+ return 0;
+
+ line = content;
+ while (*line) {
+ next = strchr(line, '\n');
+ len = next ? (size_t)(next - line) : strlen(line);
+ if (len >= sizeof(buffer))
+ len = sizeof(buffer) - 1;
+
+ memcpy(buffer, line, len);
+ buffer[len] = '\0';
+
+ cr = strchr(buffer, '\r');
+ if (cr)
+ *cr = '\0';
+
+ while (*buffer == ' ' || *buffer == '\t')
+ memmove(buffer, buffer + 1, strlen(buffer));
+
+ if (strncmp(buffer, "0.0.0.0 ", 8) == 0 || strncmp(buffer, "127.0.0.1 ", 10) == 0)
+ count++;
+
+ if (!next)
+ break;
+ line = next + 1;
+ }
+
+ return count;
+}
diff --git a/update.h b/update.h
@@ -0,0 +1,10 @@
+/* update.h */
+
+#ifndef UPDATE_H
+#define UPDATE_H
+
+#include "get.h"
+
+int count_hosts_in_content(const char *content);
+
+#endif