├── .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 | ![GitHub Actions](https://img.shields.io/github/actions/workflow/status/isacikgoz/tldr/ci.yml) ![GitHub All Releases](https://img.shields.io/github/downloads/isacikgoz/tldr/total.svg) [![Join the chat at https://gitter.im/tldrpp/community](https://badges.gitter.im/tldrpp/community.svg)](https://gitter.im/tldrpp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Release](https://img.shields.io/github/release/isacikgoz/tldr.svg) 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 | ![screenplay](img/screenplay.gif) 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("<ctrl-c> 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=<int> 71 | TLDR_STARTINSEARCH=<bool> 72 | TLDR_DISABLECOLOR=<bool> 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 | --------------------------------------------------------------------------------