hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/jafm.c
diff options
context:
space:
mode:
Diffstat (limited to 'jafm.c')
-rw-r--r--jafm.c539
1 files changed, 539 insertions, 0 deletions
diff --git a/jafm.c b/jafm.c
new file mode 100644
index 0000000..abfb044
--- /dev/null
+++ b/jafm.c
@@ -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);
+}