all repos

viye @ 66ef839

my shot at reimplementing xiki

viye/internal/plumbing/tldr/tldr.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add tldr tool, 1 month ago
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
}