#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #define JAFM_VERSION "0.1.0" #define JAFM_TAGLINE "just a fucking manual" #define JAFM_AUTHOR "hachem " #define MAX_VISIBLE 12 #define PANEL_MIN_WIDTH 74 #define LIST_WIDTH 44 #define WORKER_THREADS 8 #define MAX_WORKER_THREADS 64 #define SYMBOL_SIZE 128 #define KIND_SIZE 16 #define TIER_BODY 1 #define TIER_SYNOPSIS 2 #define TIER_DEFINITION 3 #define TIER_NAME 4 #define WEIGHT_FILENAME 1000000 #define WEIGHT_NAME 5000 #define WEIGHT_DEFINITION 2000 #define WEIGHT_SYNOPSIS 50 #define WEIGHT_BODY 1 #define C_RESET "\x1b[0m" #define C_DIM "\x1b[2m" #define C_BOLD "\x1b[1m" #define C_KEY "\x1b[36m" #define C_TITLE "\x1b[1;36m" #define C_NAME "\x1b[1;36m" #define C_SYMBOL "\x1b[33m" #define C_MATCH "\x1b[1;33m" #define C_COUNT "\x1b[32m" struct Match { char* path; long score; char symbol[SYMBOL_SIZE]; char kind[KIND_SIZE]; char** preview; size_t preview_count; size_t preview_start; bool preview_loaded; }; struct MatchList { struct Match* items; size_t count; size_t capacity; }; static bool match_list_add(struct MatchList* matches, const char* path, long score, const char* symbol, const char* kind) { if (matches->count == matches->capacity) { size_t new_capacity = matches->capacity == 0 ? 16 : matches->capacity * 2; struct Match* grown = realloc(matches->items, new_capacity * sizeof(struct Match)); if (grown == NULL) return false; matches->items = grown; matches->capacity = new_capacity; } char* copy = strdup(path); if (copy == NULL) return false; struct Match* item = &matches->items[matches->count]; item->path = copy; item->score = score; (void) snprintf(item->symbol, sizeof(item->symbol), "%s", symbol); (void) snprintf(item->kind, sizeof(item->kind), "%s", kind); item->preview = NULL; item->preview_count = 0; item->preview_start = 0; item->preview_loaded = false; matches->count++; return true; } static void match_free(struct Match* match) { for (size_t index = 0; index < match->preview_count; index++) free(match->preview[index]); free(match->preview); free(match->path); } static void match_list_free(struct MatchList* matches) { for (size_t index = 0; index < matches->count; index++) match_free(&matches->items[index]); free(matches->items); } static int compare_matches(const void* left, const void* right) { const struct Match* first = left; const struct Match* second = right; if (first->score != second->score) return first->score > second->score ? -1 : 1; return strcmp(first->path, second->path); } static bool ends_with(const char* string, const char* suffix) { size_t string_length = strlen(string); size_t suffix_length = strlen(suffix); if (suffix_length > string_length) return false; return strcmp(string + string_length - suffix_length, suffix) == 0; } static void strip_compression_suffix(char* name) { static const char* suffixes[] = {".gz", ".zst", ".xz", ".bz2"}; for (size_t index = 0; index < sizeof(suffixes) / sizeof(suffixes[0]); index++) if (ends_with(name, suffixes[index])) { name[strlen(name) - strlen(suffixes[index])] = '\0'; return; } } static bool is_section_header(const char* line) { return strncmp(line, ".SH", 3) == 0 || strncmp(line, ".Sh", 3) == 0; } static bool is_definition_line(const char* line) { if (strchr(line, '{') == NULL) return false; return strstr(line, "struct") != NULL || strstr(line, "union") != NULL || strstr(line, "enum") != NULL || strstr(line, "typedef") != NULL; } static bool is_identifier_char(char character) { return isalnum((unsigned char) character) || character == '_'; } static bool is_whole_word(const char* line, const char* found, size_t query_length) { if (found != line && is_identifier_char(found[-1])) return false; return !is_identifier_char(found[query_length]); } static const char* find_query(const char* line, const char* query, size_t query_length, bool exact) { for (const char* cursor = strstr(line, query); cursor != NULL; cursor = strstr(cursor + query_length, query)) if (!exact || is_whole_word(line, cursor, query_length)) return cursor; return NULL; } static long count_occurrences(const char* line, const char* query, bool exact) { size_t query_length = strlen(query); if (query_length == 0) return 0; long count = 0; for (const char* cursor = strstr(line, query); cursor != NULL; cursor = strstr(cursor + query_length, query)) if (!exact || is_whole_word(line, cursor, query_length)) count++; return count; } static void extract_symbol(const char* line, const char* query, bool exact, char* symbol, size_t symbol_size) { const char* found = find_query(line, query, strlen(query), exact); if (found == NULL) return; const char* start = found; while (start > line && is_identifier_char(start[-1])) start--; const char* end = found + strlen(query); while (is_identifier_char(*end)) end++; if (start > line && start[-1] == '\\' && start[0] == 'f') { start += start[1] == '(' ? 4 : 2; if (start > end) start = end; } size_t length = (size_t) (end - start); if (length >= symbol_size) length = symbol_size - 1; memcpy(symbol, start, length); symbol[length] = '\0'; } static void classify_kind(const char* line, const char* symbol, char* kind, size_t kind_size) { const char* word = NULL; if (strncmp(line, ".Fn", 3) == 0 || strncmp(line, ".Fo", 3) == 0) word = "function"; else if (strstr(line, "struct") != NULL) word = "struct"; else if (strstr(line, "union") != NULL) word = "union"; else if (strstr(line, "enum") != NULL) word = "enum"; else if (strstr(line, "typedef") != NULL) word = "typedef"; if (word == NULL && symbol[0] != '\0') { const char* after = strstr(line, symbol); if (after != NULL) { after += strlen(symbol); while (*after == ' ' || *after == '\t') after++; if (*after == '(') word = "function"; } } (void) snprintf(kind, kind_size, "%s", word != NULL ? word : "symbol"); } struct Scan { bool in_name; bool in_synopsis; long name_hits; long definition_hits; long synopsis_hits; long body_hits; int best_tier; char symbol[SYMBOL_SIZE]; char kind[KIND_SIZE]; bool exact; const char* want_kind; bool has_wanted; int best_wanted_tier; char wanted_symbol[SYMBOL_SIZE]; }; struct Finding { long score; char symbol[SYMBOL_SIZE]; char kind[KIND_SIZE]; }; struct Search { const char* query; bool exact; const char* want_kind; }; static void scan_line(struct Scan* scan, const char* line, const char* query) { if (is_section_header(line)) { scan->in_name = strstr(line, "NAME") != NULL; scan->in_synopsis = strstr(line, "SYNOPSIS") != NULL; return; } long hits = count_occurrences(line, query, scan->exact); if (hits == 0) return; int tier; if (scan->in_name || strncmp(line, ".Nm", 3) == 0) { scan->name_hits += hits; tier = TIER_NAME; } else if (is_definition_line(line)) { scan->definition_hits += hits; tier = TIER_DEFINITION; } else if (scan->in_synopsis) { scan->synopsis_hits += hits; tier = TIER_SYNOPSIS; } else { scan->body_hits += hits; tier = TIER_BODY; } if (tier > scan->best_tier) { scan->best_tier = tier; extract_symbol(line, query, scan->exact, scan->symbol, sizeof(scan->symbol)); classify_kind(line, scan->symbol, scan->kind, sizeof(scan->kind)); } if (scan->want_kind != NULL && tier > scan->best_wanted_tier) { char symbol[SYMBOL_SIZE]; char kind[KIND_SIZE]; extract_symbol(line, query, scan->exact, symbol, sizeof(symbol)); classify_kind(line, symbol, kind, sizeof(kind)); if (strcmp(kind, scan->want_kind) == 0) { scan->best_wanted_tier = tier; scan->has_wanted = true; (void) snprintf(scan->wanted_symbol, sizeof(scan->wanted_symbol), "%s", symbol); } } } static long scan_score(const struct Scan* scan) { return scan->name_hits * WEIGHT_NAME + scan->definition_hits * WEIGHT_DEFINITION + scan->synopsis_hits * WEIGHT_SYNOPSIS + scan->body_hits * WEIGHT_BODY; } static char* read_stream(FILE* stream) { size_t capacity = 65536; size_t length = 0; char* buffer = malloc(capacity); if (buffer == NULL) return NULL; for (;;) { size_t got = fread(buffer + length, 1, capacity - length - 1, stream); length += got; if (length + 1 < capacity) break; capacity *= 2; char* grown = realloc(buffer, capacity); if (grown == NULL) { free(buffer); return NULL; } buffer = grown; } buffer[length] = '\0'; return buffer; } static char* read_plain(const char* path) { FILE* page = fopen(path, "rb"); if (page == NULL) return NULL; char* buffer = read_stream(page); (void) fclose(page); return buffer; } static char* read_command(const char* tool, const char* path) { char command[8192]; (void) snprintf(command, sizeof(command), "%s -dc -- '%s' 2>/dev/null", tool, path); FILE* pipe = popen(command, "r"); if (pipe == NULL) return NULL; char* buffer = read_stream(pipe); (void) pclose(pipe); return buffer; } static char* read_gzip(const char* path) { gzFile page = gzopen(path, "rb"); if (page == NULL) return NULL; size_t capacity = 65536; size_t length = 0; char* buffer = malloc(capacity); if (buffer == NULL) { (void) gzclose(page); return NULL; } for (;;) { int got = gzread(page, buffer + length, (unsigned) (capacity - length - 1)); if (got <= 0) break; length += (size_t) got; if (length + 1 < capacity) continue; capacity *= 2; char* grown = realloc(buffer, capacity); if (grown == NULL) { free(buffer); (void) gzclose(page); return NULL; } buffer = grown; } (void) gzclose(page); buffer[length] = '\0'; return buffer; } static char* read_page(const char* path) { if (ends_with(path, ".gz")) return read_gzip(path); if (ends_with(path, ".zst")) return read_command("zstd", path); if (ends_with(path, ".xz")) return read_command("xz", path); if (ends_with(path, ".bz2")) return read_command("bzip2", path); return read_plain(path); } static void scan_buffer(char* buffer, const char* query, struct Scan* scan) { if (strstr(buffer, query) == NULL) return; char* line = buffer; while (line != NULL && *line != '\0') { char* newline = strchr(line, '\n'); if (newline != NULL) *newline = '\0'; scan_line(scan, line, query); line = newline != NULL ? newline + 1 : NULL; } } static void page_find(const char* path, const struct Search* search, struct Finding* finding) { char* buffer = read_page(path); if (buffer == NULL) return; struct Scan scan = {0}; scan.exact = search->exact; scan.want_kind = search->want_kind; scan_buffer(buffer, search->query, &scan); free(buffer); if (search->want_kind != NULL) { if (!scan.has_wanted) return; finding->score = scan_score(&scan); (void) snprintf(finding->symbol, sizeof(finding->symbol), "%s", scan.wanted_symbol); (void) snprintf(finding->kind, sizeof(finding->kind), "%s", search->want_kind); return; } finding->score = scan_score(&scan); (void) snprintf(finding->symbol, sizeof(finding->symbol), "%s", scan.symbol); (void) snprintf(finding->kind, sizeof(finding->kind), "%s", scan.kind); } static long section_weight(const char* section) { const char* name = strrchr(section, '/'); name = name != NULL ? name + 1 : section; if (strncmp(name, "man", 3) == 0) name += 3; switch (name[0]) { case '0': case '2': case '4': case '5': case '7': case '9': return 8; default: return 1; } } static bool filename_matches(const char* name, const char* query) { char base[256]; (void) snprintf(base, sizeof(base), "%s", name); strip_compression_suffix(base); char* section_dot = strrchr(base, '.'); if (section_dot != NULL) *section_dot = '\0'; return strcmp(base, query) == 0; } static struct termios original_termios; static volatile sig_atomic_t raw_mode_active = 0; static void restore_terminal(void) { if (!raw_mode_active) return; (void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios); (void) write(STDOUT_FILENO, "\x1b[?25h", 6); raw_mode_active = 0; } static void handle_signal(int signal_number) { restore_terminal(); (void) signal(signal_number, SIG_DFL); (void) raise(signal_number); } static void disable_raw_mode(void) { restore_terminal(); } static bool enable_raw_mode(void) { if (tcgetattr(STDIN_FILENO, &original_termios) == -1) return false; struct termios raw = original_termios; raw.c_lflag &= ~(tcflag_t)(ECHO | ICANON | ISIG); raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) return false; raw_mode_active = 1; (void) write(STDOUT_FILENO, "\x1b[?25l", 6); (void) signal(SIGINT, handle_signal); (void) signal(SIGTERM, handle_signal); (void) signal(SIGHUP, handle_signal); (void) signal(SIGQUIT, handle_signal); return true; } static void terminal_size(size_t* rows, size_t* cols) { struct winsize window; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_row > 0) { *rows = window.ws_row; *cols = window.ws_col > 0 ? window.ws_col : 80; } else { *rows = 24; *cols = 80; } } static void page_label(const char* path, char* label, size_t label_size) { const char* slash = strrchr(path, '/'); const char* name = slash != NULL ? slash + 1 : path; char base[256]; (void) snprintf(base, sizeof(base), "%s", name); strip_compression_suffix(base); char* section_dot = strrchr(base, '.'); if (section_dot != NULL) { *section_dot = '\0'; (void) snprintf(label, label_size, "%s(%s)", base, section_dot + 1); } else (void) snprintf(label, label_size, "%s", base); } static void load_preview(struct Match* match, const char* query); static size_t fit(char* out, size_t out_size, const char* source, size_t width) { size_t column = 0; size_t offset = 0; for (const char* cursor = source; *cursor != '\0' && column < width; cursor++) { unsigned char character = (unsigned char) *cursor; if (character == '\t') { size_t spaces = 8 - (column % 8); while (spaces > 0 && column < width && offset + 1 < out_size) { out[offset++] = ' '; column++; spaces--; } } else if (character >= 32 && character < 127) { if (offset + 1 < out_size) { out[offset++] = *cursor; column++; } } else if (character >= 128) { if (offset + 1 < out_size) { out[offset++] = *cursor; if ((character & 0xC0) != 0x80) column++; } } } out[offset] = '\0'; return column; } static size_t print_segment(const char* text, const char* color, size_t remaining) { if (remaining == 0) return 0; char buffer[512]; size_t width = fit(buffer, sizeof(buffer), text, remaining); if (width == 0) return 0; if (color[0] != '\0') (void) printf("%s%s\x1b[0m", color, buffer); else (void) printf("%s", buffer); return width; } static size_t print_list_cell(const struct Match* item, bool selected, size_t budget) { char label[256]; page_label(item->path, label, sizeof(label)); size_t used = print_segment(label, selected ? C_BOLD : C_DIM, budget); if (item->symbol[0] != '\0') { used += print_segment(" ", "", budget - used); used += print_segment(item->symbol, C_SYMBOL, budget - used); if (item->kind[0] != '\0') { char kind[KIND_SIZE + 3]; (void) snprintf(kind, sizeof(kind), " (%s)", item->kind); used += print_segment(kind, C_DIM, budget - used); } } return used; } static void print_highlighted(const char* text, const char* query) { size_t query_length = strlen(query); if (query_length == 0) { (void) printf("%s", text); return; } const char* cursor = text; for (const char* hit = strstr(cursor, query); hit != NULL; hit = strstr(cursor, query)) { (void) printf("%.*s", (int) (hit - cursor), cursor); (void) printf(C_MATCH "%.*s" C_RESET, (int) query_length, hit); cursor = hit + query_length; } (void) printf("%s", cursor); } struct Layout { bool panel; size_t usable; size_t left_width; size_t right_width; }; static size_t list_column_width(const struct MatchList* matches) { size_t widest = LIST_WIDTH; for (size_t index = 0; index < matches->count; index++) { char label[256]; page_label(matches->items[index].path, label, sizeof(label)); const char* symbol = matches->items[index].symbol; const char* kind = matches->items[index].kind; size_t entry = strlen(label) + 2; if (symbol[0] != '\0') { entry += 2 + strlen(symbol); if (kind[0] != '\0') entry += 3 + strlen(kind); } if (entry > widest) widest = entry; } return widest; } static struct Layout compute_layout(const struct MatchList* matches, size_t cols) { size_t indent = 2; size_t usable = cols > indent + 1 ? cols - indent - 1 : 0; struct Layout layout = {false, usable, usable, 0}; if (usable < PANEL_MIN_WIDTH) return layout; size_t separator = 3; size_t minimum_preview = 24; size_t cap = usable - minimum_preview - separator; size_t widest = list_column_width(matches); layout.panel = true; layout.left_width = widest < cap ? widest : cap; layout.right_width = usable - layout.left_width - separator; return layout; } static void render_row(const struct MatchList* matches, size_t list_index, size_t highlight, size_t end, const struct Layout* layout, const struct Match* current, size_t row, const char* query) { (void) printf("\x1b[2K "); if (list_index < end) { bool selected = list_index == highlight; if (selected) (void) printf(C_KEY "\xe2\x9d\xaf" C_RESET " "); else (void) printf(" "); size_t budget = layout->left_width > 2 ? layout->left_width - 2 : 0; size_t used = print_list_cell(&matches->items[list_index], selected, budget); if (layout->panel) for (size_t pad = used + 2; pad < layout->left_width; pad++) (void) putchar(' '); } else if (layout->panel) for (size_t pad = 0; pad < layout->left_width; pad++) (void) putchar(' '); if (layout->panel) { (void) printf(C_DIM " \xe2\x94\x82" C_RESET " "); size_t preview_index = current->preview_start + row; if (current->preview_loaded && preview_index < current->preview_count) { char cell[1024]; (void) fit(cell, sizeof(cell), current->preview[preview_index], layout->right_width); print_highlighted(cell, query); } } (void) printf("\r\n"); } static void render_footer(const struct Match* current, size_t highlight, size_t count, size_t usable) { (void) printf("\x1b[2K "); const char* path = current->path; const char* slash = strrchr(path, '/'); if (slash != NULL) { char directory[1024]; (void) snprintf(directory, sizeof(directory), "%.*s", (int) (slash - path + 1), path); size_t used = print_segment(directory, C_DIM, usable); (void) print_segment(slash + 1, C_NAME, usable - used); } else (void) print_segment(path, C_NAME, usable); (void) printf("\r\n"); (void) printf("\x1b[2K "); size_t used = 0; used += print_segment("up/down", C_KEY, usable - used); used += print_segment(" move ", C_DIM, usable - used); used += print_segment("enter", C_KEY, usable - used); used += print_segment(" open ", C_DIM, usable - used); used += print_segment("q", C_KEY, usable - used); used += print_segment(" quit ", C_DIM, usable - used); char counter[32]; (void) snprintf(counter, sizeof(counter), "[%zu/%zu]", highlight + 1, count); (void) print_segment(counter, C_COUNT, usable - used); (void) printf("\r\n"); } static size_t render_menu(const struct MatchList* matches, size_t highlight, size_t top, size_t visible, size_t cols, const char* query, size_t previous_height) { if (previous_height > 0) (void) printf("\x1b[%zuA", previous_height); struct Layout layout = compute_layout(matches, cols); const struct Match* current = &matches->items[highlight]; size_t height = 0; (void) printf("\r\x1b[2K " C_TITLE "jafm" C_RESET " " C_DIM "%zu matches" C_RESET "\r\n", matches->count); height++; (void) printf("\x1b[2K\r\n"); height++; size_t end = top + visible; if (end > matches->count) end = matches->count; for (size_t row = 0; row < visible; row++) { render_row(matches, top + row, highlight, end, &layout, current, row, query); height++; } (void) printf("\x1b[2K\r\n"); height++; render_footer(current, highlight, matches->count, layout.usable); height += 2; (void) printf("\x1b[J"); return height; } static bool select_interactive(struct MatchList* matches, const char* query, size_t* selected) { if (!enable_raw_mode()) return false; size_t highlight = 0; size_t top = 0; size_t previous_height = 0; bool chosen = false; bool done = false; while (!done) { size_t rows = 0; size_t cols = 0; terminal_size(&rows, &cols); size_t reserved = 5; size_t visible = rows > reserved ? rows - reserved : 1; if (visible > MAX_VISIBLE) visible = MAX_VISIBLE; if (highlight < top) top = highlight; else if (highlight >= top + visible) top = highlight - visible + 1; load_preview(&matches->items[highlight], query); previous_height = render_menu(matches, highlight, top, visible, cols, query, previous_height); (void) fflush(stdout); char key = 0; if (read(STDIN_FILENO, &key, 1) != 1) break; if (key == '\r' || key == '\n') { chosen = true; done = true; } else if (key == 'q' || key == 3) done = true; else if (key == 'j') { if (highlight + 1 < matches->count) highlight++; } else if (key == 'k') { if (highlight > 0) highlight--; } else if (key == '\x1b') { char sequence[2]; if (read(STDIN_FILENO, &sequence[0], 1) == 1 && read(STDIN_FILENO, &sequence[1], 1) == 1 && (sequence[0] == '[' || sequence[0] == 'O')) { if (sequence[1] == 'B' && highlight + 1 < matches->count) highlight++; else if (sequence[1] == 'A' && highlight > 0) highlight--; } } } if (previous_height > 0) (void) printf("\x1b[%zuA\r\x1b[J", previous_height); (void) fflush(stdout); disable_raw_mode(); if (chosen) *selected = highlight; return chosen; } static bool read_selection(size_t count, size_t* selected) { (void) printf("select [1-%zu]: ", count); char input[64]; if (fgets(input, sizeof(input), stdin) == NULL) return false; char* end = NULL; long value = strtol(input, &end, 10); if (end == input || value < 1 || (size_t) value > count) return false; *selected = (size_t) value - 1; return true; } struct Task { char* path; long weight; }; struct TaskList { struct Task* items; size_t count; size_t capacity; }; static bool task_list_add(struct TaskList* tasks, const char* path, long weight) { if (tasks->count == tasks->capacity) { size_t new_capacity = tasks->capacity == 0 ? 1024 : tasks->capacity * 2; struct Task* grown = realloc(tasks->items, new_capacity * sizeof(struct Task)); if (grown == NULL) return false; tasks->items = grown; tasks->capacity = new_capacity; } char* copy = strdup(path); if (copy == NULL) return false; tasks->items[tasks->count].path = copy; tasks->items[tasks->count].weight = weight; tasks->count++; return true; } static void task_list_free(struct TaskList* tasks) { for (size_t index = 0; index < tasks->count; index++) free(tasks->items[index].path); free(tasks->items); } static void collect_pages(const char* section, struct TaskList* tasks) { DIR* section_stream = opendir(section); if (section_stream == NULL) return; long weight = section_weight(section); for (const struct dirent* entry = readdir(section_stream); entry != NULL; entry = readdir(section_stream)) if (entry->d_name[0] != '.') { char path[4096]; (void) snprintf(path, sizeof(path), "%s/%s", section, entry->d_name); (void) task_list_add(tasks, path, weight); } (void) closedir(section_stream); } static void collect_sections(const char* directory, struct TaskList* tasks) { DIR* directory_stream = opendir(directory); if (directory_stream == NULL) return; for (const struct dirent* entry = readdir(directory_stream); entry != NULL; entry = readdir(directory_stream)) if (strncmp(entry->d_name, "man", 3) == 0) { char section[4096]; (void) snprintf(section, sizeof(section), "%s/%s", directory, entry->d_name); collect_pages(section, tasks); } (void) closedir(directory_stream); } static void scan_task(const struct Task* task, const struct Search* search, struct MatchList* matches) { const char* path = task->path; struct Finding finding = {0}; page_find(path, search, &finding); if (search->want_kind != NULL && finding.score == 0) return; long score = finding.score * task->weight; const char* base = strrchr(path, '/'); base = base != NULL ? base + 1 : path; if (filename_matches(base, search->query)) { score += WEIGHT_FILENAME; if (finding.symbol[0] == '\0') (void) snprintf(finding.symbol, sizeof(finding.symbol), "%s", search->query); } if (score > 0) (void) match_list_add(matches, path, score, finding.symbol, finding.kind); } struct Worker { const struct Task* tasks; size_t start; size_t end; const struct Search* search; struct MatchList matches; }; static void* worker_main(void* argument) { struct Worker* worker = argument; for (size_t index = worker->start; index < worker->end; index++) scan_task(&worker->tasks[index], worker->search, &worker->matches); return NULL; } static size_t worker_count(size_t task_count, size_t requested) { size_t count = requested == 0 ? WORKER_THREADS : requested; if (count > MAX_WORKER_THREADS) count = MAX_WORKER_THREADS; if (count > task_count) count = task_count; return count == 0 ? 1 : count; } static void scan_tasks(const struct TaskList* tasks, const struct Search* search, struct MatchList* matches, size_t requested) { size_t threads = worker_count(tasks->count, requested); struct Worker* workers = calloc(threads, sizeof(struct Worker)); pthread_t* handles = calloc(threads, sizeof(pthread_t)); if (workers == NULL || handles == NULL) { free(workers); free(handles); return; } size_t per_thread = tasks->count / threads; size_t remainder = tasks->count % threads; size_t offset = 0; for (size_t index = 0; index < threads; index++) { size_t chunk = per_thread + (index < remainder ? 1 : 0); workers[index].tasks = tasks->items; workers[index].start = offset; workers[index].end = offset + chunk; workers[index].search = search; offset += chunk; (void) pthread_create(&handles[index], NULL, worker_main, &workers[index]); } for (size_t index = 0; index < threads; index++) { (void) pthread_join(handles[index], NULL); struct MatchList* partial = &workers[index].matches; for (size_t item = 0; item < partial->count; item++) (void) match_list_add(matches, partial->items[item].path, partial->items[item].score, partial->items[item].symbol, partial->items[item].kind); match_list_free(partial); } free(workers); free(handles); } static void dedupe_matches(struct MatchList* matches) { if (matches->count == 0) return; char (*labels)[256] = malloc(matches->count * sizeof(*labels)); if (labels == NULL) return; size_t write = 0; for (size_t read = 0; read < matches->count; read++) { char label[256]; page_label(matches->items[read].path, label, sizeof(label)); bool seen = false; for (size_t index = 0; index < write; index++) if (strcmp(labels[index], label) == 0) { seen = true; break; } if (seen) match_free(&matches->items[read]); else { (void) snprintf(labels[write], sizeof(labels[write]), "%s", label); matches->items[write] = matches->items[read]; write++; } } free(labels); matches->count = write; } static bool page_man_args(const char* path, char* root, size_t root_size, char* section, size_t section_size, char* name, size_t name_size) { char work[4096]; (void) snprintf(work, sizeof(work), "%s", path); strip_compression_suffix(work); char* name_slash = strrchr(work, '/'); if (name_slash == NULL) return false; *name_slash = '\0'; char* page_name = name_slash + 1; char* section_dot = strrchr(page_name, '.'); if (section_dot != NULL) *section_dot = '\0'; char* root_slash = strrchr(work, '/'); if (root_slash == NULL) return false; *root_slash = '\0'; char* page_section = root_slash + 1; if (strncmp(page_section, "man", 3) == 0) page_section += 3; (void) snprintf(root, root_size, "%s", work); (void) snprintf(section, section_size, "%s", page_section); (void) snprintf(name, name_size, "%s", page_name); return true; } static int open_page(const char* path) { char root[4096]; char section[64]; char name[256]; if (!page_man_args(path, root, sizeof(root), section, sizeof(section), name, sizeof(name))) return 1; (void) execlp("man", "man", "-M", root, section, name, (char*) NULL); (void) fprintf(stderr, "jafm: failed to run man\n"); return 1; } static void load_preview(struct Match* match, const char* query) { if (match->preview_loaded) return; match->preview_loaded = true; char root[4096]; char section[64]; char name[256]; if (!page_man_args(match->path, root, sizeof(root), section, sizeof(section), name, sizeof(name))) return; char command[8192]; (void) snprintf(command, sizeof(command), "man -M '%s' '%s' '%s' 2>/dev/null | col -b", root, section, name); FILE* pipe = popen(command, "r"); if (pipe == NULL) return; size_t capacity = 0; bool found_start = false; char line[4096]; while (fgets(line, sizeof(line), pipe) != NULL) { line[strcspn(line, "\n")] = '\0'; if (match->preview_count == capacity) { size_t new_capacity = capacity == 0 ? 64 : capacity * 2; char** grown = realloc(match->preview, new_capacity * sizeof(char*)); if (grown == NULL) break; match->preview = grown; capacity = new_capacity; } char* copy = strdup(line); if (copy == NULL) break; if (!found_start && strstr(line, query) != NULL) { match->preview_start = match->preview_count; found_start = true; } match->preview[match->preview_count] = copy; match->preview_count++; } (void) pclose(pipe); } static bool is_valid_kind(const char* kind) { static const char* kinds[] = {"struct", "union", "enum", "typedef", "function"}; for (size_t index = 0; index < sizeof(kinds) / sizeof(kinds[0]); index++) if (strcmp(kind, kinds[index]) == 0) return true; return false; } static void print_usage(FILE* stream, const char* program) { (void) fprintf(stream, "usage: %s [-1] [-l] [-e] [-t type] [-j threads] \n", program); (void) fprintf(stream, " %s [-v | -h]\n", program); } static void print_help(const char* program) { (void) printf("jafm %s - %s\n\n", JAFM_VERSION, JAFM_TAGLINE); (void) printf("greps your whole manpath for a symbol and opens the page that actually\n" "documents it, because `man ` gives up way too easily.\n\n"); print_usage(stdout, program); (void) printf("\noptions:\n" " -1, --first open the best match without asking\n" " -l, --list just print what it found, open nothing\n" " -e, --exact match the symbol as a whole word only\n" " -t, --type KIND only show matches where the symbol is a KIND\n" " (struct, union, enum, typedef, function)\n" " -j, --jobs N scan with N worker threads (default %d)\n" " -v, --version print version and bail\n" " -h, --help print this and bail\n" "\nby %s\n", WORKER_THREADS, JAFM_AUTHOR); } static void print_match_list(const struct MatchList* matches, bool numbered) { for (size_t index = 0; index < matches->count; index++) { char label[256]; page_label(matches->items[index].path, label, sizeof(label)); const char* symbol = matches->items[index].symbol; const char* kind = matches->items[index].kind; if (numbered) (void) printf("%zu) ", index + 1); if (symbol[0] != '\0') (void) printf("%s %s (%s)\n", label, symbol, kind); else (void) printf("%s\n", label); } } int main(int argc, char** argv) { static const struct option long_options[] = { { "first", no_argument, NULL, '1'}, { "list", no_argument, NULL, 'l'}, { "exact", no_argument, NULL, 'e'}, { "type", required_argument, NULL, 't'}, { "jobs", required_argument, NULL, 'j'}, { "version", no_argument, NULL, 'v'}, { "help", no_argument, NULL, 'h'}, { NULL, 0, NULL, 0 } }; size_t requested_threads = 0; bool open_first = false; bool list_only = false; bool exact = false; const char* want_kind = NULL; for (int option = getopt_long(argc, argv, "1let:j:vh", long_options, NULL); option != -1; option = getopt_long(argc, argv, "1let:j:vh", long_options, NULL)) switch (option) { case '1': open_first = true; break; case 'l': list_only = true; break; case 'e': exact = true; break; case 't': if (!is_valid_kind(optarg)) { (void) fprintf(stderr, "jafm: invalid type: %s " "(struct, union, enum, typedef, function)\n", optarg); return 1; } want_kind = optarg; break; case 'j': { char* end = NULL; long value = strtol(optarg, &end, 10); if (end == optarg || *end != '\0' || value < 1) { (void) fprintf(stderr, "jafm: invalid thread count: %s\n", optarg); return 1; } requested_threads = (size_t) value; break; } case 'v': (void) printf("jafm %s - %s\n", JAFM_VERSION, JAFM_TAGLINE); (void) printf("by %s\n", JAFM_AUTHOR); return 0; case 'h': print_help(argv[0]); return 0; default: print_usage(stderr, argv[0]); return 1; } if (optind != argc - 1) { print_usage(stderr, argv[0]); return 1; } const char* query = argv[optind]; FILE* manpath_pipe = popen("manpath 2>/dev/null", "r"); if (manpath_pipe == NULL) { (void) fprintf(stderr, "jafm: failed to run manpath\n"); return 1; } char buffer[4096]; if (fgets(buffer, sizeof(buffer), manpath_pipe) == NULL) { (void) fprintf(stderr, "jafm: failed to read manpath\n"); (void) pclose(manpath_pipe); return 1; } (void) pclose(manpath_pipe); buffer[strcspn(buffer, "\n")] = '\0'; struct TaskList tasks = {0}; char* save_pointer = NULL; for (const char* directory = strtok_r(buffer, ":", &save_pointer); directory != NULL; directory = strtok_r(NULL, ":", &save_pointer)) collect_sections(directory, &tasks); struct Search search = {query, exact, want_kind}; struct MatchList matches = {0}; scan_tasks(&tasks, &search, &matches, requested_threads); task_list_free(&tasks); if (matches.count == 0) { (void) fprintf(stderr, "jafm: no matches for %s\n", query); match_list_free(&matches); return 1; } qsort(matches.items, matches.count, sizeof(struct Match), compare_matches); dedupe_matches(&matches); if (list_only) { print_match_list(&matches, false); match_list_free(&matches); return 0; } size_t selected = 0; bool has_selection; if (open_first || matches.count == 1) has_selection = true; else if (isatty(STDIN_FILENO)) has_selection = select_interactive(&matches, query, &selected); else { print_match_list(&matches, true); has_selection = read_selection(matches.count, &selected); } if (!has_selection) { (void) fprintf(stderr, "jafm: no selection\n"); match_list_free(&matches); return 1; } char chosen[4096]; (void) snprintf(chosen, sizeof(chosen), "%s", matches.items[selected].path); match_list_free(&matches); return open_page(chosen); }