diff options
| author | hachem <im@hachem.wtf> | 2026-09-02 14:19:01 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-02 14:19:01 +0200 |
| commit | e12f6224e317dd666e3a2721f57b3c5b7517d304 (patch) | |
| tree | 5b74e3816b608b199e06ac8caf7e168a1e39b283 /jafm.c | |
| parent | d246c66681768c9217ff29137dadaabd69106592 (diff) | |
feat: open with man
Diffstat (limited to 'jafm.c')
| -rw-r--r-- | jafm.c | 539 |
1 files changed, 539 insertions, 0 deletions
@@ -0,0 +1,539 @@ +#define _POSIX_C_SOURCE 200809L + +#include <dirent.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> +#include <zlib.h> + +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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 <symbol>\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); +} |
