├── .github
├── ISSUE_TEMPLATE
│ └── bug.yaml
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── cli
├── command.go
├── confirm.go
├── default.go
├── renderer.go
└── suggest.go
├── cmd
└── tldr
│ └── main.go
├── config
├── configurer.go
├── configurer_test.go
├── messages.go
├── source.go
└── source_test.go
├── go.mod
├── go.sum
├── img
├── benchmarks.png
├── screenplay.gif
└── screenshot.png
└── pages
├── page.go
├── page_test.go
└── pages.go
/.github/ISSUE_TEMPLATE/bug.yaml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug
2 | description: File a bug/issue
3 | title: "[BUG]
"
4 | labels: [Bug, Needs Triage]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Is there an existing issue for this?
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - type: textarea
14 | attributes:
15 | label: Current Behavior
16 | description: A concise description of what you're experiencing.
17 | validations:
18 | required: false
19 | - type: textarea
20 | attributes:
21 | label: Expected Behavior
22 | description: A concise description of what you expected to happen.
23 | validations:
24 | required: false
25 | - type: textarea
26 | attributes:
27 | label: Steps To Reproduce
28 | description: Steps to reproduce the behavior.
29 | placeholder: |
30 | 1. In this environment...
31 | 2. With this config...
32 | 3. Run '...'
33 | 4. See error...
34 | validations:
35 | required: false
36 | - type: textarea
37 | attributes:
38 | label: Environment
39 | description: |
40 | examples:
41 | - **OS**: Ubuntu 20.04
42 | - **Terminal emulator**: Alacritty
43 | - **Go version (if compiled)**: 1.17
44 | value: |
45 | - OS:
46 | - Terminal emulator:
47 | - Go version:
48 | render: markdown
49 | validations:
50 | required: false
51 | - type: textarea
52 | attributes:
53 | label: Anything else?
54 | description: |
55 | Links? References? Anything that will give us more context about the issue you are encountering!
56 |
57 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
58 | validations:
59 | required: false
60 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on: [push]
2 | name: CI
3 | jobs:
4 | test:
5 | env:
6 | GOPATH: ${{ github.workspace }}
7 | GO111MODULE: off
8 |
9 | defaults:
10 | run:
11 | working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Install Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ${{ matrix.go-version }}
20 | - name: Checkout Code
21 | uses: actions/checkout@v2
22 | with:
23 | path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
24 | - name: Execute Tests
25 | run: |
26 | go get -d -t ./...
27 | go test ./... -coverprofile=coverage.txt -covermode=atomic
28 | - name: Upload coverage to Codecov
29 | uses: codecov/codecov-action@v3
30 | with:
31 | directory: .
32 | fail_ci_if_error: true
33 | files: coverage.txt
34 | flags: unittests
35 | verbose: true
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 | coverage.txt
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
15 | # Binaries
16 | build/
17 |
18 | # IDE specific files
19 | .vscode
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ibrahim Serdar Acikgoz
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |   [](https://gitter.im/tldrpp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
3 |
4 | # tldr++
5 |
6 | Community driven man pages improved with smart user interaction. **tldr++** seperates itself from any other tldr client with convenient user guidance feature.
7 |
8 | 
9 |
10 | ## Features
11 |
12 | - Fully Interactive (fill the command arguments easily)
13 | - Search from commands to find your desired command (exact + fuzzy search)
14 | - Smart file suggestions (further suggestions will be added)
15 | - Simple implementation
16 | - One of the fastest clients, even fastest (see [Benchmarks](https://github.com/isacikgoz/tldr/wiki/Benchmarks))
17 | - Easy to install. Supports all mainstream OS and platforms (Linux, MacOS, *Windows (*`v1.0` *excluded for a while*)(arm, x86)
18 | - Pure-go (*even contains built-in git*)
19 |
20 | ## Installation
21 |
22 | Refer to [Release Page](https://github.com/isacikgoz/tldr/releases) for binaries.
23 |
24 | Or, you can build from source: (min. **go 1.18** compiler is recommended)
25 |
26 | ```bash
27 | go install github.com/isacikgoz/tldr/cmd/tldr@latest
28 | ```
29 |
30 | ### macOS using brew
31 |
32 | ```bash
33 | brew install isacikgoz/taps/tldr
34 | ```
35 |
36 | ### Windows using scoop
37 |
38 | This is maintained by community and the version is `v0.6.1`. (`v1.0.0` does not have Windows support yet)
39 |
40 | ```powershell
41 | scoop install tldr
42 | ```
43 |
44 | ## Use for different OS
45 |
46 | You can use tldr++ for another OS by setting `TLDR_OS` envrionment to your desired OS such as `linux`, `windows`, `osx` etc.
47 |
48 | Let's say you want to set it to Linux run the following command:
49 |
50 | ```bash
51 | export TLDR_OS=linux
52 | ```
53 |
54 | To make it permenant, you can add the line above to your shell rc file (e.g. `bashrc`, `zshrc` etc.)
55 |
56 | ## Credits
57 |
58 | - [tldr-pages](https://github.com/tldr-pages/tldr)
59 | - [go-prompt](https://github.com/c-bata/go-prompt)
60 | - [fuzzy](https://github.com/sahilm/fuzzy)
61 | - [go-git](https://github.com/src-d/go-git)
62 | - [kingpin](https://github.com/alecthomas/kingpin)
63 | - [color](https://github.com/fatih/color)
64 |
--------------------------------------------------------------------------------
/cli/command.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "os/exec"
9 | "strings"
10 | )
11 |
12 | func runCommand(c string, sudo bool) error {
13 | args := strings.Fields(c)
14 | if sudo {
15 | args = []string{"sudo"}
16 | args = append(args, strings.Fields(c)...)
17 | }
18 | cmd := exec.Command(args[0], args[1:]...)
19 | out, err := cmd.CombinedOutput()
20 | if err != nil {
21 | return err
22 | }
23 | fmt.Fprintf(os.Stdout, "%s", out)
24 | return nil
25 | }
26 |
27 | func pipeCommands(commands []string, sudo bool) error {
28 | cmds := make([]*exec.Cmd, 0)
29 | for _, c := range commands {
30 | args := strings.Fields(strings.TrimSpace(c))
31 | if sudo {
32 | args = append([]string{"sudo"}, args...)
33 | }
34 | cmds = append(cmds, exec.Command(args[0], args[1:]...))
35 | }
36 |
37 | return execute(cmds...)
38 | }
39 |
40 | func execute(stack ...*exec.Cmd) error {
41 | var b bytes.Buffer
42 | var errBuf bytes.Buffer
43 | pipes := make([]*io.PipeWriter, len(stack)-1)
44 | i := 0
45 | for ; i < len(stack)-1; i++ {
46 | reader, writer := io.Pipe()
47 | stack[i].Stdout = writer
48 | stack[i].Stderr = &errBuf
49 | stack[i+1].Stdin = reader
50 | pipes[i] = writer
51 | }
52 | stack[i].Stdout = &b
53 | stack[i].Stderr = &errBuf
54 |
55 | if err := call(stack, pipes); err != nil {
56 | io.Copy(os.Stderr, &errBuf)
57 | return err
58 | }
59 | _, err := io.Copy(os.Stdout, &b)
60 | return err
61 | }
62 |
63 | func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) {
64 | if stack[0].Process == nil {
65 | if err = stack[0].Start(); err != nil {
66 | return err
67 | }
68 | }
69 | if len(stack) > 1 {
70 | if err = stack[1].Start(); err != nil {
71 | return err
72 | }
73 | defer func() {
74 | if err == nil {
75 | pipes[0].Close()
76 | err = call(stack[1:], pipes[1:])
77 | }
78 | }()
79 | }
80 | return stack[0].Wait()
81 | }
82 |
--------------------------------------------------------------------------------
/cli/confirm.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/fatih/color"
10 | )
11 |
12 | // ConfirmCommand asks user for confirmation
13 | func ConfirmCommand(command string) error {
14 | green := color.New(color.FgGreen, color.Bold)
15 | fmt.Print(green.Sprint("? "))
16 |
17 | fmt.Print(command)
18 | fmt.Print(" [Y/n] ")
19 | s := bufio.NewScanner(os.Stdin)
20 | s.Scan()
21 | input := s.Text()
22 | sudo := strings.HasSuffix(input, "!")
23 | if strings.HasPrefix(input, "Y") || strings.HasPrefix(input, "y") || s.Text() == "" {
24 |
25 | cmds := strings.Split(strings.TrimSpace(command), "|")
26 | if len(cmds) >= 2 {
27 | return pipeCommands(cmds, sudo)
28 | }
29 | return runCommand(command, sudo)
30 |
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/cli/default.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/isacikgoz/gitin/prompt"
8 | "github.com/isacikgoz/gitin/term"
9 | "github.com/isacikgoz/tldr/pages"
10 | )
11 |
12 | // DefaultPrompt is the default tldr prompt
13 | type DefaultPrompt struct {
14 | prompt *prompt.Prompt
15 | item interface{}
16 | }
17 |
18 | // NewDefaultPrompt creates a prompt for tldr app
19 | func NewDefaultPrompt(pgs []string, opts *prompt.Options, static, random bool) (*DefaultPrompt, error) {
20 | var page *pages.Page
21 | var err error
22 | if random {
23 | page, err = pages.QueryRandom()
24 | } else if len(pgs) == 0 {
25 | page, err = pages.ReadAll()
26 | } else {
27 | page, err = pages.Read(pgs)
28 | }
29 | if err != nil {
30 | return nil, fmt.Errorf("could not read page: %v", err)
31 | }
32 |
33 | fmt.Printf("%s", page.Desc)
34 | list, err := prompt.NewList(page.Tips, opts.LineSize)
35 | if err != nil {
36 | return nil, fmt.Errorf("could not create list: %v", err)
37 | }
38 | d := &DefaultPrompt{}
39 | p := prompt.Create("", opts, list,
40 | prompt.WithItemRenderer(renderItem),
41 | prompt.WithInformation(information),
42 | prompt.WithSelectionHandler(d.selection),
43 | )
44 | p.SetExitMsg(defaultExitMessage(list))
45 | if static {
46 | if err := printStatic(page.Tips); err != nil {
47 | return nil, err
48 | }
49 | return d, nil
50 | }
51 | d.prompt = p
52 | return d, nil
53 | }
54 |
55 | // Run starts the prompt within
56 | func (d *DefaultPrompt) Run(ctx context.Context) error {
57 | if d.prompt == nil {
58 | return nil
59 | }
60 | return d.prompt.Run(ctx)
61 | }
62 |
63 | // Selection returns the selected item
64 | func (d *DefaultPrompt) Selection() interface{} {
65 | return d.item
66 | }
67 |
68 | // selection implements the prompt.selectionHandlerFunc interface
69 | func (d *DefaultPrompt) selection(item interface{}) error {
70 | d.item = item
71 | var cells [][]term.Cell
72 | cells = append(cells, term.Cprint(""))
73 | cells = append(cells, renderItem(item, nil, false)...)
74 | d.prompt.SetExitMsg(cells)
75 | d.prompt.Stop()
76 | return nil
77 | }
78 |
79 | func defaultExitMessage(l *prompt.SyncList) [][]term.Cell {
80 | var cells [][]term.Cell
81 | cells = append(cells, term.Cprint(""))
82 | items, _ := l.Items()
83 | for _, item := range items {
84 | cells = append(cells, renderItem(item, nil, false)...)
85 | }
86 | return cells
87 | }
88 |
--------------------------------------------------------------------------------
/cli/renderer.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | "github.com/fatih/color"
9 | "github.com/isacikgoz/gitin/term"
10 | "github.com/isacikgoz/tldr/pages"
11 | )
12 |
13 | func renderItem(item interface{}, matches []int, selected bool) [][]term.Cell {
14 | var tipText [][]term.Cell
15 | tip := item.(*pages.Tip)
16 | // start rendering tip text
17 | var line1 []term.Cell
18 | if selected {
19 | line1 = append(line1, term.Cprint("> ", color.FgCyan)...)
20 | } else {
21 | line1 = append(line1, term.Cprint(" ")...)
22 | }
23 | line1 = append(line1, term.Cprint(tip.String(), color.FgHiBlue)...)
24 | // start rendering command template
25 | line2 := term.Cprint(" ", color.FgWhite)
26 | s := fmt.Sprint(tip.Cmd)
27 | start := 0
28 | index := ""
29 | if len(tip.Cmd.Args) == 0 {
30 | line2 = append(line2, term.Cprint(s)...) // in case there is no args
31 | }
32 | for _, arg := range tip.Cmd.Args {
33 | s = strings.Replace(s, "{{"+arg+"}}", arg, 1) // fix the arg
34 | i := strings.Index(s, arg)
35 | if i > start {
36 | cmd := s[start:i] // w/o arg
37 | line2 = append(line2, term.Cprint(cmd)...) // append cmd
38 | line2 = append(line2, term.Cprint(arg, color.FgCyan)...) // append arg
39 | index += cmd + arg // to keep the index of where to start next
40 | start = len(index)
41 | continue
42 | }
43 | // if the arg is at the beginning
44 | line2 = append(line2, term.Cprint(arg, color.FgCyan)...)
45 | cmd := s[len(arg):] // w/o arg
46 | line2 = append(line2, term.Cprint(cmd)...)
47 | }
48 | tipText = append(tipText, line1)
49 | tipText = append(tipText, line2)
50 | return tipText
51 | }
52 |
53 | func information(item interface{}) [][]term.Cell {
54 | i := term.Cprint(" to exit, < / > to search.", color.FgRed)
55 | return [][]term.Cell{i}
56 | }
57 |
58 | func printStatic(tips []*pages.Tip) error {
59 | if err := term.Init(os.Stdin, os.Stdout); err != nil {
60 | return err
61 | }
62 | writer := term.NewBufferedWriter(os.Stdout)
63 |
64 | for _, tip := range tips {
65 | cells := renderItem(tip, nil, false)
66 | for _, line := range cells {
67 | writer.WriteCells(line)
68 | }
69 | }
70 |
71 | return writer.Flush()
72 | }
73 |
--------------------------------------------------------------------------------
/cli/suggest.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | cp "github.com/c-bata/go-prompt"
10 | "github.com/c-bata/go-prompt/completer"
11 | "github.com/isacikgoz/tldr/pages"
12 | )
13 |
14 | var (
15 | pathTo = "path/to/"
16 | ext = ".*"
17 | )
18 |
19 | // SuggestCommand lets user to fill args with suggestions
20 | func SuggestCommand(item interface{}) string {
21 | answers := make([]string, 0)
22 | t, ok := item.(*pages.Tip)
23 | if !ok {
24 | return ""
25 | }
26 | fmt.Println()
27 | for _, arg := range t.Cmd.Args {
28 | // cs, _ := suggestCompleterFunc(arg)
29 | ext = getFileExtension(arg)
30 | answers = append(answers, cp.Input(
31 | "$"+" "+arg+" -> ",
32 | fileExtCompleterFunc,
33 | cp.OptionPreviewSuggestionTextColor(cp.Cyan),
34 | cp.OptionInputTextColor(cp.Cyan),
35 | cp.OptionAddKeyBind(cp.KeyBind{
36 | Key: cp.ControlC,
37 | Fn: func(buf *cp.Buffer) {
38 | os.Exit(0)
39 | return
40 | }}),
41 | cp.OptionAddKeyBind(cp.KeyBind{
42 | Key: cp.Escape,
43 | Fn: func(buf *cp.Buffer) {
44 |
45 | return
46 | }}),
47 | cp.OptionCompletionWordSeparator(completer.FilePathCompletionSeparator),
48 | ))
49 | }
50 |
51 | fs := t.Cmd.String()
52 | for i, arg := range t.Cmd.Args {
53 | // since we have double curlybraces on args
54 | fs = strings.Replace(fs, "{{"+arg+"}}", answers[i], 1)
55 | }
56 | return fs
57 | }
58 |
59 | // if the arg extension is matched, suggested values moves top of the slice
60 | // implementation could be beter
61 | func fileExtCompleterFunc(t cp.Document) []cp.Suggest {
62 | s := make([]cp.Suggest, 0)
63 | if len(ext) > 0 {
64 | filePathExtCompleter := completer.FilePathCompleter{
65 | IgnoreCase: true,
66 | Filter: func(fi os.FileInfo) bool {
67 | promoted := strings.HasSuffix(fi.Name(), ext)
68 | return promoted
69 | },
70 | }
71 | s = filePathExtCompleter.Complete(t)
72 | }
73 | f := filePathCompleterFunc(t)
74 | s = append(s, f...)
75 |
76 | return removeDuplicates(s)
77 | }
78 |
79 | // default file path completer, return all files
80 | func filePathCompleterFunc(d cp.Document) []cp.Suggest {
81 | filePathCompleter := completer.FilePathCompleter{
82 | IgnoreCase: true,
83 | }
84 | return filePathCompleter.Complete(d)
85 | }
86 |
87 | func suggestCompleterFunc(arg string) (completer.FilePathCompleter, error) {
88 | if strings.Contains(arg, pathTo) {
89 | filePathCompleter := completer.FilePathCompleter{
90 | IgnoreCase: true,
91 | }
92 | return filePathCompleter, nil
93 | }
94 | ext := getFileExtension(arg)
95 | // the arg should be longer than regular extension length such as "a.z"
96 | if len(arg) > 3 && len(ext) > 0 {
97 | filePathCompleter := completer.FilePathCompleter{
98 | IgnoreCase: true,
99 | Filter: func(fi os.FileInfo) bool {
100 | promoted := strings.HasSuffix(fi.Name(), ext)
101 | return promoted
102 | },
103 | }
104 | return filePathCompleter, nil
105 | }
106 | return completer.FilePathCompleter{}, errors.New("Can't suggest file")
107 | }
108 |
109 | // returns the file extension of the argument
110 | func getFileExtension(arg string) string {
111 | // probably not a file. hence, wont have an extension
112 | if strings.Contains(arg, "..") || len(arg) < 2 {
113 | return ""
114 | }
115 | // since the args is surrounded with "}}"
116 | r := []rune(arg)
117 | var ext string
118 | for i := len(r) - 1; i >= 0; i-- {
119 | ext = string(r[i]) + ext
120 | if r[i] == '.' || i < len(r)-3 {
121 | break
122 | }
123 | }
124 | // we expect a dot to determine if it is an extension
125 | if strings.Contains(ext, ".") {
126 | return ext
127 | }
128 | return ""
129 | }
130 |
131 | // removes duplicate entries from prompt.Suggest slice
132 | func removeDuplicates(elements []cp.Suggest) []cp.Suggest {
133 | // Use map to record duplicates as we find them.
134 | encountered := map[cp.Suggest]bool{}
135 | result := []cp.Suggest{}
136 |
137 | for v := range elements {
138 | if encountered[elements[v]] == true {
139 | // Do not add duplicate.
140 | } else {
141 | // Record this element as an encountered element.
142 | encountered[elements[v]] = true
143 | // Append to result slice.
144 | result = append(result, elements[v])
145 | }
146 | }
147 | // Return the new slice.
148 | return result
149 | }
150 |
--------------------------------------------------------------------------------
/cmd/tldr/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/alecthomas/kingpin/v2"
9 | "github.com/isacikgoz/gitin/prompt"
10 | "github.com/isacikgoz/tldr/cli"
11 | "github.com/isacikgoz/tldr/config"
12 | env "github.com/kelseyhightower/envconfig"
13 | )
14 |
15 | func main() {
16 |
17 | clear := kingpin.Flag("clear-cache", "Clear local repository then clone github.com/tldr-pages/tldr").Short('c').Bool()
18 | update := kingpin.Flag("update", "Pulls the latest commits from github.com/tldr-pages/tldr").Short('u').Bool()
19 | static := kingpin.Flag("static", "Static mode, application behaves like a conventional tldr client.").Short('s').Default("false").Bool()
20 | random := kingpin.Flag("random", "Random page for testing purposes.").Short('r').Default("false").Bool()
21 | pages := kingpin.Arg("command", "Name of the command. (e.g. tldr grep)").Strings()
22 |
23 | kingpin.UsageTemplate(kingpin.DefaultUsageTemplate + additionalHelp() + "\n")
24 | kingpin.Version("tldr++ version 1.0-alpha")
25 | kingpin.CommandLine.HelpFlag.Short('h')
26 | kingpin.CommandLine.VersionFlag.Short('v')
27 | kingpin.Parse()
28 |
29 | config.StartUp(*clear, *update)
30 |
31 | var o prompt.Options
32 | err := env.Process("tldr", &o)
33 | exitIfError(err)
34 |
35 | exitIfError(run(*pages, &o, *static, *random))
36 |
37 | }
38 |
39 | func run(pages []string, opts *prompt.Options, static, random bool) error {
40 | p, err := cli.NewDefaultPrompt(pages, opts, static, random)
41 | if err != nil {
42 | return err
43 | }
44 | ctx := context.Background()
45 | ctx, cancel := context.WithCancel(ctx)
46 | defer cancel()
47 | err = p.Run(ctx)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | item := p.Selection()
53 | if item == nil {
54 | return nil
55 | }
56 | command := cli.SuggestCommand(item)
57 |
58 | return cli.ConfirmCommand(command)
59 | }
60 |
61 | func exitIfError(err error) {
62 | if err != nil {
63 | fmt.Fprintf(os.Stderr, "%v\n", err)
64 | os.Exit(1)
65 | }
66 | }
67 |
68 | func additionalHelp() string {
69 | return `Environment Variables:
70 | TLDR_LINESIZE=
71 | TLDR_STARTINSEARCH=
72 | TLDR_DISABLECOLOR=
73 |
74 | Press ? for controls while application is running.`
75 | }
76 |
--------------------------------------------------------------------------------
/config/configurer.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "strings"
8 |
9 | "github.com/fatih/color"
10 | env "github.com/kelseyhightower/envconfig"
11 | )
12 |
13 | type envConf struct {
14 | OS string
15 | }
16 |
17 | // StartUp
18 | func StartUp(clear, update bool) error {
19 | ok, _ := exists(SourceDir)
20 | // is staled, first check for internet connectivity, we don't want to
21 | // existing source if so
22 | if st, _ := staled(); st && !update {
23 | yellow := color.New(color.FgYellow)
24 | fmt.Println(yellow.Sprint("TLDR repository is older than 2 weeks, consider updating it with -u option."))
25 | }
26 | if clear || !ok {
27 | err := Clear()
28 | if err != nil {
29 |
30 | }
31 | os.Exit(0)
32 | } else if update {
33 | err := PullSource()
34 | if err != nil {
35 |
36 | }
37 | os.Exit(0)
38 | }
39 | return nil
40 | }
41 |
42 | // OSName is the running program's operating system
43 | func OSName() (n string) {
44 | var conf envConf
45 | var osname string
46 | err := env.Process("tldr", &conf)
47 | if err != nil {
48 | //log.Warn(err.Error())
49 | }
50 | if len(conf.OS) > 0 {
51 | osname = strings.ToLower(conf.OS)
52 | } else {
53 | osname = runtime.GOOS
54 | }
55 | switch osname {
56 | case "windows":
57 | n = osname
58 | case "darwin":
59 | n = "osx"
60 | case "linux":
61 | n = osname
62 | case "solaris":
63 | n = "sunos"
64 | default:
65 | fmt.Println("Operating system couldn't be recognized")
66 | os.Exit(1)
67 | }
68 | return n
69 | }
70 |
71 | func PrintLogo() {
72 | fmt.Printf("%s\n", colorLogo())
73 | }
74 |
75 | // exists checks if the file exists
76 | func exists(path string) (bool, error) {
77 | _, err := os.Stat(path)
78 | if err == nil {
79 | return true, nil
80 | }
81 | if os.IsNotExist(err) {
82 | return false, nil
83 | }
84 | return true, err
85 | }
86 |
--------------------------------------------------------------------------------
/config/configurer_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "runtime"
5 | "testing"
6 | )
7 |
8 | func TestStartUp(t *testing.T) {
9 | t.Skip("Skipping test as os.Exit(0) is called in StartUp")
10 | var tests = []struct {
11 | input_1 bool
12 | input_2 bool
13 | }{
14 | {false, false},
15 | }
16 | for _, test := range tests {
17 | if err := StartUp(test.input_1, test.input_2); err != nil {
18 | t.Errorf("Test Failed: {%t, %t} inputted, recieved: {%s}", test.input_1,
19 | test.input_2, err.Error())
20 | }
21 | }
22 | }
23 |
24 | func TestOSName(t *testing.T) {
25 | if osName := OSName(); osName != runtime.GOOS {
26 | if osName == "osx" {
27 | return
28 | }
29 | t.Errorf("Test Failed: {%s} expected, recieved: {%s}", osName, runtime.GOOS)
30 | }
31 | }
32 |
33 | func TestExists(t *testing.T) {
34 | var tests = []struct {
35 | input string
36 | expected bool
37 | }{
38 | {"./configurer.go", true},
39 | {"/path/to/file", false},
40 | }
41 | for _, test := range tests {
42 | if output, err := exists(test.input); output != test.expected || err != nil {
43 | t.Errorf("Test Failed: {%s} inputted, {%t} expected, recieved: {%t}", test.input, test.expected, output)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/config/messages.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/fatih/color"
5 | )
6 |
7 | func colorLogo() string {
8 |
9 | cyan := color.New(color.FgCyan)
10 | blue := color.New(color.FgHiBlue)
11 | logo := cyan.Sprint(`
12 | __ __ __
13 | / /_/ /___/ /____ __ __
14 | / __/ / __ / ___/_/ /___/ /_`) + blue.Sprint(`
15 | / /_/ / /_/ / / /_ __/_ __/
16 | \__/_/\__,_/_/ /_/ /_/
17 |
18 | `)
19 | return logo
20 | }
21 |
22 | func logo() string {
23 | logo := `
24 | __ __ __
25 | / /_/ /___/ /____ __ __
26 | / __/ / __ / ___/_/ /___/ /_
27 | / /_/ / /_/ / / /_ __/_ __/
28 | \__/_/\__,_/_/ /_/ /_/
29 |
30 | `
31 | return logo
32 | }
33 |
--------------------------------------------------------------------------------
/config/source.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "time"
8 |
9 | "github.com/adrg/xdg"
10 | git "github.com/go-git/go-git/v5"
11 | "golang.org/x/exp/slices"
12 | )
13 |
14 | const (
15 | giturl = "https://github.com/tldr-pages/tldr.git"
16 | )
17 |
18 | var (
19 | dir = DataDir() + "/tldr"
20 | SourceDir = dir
21 | old = ".old"
22 | )
23 |
24 | var approvedOSes = []string{"windows", "darwin", "linux", "android", "solaris"}
25 |
26 | // Clear removes the existing tldr directory. TODO: maybe require user to confirm
27 | func Clear() error {
28 | os.Rename(SourceDir, SourceDir+old)
29 |
30 | PrintLogo()
31 | _, err := git.PlainClone(dir, false, &git.CloneOptions{
32 | URL: giturl,
33 | RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
34 | Progress: os.Stdout,
35 | })
36 | if err == nil {
37 | fmt.Printf("Successfully cloned into: %s\n", dir)
38 | os.RemoveAll(SourceDir + old)
39 | }
40 | if err != nil {
41 | os.RemoveAll(SourceDir)
42 | os.Rename(SourceDir+old, SourceDir)
43 | }
44 | return err
45 | }
46 |
47 | // Pulls the github.com/tldr-pages/tldr repository
48 | func PullSource() error {
49 |
50 | r, err := git.PlainOpen(dir)
51 | if err != nil {
52 | return err
53 | }
54 | w, err := r.Worktree()
55 | if err != nil {
56 | return err
57 | }
58 | err = w.Pull(&git.PullOptions{
59 | RemoteName: "origin",
60 | Progress: os.Stdout,
61 | })
62 | if err != nil {
63 | if err == git.NoErrAlreadyUpToDate {
64 | fmt.Printf("%s\n", "No changes at tldr-pages repository.")
65 | } else {
66 | fmt.Printf("%s\n", err.Error())
67 | }
68 | } else {
69 | fmt.Printf("Successfully pulled into: %s\n", dir)
70 | }
71 | return err
72 | }
73 |
74 | // DataDir returns OS dependent data dir. see XDG Base Directory Specification:
75 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
76 | func DataDir() (d string) {
77 | osname := runtime.GOOS
78 | if slices.Contains(approvedOSes, osname) {
79 | d = xdg.StateHome
80 | } else {
81 | fmt.Println("Operating system couldn't be recognized")
82 | }
83 | return d
84 | }
85 |
86 | // staled checks if the source folder is older than two weeks
87 | func staled() (bool, error) {
88 | file, err := os.Open(SourceDir)
89 | if err != nil {
90 | return false, err
91 | }
92 |
93 | defer file.Close()
94 |
95 | fstat, err := file.Stat()
96 | if err != nil {
97 | return false, err
98 | }
99 |
100 | diff := time.Now().Sub(fstat.ModTime())
101 | // Two week update time, seems fair.
102 | if diff > 24*7*2*time.Hour {
103 | return true, nil
104 | }
105 | return false, nil
106 | }
107 |
--------------------------------------------------------------------------------
/config/source_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 |
6 | git "github.com/go-git/go-git/v5"
7 | )
8 |
9 | func TestClear(t *testing.T) {
10 | if err := Clear(); err != nil {
11 | t.Errorf("Test Failed: %s", err.Error())
12 | }
13 | }
14 |
15 | func TestPullSource(t *testing.T) {
16 | if err := PullSource(); err != nil && err != git.NoErrAlreadyUpToDate {
17 | t.Errorf("Test Failed: %s", err.Error())
18 | }
19 | }
20 |
21 | func TestDataDir(t *testing.T) {
22 | if len(DataDir()) <= 0 {
23 | t.Errorf("Test Failed.")
24 | }
25 |
26 | }
27 |
28 | func TestStaled(t *testing.T) {
29 | if _, err := staled(); err != nil {
30 | t.Errorf("Test Failed: %s", err.Error())
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/isacikgoz/tldr
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/adrg/xdg v0.4.0
7 | github.com/alecthomas/kingpin/v2 v2.3.2
8 | github.com/c-bata/go-prompt v0.2.6
9 | github.com/fatih/color v1.13.0
10 | github.com/go-git/go-git/v5 v5.5.2
11 | github.com/isacikgoz/gitin v0.2.6-0.20210418153550-465ee4a7e407
12 | github.com/kelseyhightower/envconfig v1.4.0
13 | )
14 |
15 | require (
16 | github.com/Microsoft/go-winio v0.6.0 // indirect
17 | github.com/ProtonMail/go-crypto v0.0.0-20230113180642-068501e20d67 // indirect
18 | github.com/acomagu/bufpipe v1.0.3 // indirect
19 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
20 | github.com/cloudflare/circl v1.3.1 // indirect
21 | github.com/emirpasic/gods v1.18.1 // indirect
22 | github.com/go-git/gcfg v1.5.0 // indirect
23 | github.com/go-git/go-billy/v5 v5.4.0 // indirect
24 | github.com/imdario/mergo v0.3.13 // indirect
25 | github.com/isacikgoz/fuzzy v0.2.0 // indirect
26 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
27 | github.com/kevinburke/ssh_config v1.2.0 // indirect
28 | github.com/mattn/go-colorable v0.1.13 // indirect
29 | github.com/mattn/go-isatty v0.0.17 // indirect
30 | github.com/mattn/go-runewidth v0.0.14 // indirect
31 | github.com/mattn/go-tty v0.0.4 // indirect
32 | github.com/pjbgf/sha1cd v0.2.3 // indirect
33 | github.com/pkg/term v1.2.0-beta.2 // indirect
34 | github.com/rivo/uniseg v0.4.3 // indirect
35 | github.com/sergi/go-diff v1.3.1 // indirect
36 | github.com/skeema/knownhosts v1.1.0 // indirect
37 | github.com/xanzy/ssh-agent v0.3.3 // indirect
38 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
39 | golang.org/x/crypto v0.5.0 // indirect
40 | golang.org/x/exp v0.0.0-20230519143937-03e91628a987
41 | golang.org/x/mod v0.7.0 // indirect
42 | golang.org/x/net v0.5.0 // indirect
43 | golang.org/x/sys v0.4.0 // indirect
44 | golang.org/x/tools v0.5.0 // indirect
45 | gopkg.in/warnings.v0 v0.1.2 // indirect
46 | )
47 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
2 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
3 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
4 | github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
5 | github.com/ProtonMail/go-crypto v0.0.0-20230113180642-068501e20d67 h1:prYO7+yhqRjrP/lryCKZG4ieyeoKE40r8BKokURLd5M=
6 | github.com/ProtonMail/go-crypto v0.0.0-20230113180642-068501e20d67/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
7 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
8 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
9 | github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
10 | github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
11 | github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
12 | github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
14 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
15 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
16 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
17 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
18 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
19 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
20 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
21 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
22 | github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
23 | github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
24 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
25 | github.com/cloudflare/circl v1.3.1 h1:4OVCZRL62ijwEwxnF6I7hLwxvIYi3VaZt8TflkqtrtA=
26 | github.com/cloudflare/circl v1.3.1/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
27 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
31 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
32 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
33 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
34 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
35 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
36 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
37 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
38 | github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
39 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
40 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
41 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
42 | github.com/go-git/go-billy/v5 v5.4.0 h1:Vaw7LaSTRJOUric7pe4vnzBSgyuf2KrLsu2Y4ZpQBDE=
43 | github.com/go-git/go-billy/v5 v5.4.0/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
44 | github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
45 | github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
46 | github.com/go-git/go-git/v5 v5.5.2 h1:v8lgZa5k9ylUw+OR/roJHTxR4QItsNFI5nKtAXFuynw=
47 | github.com/go-git/go-git/v5 v5.5.2/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI=
48 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
49 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
50 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
51 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
52 | github.com/isacikgoz/fuzzy v0.2.0 h1:b2AUOLrmR36em9UhkWMkIrEJZFeoPgl9kZzBiktpntU=
53 | github.com/isacikgoz/fuzzy v0.2.0/go.mod h1:VEYn1Gfwj4lMg+FTH603LmQni/zTrhxKv7nTFG+RO8U=
54 | github.com/isacikgoz/gia v0.2.0/go.mod h1:pTtCjwM3VmUTZNGXirQ5ixW5NGunvFEO7ah9Boxj6IM=
55 | github.com/isacikgoz/gitin v0.2.6-0.20210418153550-465ee4a7e407 h1:Yv9Trm/G3LsyItbo/VnNcVbt6H7ynemh9O3aRMof1pE=
56 | github.com/isacikgoz/gitin v0.2.6-0.20210418153550-465ee4a7e407/go.mod h1:sfU+lRZsFvAjejjKYUaTenzoZ9E4rwSx4dYdPeOB9L8=
57 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
58 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
59 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
60 | github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
61 | github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
62 | github.com/juju/loggo v0.0.0-20160511211902-0e0537f18a29/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
63 | github.com/juju/testing v0.0.0-20160203233110-321edad6b2d1/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
64 | github.com/juju/utils v0.0.0-20160815113839-bdb77b07e7e3/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
65 | github.com/justincampbell/bigduration v0.0.0-20160531141349-e45bf03c0666/go.mod h1:xqGOmDZzLOG7+q/CgsbXv10g4tgPsbjhmAxyaTJMvis=
66 | github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d/go.mod h1:U7FWcK1jzZJnYuSnxP6efX3ZoHbK1CEpD0ThYyGNPNI=
67 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
68 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
69 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
70 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
71 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
72 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
73 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
76 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
77 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
78 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
79 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
80 | github.com/libgit2/git2go/v30 v30.0.9/go.mod h1:bEqWPWaJjDpnkerA2FlyUdsuhc5/3UPBjYJ6SV0X3gY=
81 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
82 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
83 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
84 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
85 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
86 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
87 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
88 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
89 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
90 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
91 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
92 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
93 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
94 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
95 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
96 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
97 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
98 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
99 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
100 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
101 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
102 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
103 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
104 | github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E=
105 | github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
106 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
107 | github.com/nsf/termbox-go v0.0.0-20190325093121-288510b9734e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
108 | github.com/pjbgf/sha1cd v0.2.3 h1:uKQP/7QOzNtKYH7UTohZLcjF5/55EnTw0jO/Ru4jZwI=
109 | github.com/pjbgf/sha1cd v0.2.3/go.mod h1:HOK9QrgzdHpbc2Kzip0Q1yi3M2MFGPADtR6HjG65m5M=
110 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
111 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
112 | github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
113 | github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
114 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
115 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
116 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
117 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
118 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
119 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
120 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
121 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
122 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
123 | github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
124 | github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
125 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
126 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
127 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
128 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
129 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
130 | github.com/waigani/diffparser v0.0.0-20190426062500-1f7065f429b5/go.mod h1:CefseIIgCUqtn0B83Lc3+8F2L1V9viokWY2GQlkWGfs=
131 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d/go.mod h1:BzSc3WEF8R+lCaP5iGFRxd5kIXy4JKOZAwNe1w0cdc0=
132 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
133 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
134 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
135 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
136 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
137 | golang.org/x/crypto v0.0.0-20160824173033-351dc6a5bf92/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
138 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
139 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
140 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
141 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
142 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
143 | golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
144 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
145 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
146 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
147 | golang.org/x/exp v0.0.0-20230519143937-03e91628a987 h1:3xJIFvzUFbu4ls0BTBYcgbCGhA63eAOEMxIHugyXJqA=
148 | golang.org/x/exp v0.0.0-20230519143937-03e91628a987/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
149 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
150 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
151 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
152 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
153 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
154 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
155 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
156 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
157 | golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
158 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
159 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
160 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
161 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
162 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
163 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
164 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
165 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
166 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
168 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
169 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
170 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
171 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
172 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
173 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
174 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
175 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
176 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
177 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
178 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
179 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
180 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
181 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
182 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
183 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
184 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
185 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
186 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
187 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
188 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
189 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
190 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
191 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
192 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
193 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
194 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
195 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
196 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
197 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
198 | golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
199 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
200 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
201 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
202 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
203 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
204 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
205 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
206 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
207 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
208 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
209 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
210 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
211 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
212 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
213 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
214 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
215 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
216 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
217 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
218 | gopkg.in/mgo.v2 v2.0.0-20150529124711-01ee097136da/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
219 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
220 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
221 | gopkg.in/yaml.v2 v2.0.0-20160715033755-e4d366fc3c79/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
222 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
223 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
224 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
225 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
226 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
227 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
228 |
--------------------------------------------------------------------------------
/img/benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isacikgoz/tldr/b6daabc636cffd2e82ba6b00cc82ab6ddef57a1e/img/benchmarks.png
--------------------------------------------------------------------------------
/img/screenplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isacikgoz/tldr/b6daabc636cffd2e82ba6b00cc82ab6ddef57a1e/img/screenplay.gif
--------------------------------------------------------------------------------
/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isacikgoz/tldr/b6daabc636cffd2e82ba6b00cc82ab6ddef57a1e/img/screenshot.png
--------------------------------------------------------------------------------
/pages/page.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // Page is the representation of a tldr page itself
9 | type Page struct {
10 | Name string
11 | Desc string
12 | Tips []*Tip
13 | }
14 |
15 | // Tip is the list item of a tldr page
16 | type Tip struct {
17 | Desc string
18 | Cmd *Command
19 | }
20 |
21 | // Command is the representation of a tip's command suggestion
22 | type Command struct {
23 | Command string
24 | Args []string
25 | }
26 |
27 | // ParsePage parses from bare markdown string. Rather than parsing markdown itself
28 | // initial implementation approach is stripping from a single string
29 | func ParsePage(s string) *Page {
30 | l := strings.Split(s, "\n")
31 |
32 | n := l[0][2:]
33 | var d string
34 | var c int
35 | for ln := 2; ln < len(l); ln++ {
36 | line := l[ln]
37 | if len(line) > 0 && line[0] == '>' {
38 | d = d + line[2:] + "\n"
39 | } else {
40 | c = ln
41 | break
42 | }
43 | }
44 |
45 | tips := make([]*Tip, 0)
46 | for ln := c; ln < len(l); {
47 | line := l[ln]
48 | if len(line) > 0 && line[0] == '-' {
49 | // remove last rune then first two runes
50 | d := line[:len(line)-1][2:]
51 | c := l[ln+2]
52 | var cmd *Command
53 | if len(c) > 0 && c[0] == '`' {
54 | cmd = &Command{
55 | Command: c[:len(c)-1][1:],
56 | Args: stripCommandArgs(c),
57 | }
58 | ln = ln + 2
59 | } else {
60 | break
61 | }
62 | tips = append(tips, &Tip{
63 | Desc: d,
64 | Cmd: cmd,
65 | })
66 | }
67 | ln++
68 | }
69 |
70 | p := &Page{
71 | Name: n,
72 | Desc: d,
73 | Tips: tips,
74 | }
75 | return p
76 | }
77 |
78 | func (p *Page) String() string {
79 | return fmt.Sprintf("%s\n%s", p.Name, p.Desc)
80 | }
81 |
82 | func (t *Tip) String() string {
83 | return t.Desc
84 | }
85 |
86 | func (c *Command) String() string {
87 | return c.Command
88 | }
89 |
90 | func stripCommandArgs(in string) []string {
91 | out := make([]string, 0)
92 | ir := []rune(in)
93 | argStart := 0
94 | argEnd := 0
95 | for ix := 0; ix < len(ir); {
96 | if ir[ix] == '{' && ir[ix+1] == '{' {
97 | argStart = ix + 2
98 | ix = ix + 2
99 | } else if ir[ix] == '}' && ir[ix+1] == '}' {
100 | argEnd = ix
101 | out = append(out, in[:argEnd][argStart:])
102 | ix = ix + 2
103 | } else {
104 | ix++
105 | }
106 | }
107 | return out
108 | }
109 |
--------------------------------------------------------------------------------
/pages/page_test.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestParsePage(t *testing.T) {
8 | testPage := "# cd \n\n" +
9 | "> Change the current working directory.\n\n" +
10 | "- Go to the given directory:\n\n" +
11 | "`cd {{path/to/directory}}`\n\n" +
12 | "- Go to home directory of current user:\n\n" +
13 | "`cd`\n\n" +
14 | "- Go up to the parent of the current directory:\n\n" +
15 | "`cd ..`\n\n" +
16 | "- Go to the previously chosen directory:\n\n" +
17 | "`cd -`\n"
18 |
19 | page := ParsePage(testPage)
20 | if page == nil {
21 | t.Fatal("could not generate page")
22 | }
23 | if len(page.Tips) != 4 {
24 | t.Fatal("could not generate tips as expected")
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pages/pages.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "math/rand"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/isacikgoz/tldr/config"
12 | )
13 |
14 | const (
15 | sep = string(os.PathSeparator)
16 | ext = ".md"
17 | )
18 |
19 | // Read finds and creates the Page, if it does not find, simply returns abstract
20 | // contribution guide
21 | func Read(seq []string) (p *Page, err error) {
22 | page := ""
23 | for i, l := range seq {
24 | if len(seq)-1 == i {
25 | page = page + l
26 | break
27 | } else {
28 | page = page + l + "-"
29 | }
30 | }
31 | // Common pages are more, so we have better luck there
32 | p, err = queryCommon(page)
33 | if err != nil {
34 | p, err = queryOS(page)
35 | if err != nil {
36 | return p, errors.New("This page (" + page + ") doesn't exist yet!\n" +
37 | "Submit new pages here: https://github.com/tldr-pages/tldr")
38 | }
39 | }
40 | return p, nil
41 | }
42 |
43 | // Queries from common folder
44 | func queryCommon(page string) (p *Page, err error) {
45 | d := config.SourceDir + sep + "pages" + sep + "common" + sep
46 | b, err := ioutil.ReadFile(d + page + ".md")
47 | if err != nil {
48 | return p, err
49 | }
50 | p = ParsePage(string(b))
51 | return p, nil
52 | }
53 |
54 | // Queries from os specific folder
55 | func queryOS(page string) (p *Page, err error) {
56 | d := config.SourceDir + sep + "pages" + sep + config.OSName() + sep
57 | b, err := ioutil.ReadFile(d + page + ".md")
58 | if err != nil {
59 | return p, err
60 | }
61 | p = ParsePage(string(b))
62 | return p, nil
63 | }
64 |
65 | // QueryRandom brings a random page from the source
66 | func QueryRandom() (p *Page, err error) {
67 | dCmn := config.SourceDir + sep + "pages" + sep + "common" + sep
68 | dOs := config.SourceDir + sep + "pages" + sep + config.OSName() + sep
69 | paths := []string{dCmn, dOs}
70 | sources := make([]string, 0)
71 | for _, p := range paths {
72 | files, err := ioutil.ReadDir(p)
73 | if err != nil {
74 | break
75 | }
76 | for _, f := range files {
77 | if strings.HasSuffix(f.Name(), ".md") {
78 | sources = append(sources, f.Name()[:len(f.Name())-3])
79 | }
80 | }
81 | }
82 | rand.Seed(time.Now().UTC().UnixNano())
83 | page := sources[rand.Intn(len(sources))]
84 | return Read([]string{page})
85 | }
86 |
87 | // ReadAll reads every single page from the source inta single page
88 | func ReadAll() (p *Page, err error) {
89 | dCmn := config.SourceDir + sep + "pages" + sep + "common" + sep
90 | dOs := config.SourceDir + sep + "pages" + sep + config.OSName() + sep
91 | paths := []string{dCmn, dOs}
92 | p = &Page{Name: "Search All"}
93 | p.Tips = make([]*Tip, 0)
94 | for _, pt := range paths {
95 | files, err := ioutil.ReadDir(pt)
96 | if err != nil {
97 | break
98 | }
99 | for _, f := range files {
100 | if strings.HasSuffix(f.Name(), ".md") {
101 | page, err := Read([]string{f.Name()[:len(f.Name())-3]})
102 | if err != nil {
103 | continue
104 | }
105 | p.Tips = append(p.Tips, page.Tips...)
106 | }
107 | }
108 | }
109 | return p, nil
110 | }
111 |
--------------------------------------------------------------------------------