├── .build.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── config.h ├── screenshot.png └── tldr.c /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - build-base 4 | - curl-dev 5 | - libarchive-dev 6 | tasks: 7 | - build: make -C tinytldr 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{c,h}] 9 | indent_style = tab 10 | indent_size = 8 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tldr 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ivan Kovmir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= cc 2 | 3 | LIBNOTIFY_LIBS = `pkg-config --libs libarchive libcurl` 4 | LIBNOTIFY_INCS = `pkg-config --cflags libarchive libcurl` 5 | 6 | LIBS += $(LIBNOTIFY_LIBS) 7 | INCS += $(LIBNOTIFY_INCS) 8 | 9 | CFLAGS += -std=c99 10 | CFLAGS += -pedantic 11 | CFLAGS += -Wall 12 | CFLAGS += -Wextra 13 | CFLAGS += -Wcast-align 14 | CFLAGS += -Wstrict-prototypes 15 | CFLAGS += -Wundef 16 | CFLAGS += -Wno-format-truncation 17 | 18 | CFLAGS += -D_XOPEN_SOURCE=500 19 | 20 | CFLAGS += $(INCS) 21 | LDFLAGS += $(LIBS) 22 | 23 | PREFIX ?= /usr/local 24 | PROJECT = tldr 25 | 26 | INSTALL ?= install 27 | STRIP ?= strip 28 | 29 | all: 30 | $(CC) $(CFLAGS) -O3 $(PROJECT).c $(LDFLAGS) -o $(PROJECT) 31 | 32 | debug: 33 | $(CC) $(CFLAGS) -g $(PROJECT).c $(LDFLAGS) -o $(PROJECT) 34 | 35 | gdb: debug 36 | gdb ./$(PROJECT) 37 | 38 | memcheck: debug 39 | valgrind --leak-check=yes ./$(PROJECT) 40 | 41 | memcheck_v: debug 42 | valgrind --leak-check=yes -v ./$(PROJECT) 43 | 44 | memcheck_full: debug 45 | valgrind --leak-check=full --show-leak-kinds=all ./$(PROJECT) 46 | 47 | clean: 48 | rm -f ./$(PROJECT) 49 | 50 | strip: 51 | $(STRIP) ./$(PROJECT) 52 | 53 | install: 54 | mkdir -p "$(DESTDIR)$(PREFIX)/bin" 55 | $(INSTALL) ./$(PROJECT) "$(DESTDIR)$(PREFIX)/bin/$(PROJECT)" 56 | 57 | uninstall: 58 | rm -f "$(DESTDIR)$(PREFIX)/bin/$(PROJECT)" 59 | rmdir --ignore-fail-on-non-empty "$(DESTDIR)$(PREFIX)/bin" 60 | 61 | .PHONY: all debug gdb memcheck memcheck_v memcheck_full clean strip install uninstall 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REPOSITORY MIGRATION NOTICE 2 | 3 | Migrated to [SourceHut](https://git.sr.ht/~kovmir/tinytldr). 4 | 5 | # tinytldr 6 | 7 | [![tinytldr status](https://builds.sr.ht/~kovmir/tinytldr.svg)](https://builds.sr.ht/~kovmir/tinytldr?) 8 | 9 | Minimalist [tldr][1] command line client, written in plain C99. 10 | 11 | # PREVIEW 12 | 13 | ![screenshot](screenshot.png) 14 | 15 | *[srcery][2] - terminal colorscheme on the screenshot.* 16 | 17 | # INSTALL 18 | 19 | ## Package 20 | 21 | * [Slackware][3] 22 | 23 | ## Compile from source 24 | 25 | Satisfy the [dependencies](#dependencies) first, and then: 26 | 27 | ```bash 28 | git clone https://github.com/kovmir/tinytldr 29 | cd tinytldr 30 | # Optional: Adjust ./config.h to your linking. 31 | make 32 | sudo make install 33 | ``` 34 | 35 | # USAGE 36 | 37 | ```bash 38 | tldr -u # Fetch or update pages. 39 | tldr cd # View 'cd' page. 40 | tldr windows/scoop # Or one could specify a platform. 41 | ``` 42 | 43 | # DEPENDENCIES 44 | 45 | * [GNU Make][4] 46 | * [pkg-config][5] 47 | * [GCC][6] or [Clang][7] 48 | * [libarchive][8] 49 | * [libcurl][9] 50 | 51 | # SUPPORTED OPERATING SYSTEMS 52 | 53 | * Linux 54 | * BSD 55 | * M$ Windows 56 | 57 | # FAQ 58 | 59 | **Q: Can I use it to display my personal pages?** 60 | 61 | A: Yes, you can. 62 | 63 | ```bash 64 | TLDR_PAGES="$HOME/.local/share/tinytldr/pages" 65 | mkdir "$TLDR_PAGES/mypages" 66 | echo '# My custom page' > "$TLDR_PAGES/mypages/testpage.md" 67 | tldr -u 68 | tldr testpage 69 | ``` 70 | 71 | # CREDITS 72 | 73 | Thanks [@bilditup1](https://github.com/bilditup1) for Windows support. 74 | 75 | # CONTRIBUTING 76 | 77 | When submitting PRs, please maintain the [coding style][11] used for the 78 | project. 79 | 80 | [1]: https://tldr.sh/ 81 | [2]: https://srcery.sh/ 82 | [3]: https://slackbuilds.org/repository/15.0/misc/tinytldr/?search=tinytldr 83 | [4]: https://www.gnu.org/software/make/ 84 | [5]: https://gitlab.freedesktop.org/pkg-config/pkg-config 85 | [6]: https://gcc.gnu.org/ 86 | [7]: https://clang.llvm.org/ 87 | [8]: https://www.libarchive.org/ 88 | [9]: https://curl.se/libcurl/ 89 | [10]: https://github.com/kovmir/tinytldr/issues/6#issuecomment-1884332215 90 | [11]: https://suckless.org/coding_style/ 91 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | /* URL to download man pages. */ 4 | static const char *PAGES_URL = "https://codeload.github.com/tldr-pages/tldr/zip/main"; 5 | 6 | /* Path to store man pages relative to $HOME. */ 7 | static const char *PAGES_PATH = "/.local/share/tinytldr"; 8 | 9 | /* Pages language, uncomment ONE of the following. */ 10 | static const char *PAGES_LANG = "/pages"; /* English */ 11 | /* static const char *PAGES_LANG = "/pages.de"; */ /* German */ 12 | /* static const char *PAGES_LANG = "/pages.es"; */ /* Spanish */ 13 | /* static const char *PAGES_LANG = "/pages.fr"; */ /* French */ 14 | /* static const char *PAGES_LANG = "/pages.hbs"; */ /* Serbo-Croatian */ 15 | /* static const char *PAGES_LANG = "/pages.it"; */ /* Italian */ 16 | /* static const char *PAGES_LANG = "/pages.ja"; */ /* Japanese */ 17 | /* static const char *PAGES_LANG = "/pages.ko"; */ /* Korean */ 18 | /* static const char *PAGES_LANG = "/pages.pt_BR"; */ /* Portuguese: BR */ 19 | /* static const char *PAGES_LANG = "/pages.pt_PT"; */ /* Portuguese: PT */ 20 | /* static const char *PAGES_LANG = "/pages.ta"; */ /* Tamil */ 21 | /* static const char *PAGES_LANG = "/pages.zh"; */ /* Chinese */ 22 | 23 | /* Colors and styling. */ 24 | static const char *HEADING_STYLE = "\033[31m"; 25 | static const char *SUBHEADING_STYLE = "\033[22;4m"; 26 | static const char *COMMAND_DESC_STYLE = "\033[22;32m"; 27 | static const char *COMMAND_STYLE = "\033[1m"; 28 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovmir/tinytldr/8f8bcbb9591274f3049b5efdcdc79a1ec44658c3/screenshot.png -------------------------------------------------------------------------------- /tldr.c: -------------------------------------------------------------------------------- 1 | /* MIT License 2 | * Copyright 2024 Ivan Kovmir */ 3 | 4 | /* Includes */ 5 | /* cURL must be included before libarchive in order to avoid a compiler 6 | * warning on Windows regarding the Winsock2 library. */ 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | /* libarchive */ 16 | #include 17 | #include 18 | 19 | /* Constants and Macros */ 20 | #define BUF_SIZE 1024 21 | #define D_NAME entry->d_name 22 | #define ENABLE_WIN_VT100_OUT 7 23 | #define NULL_TERMINATE(arr, size) (arr[(size)-1] = 0) 24 | 25 | /* Function prototypes */ 26 | /* Print error message and terminate the execution */ 27 | static void error_terminate(const char *msg, const char *details); 28 | /* Prints instructions on how to use the program. */ 29 | static void print_usage(void); 30 | /* Downloads pages. */ 31 | static void fetch_pages(void); 32 | /* Extracts pages and put them in place. */ 33 | static void extract_pages(void); 34 | /* Creates index file of all pages. */ 35 | static void index_pages(void); 36 | static int nftw_callback(const char *path, const struct stat *sb, 37 | int typeflag, struct FTW *ftwbuf); 38 | /* Prints all page names. */ 39 | static void list_pages(void); 40 | /* Returns a file path to a given page. */ 41 | static char *find_page(const char *page_name); 42 | /* Enables VT100 mode on Win10 1503+ ConHost & wt+mintty; else, empty. */ 43 | static void setup_console(void); 44 | /* Restores previous mode on Win10 1503+ ConHost & wt+mintty; else, empty. */ 45 | static void restore_console(void); 46 | /* Prints a given page. */ 47 | static void display_page(const char *dest_path); 48 | /* Opens index file performing all the necessary checking. */ 49 | static FILE *open_index(const char *mode); 50 | 51 | /* Save locations, styling, and other settings are set via config.h. */ 52 | #include "config.h" 53 | /* Resets console styling back to default (usually white-on-black), 54 | and clears rest of current line for consistency on Windows. */ 55 | static const char *RESET_STYLING = "\033[0m\033[0K"; 56 | /* Index file to hold available page names. */ 57 | static FILE *tldr_index; 58 | /* Path to download the tldr archive to. */ 59 | static char zip_path[BUF_SIZE]; 60 | 61 | inline void 62 | error_terminate(const char *msg, const char *details) 63 | { 64 | fprintf(stderr, "%s; details: %s.\n", msg, details? details : "none"); 65 | exit(1); 66 | } 67 | 68 | inline void 69 | print_usage(void) 70 | { 71 | printf("USAGE: tldr [options] <[platform/]command>\n\n" 72 | "[options]\n" 73 | "\t-h:\tthis help overview\n" 74 | "\t-l:\tshow all available pages\n" 75 | "\t-u:\tfetch lastest copies of man pages\n\n" 76 | "[platform]\n" 77 | "\tandroid\n" 78 | "\tcommon\n" 79 | "\tindex\n" 80 | "\tlinux\n" 81 | "\tosx\n" 82 | "\tsunos\n" 83 | "\twindows\n\n" 84 | "\n" 85 | "\tShow examples for this command\n"); 86 | } 87 | 88 | void 89 | fetch_pages(void) 90 | { 91 | CURL *ceh; /* cURL easy handle. */ 92 | CURLcode cres; /* cURL operation result. */ 93 | char err_curl[CURL_ERROR_SIZE]; /* Curl error message buffer. */ 94 | FILE *tldr_archive; /* File to download to. */ 95 | 96 | if (getenv("TEMP") != NULL) /* Defined by Windows. */ 97 | strcpy(zip_path, getenv("TEMP")); 98 | else if (getenv("TEMPDIR") != NULL) /* Can be defined by *nix users. */ 99 | strcpy(zip_path, getenv("TEMPDIR")); 100 | else /* If neither defined, presume default *nix tmp path. */ 101 | strcpy(zip_path, "/tmp"); 102 | strcat(zip_path, "/tldr_pages.zip"); 103 | NULL_TERMINATE(zip_path, BUF_SIZE); 104 | 105 | /* Write in binary mode to avoid mangling with CRLFs in Windows. */ 106 | tldr_archive = fopen(zip_path, "wb"); 107 | if (!tldr_archive) 108 | error_terminate("Failed to create a temporary file", NULL); 109 | 110 | curl_global_init(CURL_GLOBAL_ALL); 111 | ceh = curl_easy_init(); 112 | curl_easy_setopt(ceh, CURLOPT_WRITEDATA, tldr_archive); 113 | curl_easy_setopt(ceh, CURLOPT_URL, PAGES_URL); 114 | curl_easy_setopt(ceh, CURLOPT_ERRORBUFFER, err_curl); 115 | 116 | cres = curl_easy_perform(ceh); 117 | 118 | curl_easy_cleanup(ceh); 119 | curl_global_cleanup(); 120 | fclose(tldr_archive); 121 | if (cres != CURLE_OK) 122 | error_terminate("Failed to fetch pages", err_curl); 123 | } 124 | 125 | void 126 | extract_pages(void) 127 | { 128 | char src_path[BUF_SIZE]; /* Path within archive to extract from. */ 129 | char dest_path[BUF_SIZE]; /* Path to save pages to. */ 130 | FILE *tldr_archive; /* Pointer to downloaded zip file. */ 131 | int ares; /* libarchive status. */ 132 | struct archive *ap; 133 | struct archive_entry *aep; 134 | 135 | tldr_archive = fopen(zip_path, "r"); 136 | if (tldr_archive == NULL) 137 | error_terminate("Failed to open the archive", NULL); 138 | 139 | ap = archive_read_new(); 140 | if (ap == NULL) 141 | error_terminate("Failed to archive_read_new()", NULL); 142 | archive_read_support_format_zip(ap); 143 | ares = archive_read_open_FILE(ap, tldr_archive); 144 | if (ares != ARCHIVE_OK) 145 | error_terminate("Failed to archive_read_open_FILE()", 146 | archive_error_string(ap)); 147 | 148 | /* A place inside the archive to extract pages from. */ 149 | snprintf(src_path, BUF_SIZE, "%s%s/", "tldr-main", PAGES_LANG); 150 | 151 | /* Find the folder within the archive to extract from. */ 152 | while (archive_read_next_header(ap, &aep) != ARCHIVE_EOF) 153 | if (!strcmp(archive_entry_pathname(aep), src_path)) 154 | break; 155 | while (archive_read_next_header(ap, &aep) != ARCHIVE_EOF) { 156 | if (strncmp(archive_entry_pathname(aep), 157 | src_path, strlen(src_path))) 158 | break; 159 | 160 | snprintf(dest_path, BUF_SIZE, "%s%s%s", 161 | getenv("HOME"), PAGES_PATH, 162 | strchr(archive_entry_pathname(aep),'/')); 163 | 164 | archive_entry_set_pathname(aep, dest_path); 165 | ares = archive_read_extract(ap, aep, 0); 166 | if (ares != ARCHIVE_OK) 167 | error_terminate("Failed to archive_read_extract()", 168 | archive_error_string(ap)); 169 | } 170 | archive_read_free(ap); 171 | fclose(tldr_archive); 172 | remove(zip_path); 173 | } 174 | 175 | void 176 | index_pages(void) 177 | { 178 | char buf[BUF_SIZE]; 179 | 180 | snprintf(buf, BUF_SIZE, "%s%s%s", 181 | getenv("HOME"), PAGES_PATH, PAGES_LANG); 182 | 183 | tldr_index = open_index("w"); 184 | nftw(buf, nftw_callback, 10, FTW_PHYS); 185 | fclose(tldr_index); 186 | } 187 | 188 | int 189 | nftw_callback(const char *path, const struct stat *sb, 190 | int typeflag, struct FTW *ftwbuf) 191 | { 192 | (void)sb; /* Suppress compiler warnings about unused arguments. */ 193 | (void)ftwbuf; 194 | 195 | if (typeflag != FTW_F) /* Skip everything except files. */ 196 | return 0; 197 | 198 | /* Truncate the full path to include only the filename and last dir. */ 199 | fprintf(tldr_index, "%s\n", 200 | strchr(strstr(path, PAGES_LANG)+1, '/')+1); 201 | return 0; 202 | } 203 | 204 | void 205 | list_pages(void) 206 | { 207 | char buf[BUF_SIZE]; 208 | 209 | tldr_index = open_index("r"); 210 | while(fgets(buf, BUF_SIZE, tldr_index)) { 211 | NULL_TERMINATE(buf, BUF_SIZE); 212 | printf("%s", buf); 213 | } 214 | fclose(tldr_index); 215 | } 216 | 217 | char * 218 | find_page(const char *page_name) 219 | { 220 | static char buf[BUF_SIZE]; 221 | char page_filename[strlen(page_name)+5]; /* for '.md\n\0' */ 222 | 223 | tldr_index = open_index("r"); 224 | snprintf(page_filename, BUF_SIZE, "%s.md\n", page_name); 225 | while(fgets(buf, BUF_SIZE, tldr_index)) { 226 | /* page_name is either 'command' or 'platform/command'. */ 227 | if (strchr(page_name, '/')) { /* platform/command */ 228 | if(!strcmp(page_filename, buf)) { 229 | *strchr(buf, '\n') = '\0'; 230 | fclose(tldr_index); 231 | return buf; 232 | } 233 | } else { /* command */ 234 | if (!strcmp(page_filename, strchr(buf, '/')+1)) { 235 | *strchr(buf, '\n') = '\0'; 236 | fclose(tldr_index); 237 | return buf; 238 | } 239 | } 240 | } 241 | fclose(tldr_index); 242 | return NULL; 243 | } 244 | #ifdef _WIN32 245 | static DWORD outmode_init; /* will be set to initial console mode value. */ 246 | static HANDLE stdout_handle; /* handle for current console session. */ 247 | /* ref: https://docs.microsoft.com/en-us/windows/console/setconsolemode */ 248 | void 249 | setup_console(void) 250 | { 251 | stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); 252 | GetConsoleMode(stdout_handle, &outmode_init); 253 | if (!SetConsoleMode(stdout_handle, ENABLE_WIN_VT100_OUT)){ 254 | error_terminate("\nYou are likely using an older version of Windows," 255 | "\nwhich is not yet compatible with this client.\n", NULL); 256 | } 257 | } 258 | void 259 | restore_console(void) 260 | { /* Error catching would be superfluous here given program flow. */ 261 | SetConsoleMode(stdout_handle, outmode_init); 262 | } 263 | #else 264 | void setup_console(void){} 265 | void restore_console(void){} 266 | #endif 267 | void 268 | display_page(const char *page_name) 269 | { 270 | char buf[BUF_SIZE]; 271 | char *dest_path; 272 | FILE *page; 273 | 274 | dest_path = find_page(page_name); 275 | if (!dest_path) 276 | error_terminate("The page has not been found.", NULL); 277 | 278 | snprintf(buf, BUF_SIZE, "%s%s%s/%s", 279 | getenv("HOME"), PAGES_PATH, PAGES_LANG, dest_path); 280 | 281 | page = fopen(buf, "r"); 282 | setup_console(); /* Enables VT100 processing in Win10 1503+ */ 283 | while (fgets(buf, BUF_SIZE, page)) { 284 | NULL_TERMINATE(buf, BUF_SIZE); 285 | if (!strcmp(buf, "\n")) { 286 | continue; /* Skip empty lines. */ 287 | } 288 | if (buf[0] == '#') { 289 | printf("%s%s%s", HEADING_STYLE, buf, RESET_STYLING); 290 | } 291 | if (buf[0] == '>') { 292 | printf("%s%s%s", SUBHEADING_STYLE, buf, RESET_STYLING); 293 | } 294 | if (buf[0] == '-') { 295 | printf("%s%s%s", COMMAND_DESC_STYLE, buf, RESET_STYLING); 296 | } 297 | if (buf[0] == '`') { 298 | printf("%s%s%s", COMMAND_STYLE, buf, RESET_STYLING); 299 | } 300 | 301 | } 302 | fclose(page); 303 | restore_console(); /* Restores previous console mode in Win10 1503+ */ 304 | } 305 | 306 | FILE * 307 | open_index(const char *mode) 308 | { 309 | char buf[BUF_SIZE]; 310 | FILE *fp; 311 | 312 | snprintf(buf, BUF_SIZE, "%s%s/%s", 313 | getenv("HOME"), PAGES_PATH, "index"); 314 | fp = fopen(buf, mode); 315 | if (!fp) 316 | error_terminate("Failed to open index; " 317 | "you should probably run 'tldr -u'", mode); 318 | return fp; 319 | } 320 | 321 | int 322 | main(int argc, char *argv[]) 323 | { 324 | int opt; 325 | 326 | while ((opt = getopt(argc, argv, "luh")) != -1) { 327 | switch (opt) { 328 | case 'l': 329 | list_pages(); 330 | return 0; 331 | case 'u': 332 | puts("Fetching pages..."); 333 | fetch_pages(); 334 | puts("Extracting pages..."); 335 | extract_pages(); 336 | puts("Indexing pages..."); 337 | index_pages(); 338 | return 0; 339 | case 'h': 340 | print_usage(); 341 | return 0; 342 | default: 343 | print_usage(); 344 | return 1; 345 | } 346 | } 347 | 348 | if (argc != 2) { 349 | print_usage(); 350 | return 1; 351 | } 352 | 353 | display_page(argv[1]); 354 | return 0; 355 | } 356 | --------------------------------------------------------------------------------