#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #define LINE_SIZE 8192 #define MAX_VISIBLE 12 enum Rank { RANK_FILENAME, RANK_NAME, RANK_BODY, RANK_NONE }; struct Match { char* path; enum Rank rank; }; struct MatchList { struct Match* items; size_t count; size_t capacity; }; static bool match_list_add(struct MatchList* matches, const char* path, enum Rank rank) { 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; matches->items[matches->count].path = copy; matches->items[matches->count].rank = rank; matches->count++; return true; } static void match_list_free(struct MatchList* matches) { for (size_t index = 0; index < matches->count; index++) free(matches->items[index].path); 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->rank != second->rank) return first->rank < second->rank ? -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 bool is_section_header(const char* line) { return strncmp(line, ".SH", 3) == 0 || strncmp(line, ".Sh", 3) == 0; } static void classify_line(const char* line, const char* query, bool* in_name, bool* found_name, bool* found_body) { if (is_section_header(line)) *in_name = strstr(line, "NAME") != NULL; if (strstr(line, query) == NULL) return; *found_body = true; if (*in_name || strncmp(line, ".Nm", 3) == 0) *found_name = true; } static enum Rank rank_from(bool found_name, bool found_body) { if (found_name) return RANK_NAME; if (found_body) return RANK_BODY; return RANK_NONE; } static enum Rank plain_page_rank(const char* path, const char* query) { FILE* page = fopen(path, "r"); if (page == NULL) return RANK_NONE; char line[LINE_SIZE]; bool in_name = false; bool found_name = false; bool found_body = false; while (fgets(line, sizeof(line), page) != NULL) classify_line(line, query, &in_name, &found_name, &found_body); (void) fclose(page); return rank_from(found_name, found_body); } static enum Rank gzip_page_rank(const char* path, const char* query) { gzFile page = gzopen(path, "r"); if (page == NULL) return RANK_NONE; char line[LINE_SIZE]; bool in_name = false; bool found_name = false; bool found_body = false; while (gzgets(page, line, LINE_SIZE) != NULL) classify_line(line, query, &in_name, &found_name, &found_body); (void) gzclose(page); return rank_from(found_name, found_body); } static enum Rank page_rank(const char* path, const char* query) { if (ends_with(path, ".gz")) return gzip_page_rank(path, query); return plain_page_rank(path, query); } static bool filename_matches(const char* name, const char* query) { char base[256]; (void) snprintf(base, sizeof(base), "%s", name); size_t length = strlen(base); if (length > 3 && strcmp(base + length - 3, ".gz") == 0) base[length - 3] = '\0'; char* section_dot = strrchr(base, '.'); if (section_dot != NULL) *section_dot = '\0'; return strcmp(base, query) == 0; } static struct termios original_termios; static void disable_raw_mode(void) { (void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios); } 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; 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); size_t length = strlen(base); if (length > 3 && strcmp(base + length - 3, ".gz") == 0) base[length - 3] = '\0'; 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 size_t render_menu(const struct MatchList* matches, size_t highlight, size_t top, size_t visible, size_t cols, size_t previous_height) { if (previous_height > 0) (void) printf("\x1b[%zuA", previous_height); size_t height = 0; (void) printf("\r\x1b[2K \x1b[1;36mjafm\x1b[0m \x1b[2m%zu matches\x1b[0m\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 index = top; index < end; index++) { char label[256]; page_label(matches->items[index].path, label, sizeof(label)); if (index == highlight) (void) printf("\x1b[2K \x1b[36m\xe2\x9d\xaf\x1b[0m \x1b[1m%s\x1b[0m\r\n", label); else (void) printf("\x1b[2K \x1b[2m%s\x1b[0m\r\n", label); height++; } const char* path = matches->items[highlight].path; size_t path_length = strlen(path); size_t budget = cols > 4 ? cols - 4 : 0; bool truncated = path_length > budget; const char* shown = truncated ? path + (path_length - budget + 1) : path; (void) printf("\x1b[2K\r\n"); height++; (void) printf("\x1b[2K \x1b[2m%s%s\x1b[0m\r\n", truncated ? "\xe2\x80\xa6" : "", shown); height++; (void) printf("\x1b[2K \x1b[2mup/down move enter open q quit [%zu/%zu]\x1b[0m\r\n", highlight + 1, matches->count); height++; (void) printf("\x1b[J"); return height; } static bool select_interactive(const struct MatchList* matches, 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; previous_height = render_menu(matches, highlight, top, visible, cols, previous_height); 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] == '[') { 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); 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; } static void list_pages(const char* section, const char* query, struct MatchList* matches) { DIR* section_stream = opendir(section); if (section_stream == NULL) return; 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); if (filename_matches(entry->d_name, query)) (void) match_list_add(matches, path, RANK_FILENAME); else { enum Rank rank = page_rank(path, query); if (rank != RANK_NONE) (void) match_list_add(matches, path, rank); } } (void) closedir(section_stream); } static void list_sections(const char* directory, const char* query, struct MatchList* matches) { 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); list_pages(section, query, matches); } (void) closedir(directory_stream); } static int open_page(const char* path) { char work[4096]; (void) snprintf(work, sizeof(work), "%s", path); size_t length = strlen(work); if (length > 3 && strcmp(work + length - 3, ".gz") == 0) work[length - 3] = '\0'; char* name_slash = strrchr(work, '/'); if (name_slash == NULL) return 1; *name_slash = '\0'; char* name = name_slash + 1; char* section_dot = strrchr(name, '.'); if (section_dot != NULL) *section_dot = '\0'; char* root_slash = strrchr(work, '/'); if (root_slash == NULL) return 1; *root_slash = '\0'; char* root = work; char* section = root_slash + 1; if (strncmp(section, "man", 3) == 0) section += 3; (void) execlp("man", "man", "-M", root, section, name, (char*) NULL); (void) fprintf(stderr, "jafm: failed to run man\n"); return 1; } int main(int argc, char** argv) { if (argc != 2) { (void) fprintf(stderr, "usage: %s \n", argv[0]); return 1; } const char* query = argv[1]; FILE* manpath_pipe = popen("manpath", "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 MatchList matches = {0}; char* save_pointer = NULL; for (const char* directory = strtok_r(buffer, ":", &save_pointer); directory != NULL; directory = strtok_r(NULL, ":", &save_pointer)) list_sections(directory, query, &matches); 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); size_t selected = 0; bool has_selection; if (isatty(STDIN_FILENO)) has_selection = select_interactive(&matches, &selected); else { for (size_t index = 0; index < matches.count; index++) (void) printf("%zu) %s\n", index + 1, matches.items[index].path); 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); }