2 files changed,
130 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-28 16:41:32 +0300
Authored at:
2026-08-28 16:40:59 +0300
Change ID:
sppqxxqkrxzyrspvklsyorskysumntvw
Parent:
61f333d
jump to
| A | internal/plumbing/tldr/tldr.go |
| M | main.go |
A
internal/plumbing/tldr/tldr.go
··· 1 +package tldr 2 + 3 +import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "os" 10 + "path/filepath" 11 + "runtime" 12 + "strings" 13 + 14 + "github.com/olexsmir/viye/internal/viye" 15 +) 16 + 17 +type Tool struct{} 18 + 19 +func (Tool) Name() string { return "tldr" } 20 +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "tldr" } 21 +func (Tool) Execute(c *viye.Context) (string, error) { 22 + if len(c.Args) == 0 { 23 + return "", fmt.Errorf("usage: tldr <command> [subcommand...]") 24 + } 25 + content, err := page(c.Args) 26 + if err != nil { 27 + return "", err 28 + } 29 + return viye.FormatOutput(render(content)), nil 30 +} 31 + 32 +const baseURL = "https://raw.githubusercontent.com/tldr-pages/tldr/main" 33 + 34 +func page(args []string) ([]byte, error) { 35 + cache := filepath.Join(mustUserCacheDir(), "tealdeer", "tldr-pages", "pages.en") 36 + var remote []string 37 + for _, dir := range []string{"common", platformDir()} { 38 + for _, name := range names(args) { 39 + if b, err := os.ReadFile(filepath.Join(cache, dir, name)); err == nil { 40 + return b, nil 41 + } 42 + remote = append(remote, baseURL+"/pages.en/"+dir+"/"+name) 43 + } 44 + } 45 + for _, url := range remote { 46 + if body, err := fetch(url); err == nil { 47 + return body, nil 48 + } else if !errors.Is(err, errNotFound) { 49 + return nil, err 50 + } 51 + } 52 + return nil, fmt.Errorf("no tldr found for %q", strings.Join(args, " ")) 53 +} 54 + 55 +var errNotFound = errors.New("page not found") 56 + 57 +func fetch(url string) ([]byte, error) { 58 + ctx, cancel := context.WithTimeout(context.Background(), viye.Timeout) 59 + defer cancel() 60 + 61 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 62 + if err != nil { 63 + return nil, err 64 + } 65 + resp, err := http.DefaultClient.Do(req) 66 + if err != nil { 67 + return nil, err 68 + } 69 + defer resp.Body.Close() 70 + if resp.StatusCode == http.StatusNotFound { 71 + return nil, errNotFound 72 + } 73 + if resp.StatusCode != http.StatusOK { 74 + return nil, fmt.Errorf("failed to fetch tldr page: %s", resp.Status) 75 + } 76 + return io.ReadAll(resp.Body) 77 +} 78 + 79 +func names(args []string) []string { 80 + n := []string{strings.Join(args, "-") + ".md"} 81 + if len(args) > 1 { 82 + n = append(n, args[0]+".md") 83 + } 84 + return n 85 +} 86 + 87 +// platformDir maps the running OS to its tealdeer page directory. 88 +func platformDir() string { 89 + switch runtime.GOOS { 90 + case "darwin": 91 + return "osx" 92 + default: 93 + return runtime.GOOS 94 + } 95 +} 96 + 97 +func mustUserCacheDir() string { 98 + dir, err := os.UserCacheDir() 99 + if err != nil { 100 + panic(fmt.Sprintf("failed to get user cache dir: %v", err)) 101 + } 102 + return dir 103 +} 104 + 105 +// render strips tldr markdown to plain text: "# name" -> "name", "> text" -> 106 +// "text", "`code`" -> 2-space-indented code directly under its description. 107 +func render(content []byte) string { 108 + var out []string 109 + for line := range strings.SplitSeq(string(content), "\n") { 110 + switch { 111 + case strings.HasPrefix(line, "# "): 112 + out = append(out, strings.TrimPrefix(line, "# ")) 113 + case strings.HasPrefix(line, "> "): 114 + out = append(out, strings.TrimPrefix(line, "> ")) 115 + case strings.HasPrefix(line, "`") && strings.HasSuffix(line, "`"): 116 + if out[len(out)-1] == "" { // close the gap between desc and code 117 + out = out[:len(out)-1] 118 + } 119 + out = append(out, " "+strings.Trim(line, "`")) 120 + default: 121 + out = append(out, line) 122 + } 123 + } 124 + for len(out) > 0 && out[len(out)-1] == "" { // drop trailing blank 125 + out = out[:len(out)-1] 126 + } 127 + return strings.Join(out, "\n") 128 +}
M
main.go
··· 15 15 "github.com/olexsmir/viye/internal/plumbing/makefile" 16 16 "github.com/olexsmir/viye/internal/plumbing/neofetch" 17 17 "github.com/olexsmir/viye/internal/plumbing/shell" 18 + "github.com/olexsmir/viye/internal/plumbing/tldr" 18 19 "github.com/olexsmir/viye/internal/plumbing/url" 19 20 "github.com/olexsmir/viye/internal/plumbing/weather" 20 21 "github.com/olexsmir/viye/internal/viye" ··· 35 36 v.Register(&neofetch.Tool{}) 36 37 v.Register(&makefile.Tool{}) 37 38 v.Register(&curl.Tool{}) 39 + v.Register(&tldr.Tool{}) 38 40 v.Register(&weather.Tool{}) 39 41 40 42 if err := v.Run(os.Stdout, os.Args); err != nil {