#include #include #include #include #include #include #include #include #define NEX_PORT 1900 #define MAX_BUFFER 65536 #define MAX_LINKS 1000 #define MAX_URL 512 typedef struct { char url[MAX_URL]; int line_num; } Link; typedef struct { char *content; size_t content_len; Link links[MAX_LINKS]; int link_count; char current_host[256]; char current_path[512]; } Document; typedef struct { char host[256]; char path[512]; int scroll_offset; int cursor_line; } HistoryEntry; #define MAX_HISTORY 100 Document doc; int scroll_offset = 0; int cursor_line = 0; int selected_link = -1; HistoryEntry history[MAX_HISTORY]; int history_pos = -1; int history_size = 0; int nex_fetch(const char *host, const char *path, char **content, size_t *len) { struct hostent *server; struct sockaddr_in addr; int sockfd; char request[1024]; char buffer[4096]; size_t total = 0; ssize_t n; *content = NULL; *len = 0; server = gethostbyname(host); if (!server) return -1; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) return -1; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length); addr.sin_port = htons(NEX_PORT); if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(sockfd); return -1; } snprintf(request, sizeof(request), "%s\r\n", path); write(sockfd, request, strlen(request)); *content = malloc(MAX_BUFFER); if (!*content) { close(sockfd); return -1; } while ((n = read(sockfd, buffer, sizeof(buffer))) > 0) { if (total + n >= MAX_BUFFER) break; memcpy(*content + total, buffer, n); total += n; } *len = total; close(sockfd); return 0; } void parse_url(const char *url, char *host, char *path) { const char *p = url; if (strncmp(p, "nex://", 6) == 0) { p += 6; } const char *slash = strchr(p, '/'); if (slash) { size_t host_len = slash - p; strncpy(host, p, host_len); host[host_len] = '\0'; strcpy(path, slash); } else { strcpy(host, p); strcpy(path, "/"); } } void resolve_path(const char *base, const char *rel, char *result) { if (rel[0] == '/') { strcpy(result, rel); return; } if (strncmp(rel, "nex://", 6) == 0) { strcpy(result, rel); return; } char temp[512]; strcpy(temp, base); char *last_slash = strrchr(temp, '/'); if (last_slash && last_slash > temp) { *(last_slash + 1) = '\0'; } else if (last_slash) { strcpy(temp, "/"); } const char *rel_ptr = rel; while (strncmp(rel_ptr, "../", 3) == 0) { if (strlen(temp) > 1) { temp[strlen(temp) - 1] = '\0'; char *prev_slash = strrchr(temp, '/'); if (prev_slash && prev_slash > temp) { *(prev_slash + 1) = '\0'; } else { strcpy(temp, "/"); } } rel_ptr += 3; } if (*rel_ptr != '\0') { strcat(temp, rel_ptr); } strcpy(result, temp); } void parse_document() { doc.link_count = 0; if (!doc.content) return; char *line = doc.content; int line_num = 0; for (size_t i = 0; i < doc.content_len; i++) { if (doc.content[i] == '\n' || i == doc.content_len - 1) { size_t line_len = doc.content + i - line; if (i == doc.content_len - 1 && doc.content[i] != '\n') line_len++; if (line_len >= 3 && line[0] == '=' && line[1] == '>' && line[2] == ' ') { if (doc.link_count < MAX_LINKS) { size_t url_len = line_len - 3; if (url_len >= MAX_URL) url_len = MAX_URL - 1; strncpy(doc.links[doc.link_count].url, line + 3, url_len); doc.links[doc.link_count].url[url_len] = '\0'; char *end = doc.links[doc.link_count].url + url_len - 1; while (end >= doc.links[doc.link_count].url && isspace(*end)) { *end = '\0'; end--; } doc.links[doc.link_count].line_num = line_num; doc.link_count++; } } line = doc.content + i + 1; line_num++; } } } int find_link_at_line(int line) { for (int i = 0; i < doc.link_count; i++) { if (doc.links[i].line_num == line) { return i; } } return -1; } int load_url(const char *url) { char host[256], path[512]; if (strncmp(url, "nex://", 6) == 0) { parse_url(url, host, path); } else { strcpy(host, doc.current_host); resolve_path(doc.current_path, url, path); } if (doc.content) { free(doc.content); doc.content = NULL; } size_t len; if (nex_fetch(host, path, &doc.content, &len) < 0) { return -1; } doc.content_len = len; strcpy(doc.current_host, host); strcpy(doc.current_path, path); parse_document(); scroll_offset = 0; cursor_line = 0; selected_link = -1; return 0; } void add_to_history(const char *host, const char *path, int save_scroll) { if (history_pos < history_size - 1) { history_size = history_pos + 1; } if (history_pos >= 0 && strcmp(history[history_pos].host, host) == 0 && strcmp(history[history_pos].path, path) == 0) { return; } if (history_size < MAX_HISTORY) { history_size++; history_pos++; } else { memmove(&history[0], &history[1], sizeof(HistoryEntry) * (MAX_HISTORY - 1)); history_pos = MAX_HISTORY - 1; } strcpy(history[history_pos].host, host); strcpy(history[history_pos].path, path); history[history_pos].scroll_offset = save_scroll ? scroll_offset : 0; history[history_pos].cursor_line = save_scroll ? cursor_line : 0; } int load_url_with_history(const char *url) { char host[256], path[512]; if (strncmp(url, "nex://", 6) == 0) { parse_url(url, host, path); } else { strcpy(host, doc.current_host); resolve_path(doc.current_path, url, path); } if (doc.content) { add_to_history(doc.current_host, doc.current_path, 1); } if (load_url(url) < 0) { return -1; } add_to_history(host, path, 0); return 0; } int history_back() { if (history_pos > 0) { history[history_pos].scroll_offset = scroll_offset; history[history_pos].cursor_line = cursor_line; history_pos--; char url[MAX_URL]; snprintf(url, sizeof(url), "nex://%s%s", history[history_pos].host, history[history_pos].path); if (load_url(url) == 0) { scroll_offset = history[history_pos].scroll_offset; cursor_line = history[history_pos].cursor_line; selected_link = find_link_at_line(cursor_line); return 0; } } return -1; } int history_forward() { if (history_pos < history_size - 1) { history[history_pos].scroll_offset = scroll_offset; history[history_pos].cursor_line = cursor_line; history_pos++; char url[MAX_URL]; snprintf(url, sizeof(url), "nex://%s%s", history[history_pos].host, history[history_pos].path); if (load_url(url) == 0) { scroll_offset = history[history_pos].scroll_offset; cursor_line = history[history_pos].cursor_line; selected_link = find_link_at_line(cursor_line); return 0; } } return -1; } void draw_ui() { int h, w; getmaxyx(stdscr, h, w); clear(); attron(A_REVERSE); mvhline(0, 0, ' ', w); char title[256]; snprintf(title, sizeof(title), "NexNCursed - %s%s", doc.current_host, doc.current_path); mvprintw(0, 1, "%.*s", w - 2, title); attroff(A_REVERSE); int content_height = h - 3; char *line = doc.content; int line_num = 0; int y = 1; for (size_t i = 0; i < doc.content_len && y < h - 2; i++) { if (line_num >= scroll_offset && line_num < scroll_offset + content_height) { if (line_num == cursor_line) { attron(A_REVERSE); } if (doc.content[i] == '\n' || i == doc.content_len - 1) { size_t line_len = doc.content + i - line; if (i == doc.content_len - 1 && doc.content[i] != '\n') line_len++; mvprintw(y, 0, "%.*s", (int)(line_len < w ? line_len : w), line); if (line_num == cursor_line) { attroff(A_REVERSE); } line = doc.content + i + 1; y++; } } if (doc.content[i] == '\n') { line_num++; line = doc.content + i + 1; } } attron(A_REVERSE); mvhline(h - 2, 0, ' ', w); mvprintw(h - 2, 1, "h/l - history back/forward | j/k - down/up | o - open | f - " "follow | q - quit"); attroff(A_REVERSE); mvhline(h - 1, 0, ' ', w); if (selected_link >= 0) { mvprintw(h - 1, 1, "follow: %s", doc.links[selected_link].url); } refresh(); } int count_lines() { int lines = 1; for (size_t i = 0; i < doc.content_len; i++) { if (doc.content[i] == '\n') lines++; } return lines; } void input_prompt(const char *prompt, char *buf, size_t buf_size) { int h, w; getmaxyx(stdscr, h, w); echo(); curs_set(1); mvhline(h - 1, 0, ' ', w); mvprintw(h - 1, 1, "%s", prompt); refresh(); getnstr(buf, buf_size - 1); noecho(); curs_set(0); } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s [path]\n", argv[0]); return 1; } memset(&doc, 0, sizeof(doc)); char initial_url[MAX_URL]; if (argc >= 3) { snprintf(initial_url, sizeof(initial_url), "nex://%s%s", argv[1], argv[2]); } else { snprintf(initial_url, sizeof(initial_url), "nex://%s/", argv[1]); } if (load_url(initial_url) < 0) { fprintf(stderr, "Failed to connect to %s\n", argv[1]); return 1; } initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0); int running = 1; int h, w; while (running) { getmaxyx(stdscr, h, w); draw_ui(); int ch = getch(); int total_lines = count_lines(); int content_height = h - 3; switch (ch) { case 'q': case 'Q': running = 0; break; case 'j': case KEY_DOWN: if (cursor_line < total_lines - 1) { cursor_line++; if (cursor_line >= scroll_offset + content_height) { scroll_offset++; } selected_link = find_link_at_line(cursor_line); } break; case 'k': case KEY_UP: if (cursor_line > 0) { cursor_line--; if (cursor_line < scroll_offset) { scroll_offset--; } selected_link = find_link_at_line(cursor_line); } break; case 'f': case 'F': if (selected_link >= 0) { if (load_url_with_history(doc.links[selected_link].url) < 0) { mvprintw(h - 1, 1, "Failed to load URL"); refresh(); napms(1000); } } break; case 'o': case 'O': { char url[MAX_URL]; input_prompt("open: ", url, sizeof(url)); if (strlen(url) > 0) { if (load_url_with_history(url) < 0) { mvprintw(h - 1, 1, "Failed to load URL"); refresh(); napms(1000); } } break; } case 'h': case 'H': case KEY_LEFT: if (history_back() < 0) { mvprintw(h - 1, 1, "No previous page in history"); refresh(); napms(500); } break; case 'l': case 'L': case KEY_RIGHT: if (history_forward() < 0) { mvprintw(h - 1, 1, "No next page in history"); refresh(); napms(500); } break; } } endwin(); if (doc.content) { free(doc.content); } return 0; }