all repos

viye @ fbda676

my shot at reimplementing xiki
2 files changed, 130 insertions(+), 0 deletions(-)
add tldr tool
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
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 {