├── .github └── workflows │ ├── build.yml │ ├── goreleaser.yml │ └── lint.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── assets └── scrot.png ├── go.mod ├── go.sum ├── lib.go ├── main.go └── styles.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | paths: 5 | - "**.go" 6 | - ".github/workflows/build.yml" 7 | pull_request: 8 | paths: 9 | - "**.go" 10 | - ".github/workflows/build.yml" 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | go-version: [~1.18, ^1] 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | env: 20 | GO111MODULE: "on" 21 | steps: 22 | - name: Install Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | 30 | - name: Download Go modules 31 | run: go mod download 32 | 33 | - name: Build 34 | run: go build -v ./... 35 | 36 | - name: Test 37 | run: go test ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Import GPG Key 13 | id: import_gpg 14 | uses: crazy-max/ghaction-import-gpg@v4 15 | with: 16 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 17 | passphrase: ${{ secrets.PASSPHRASE }} 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - name: Set up Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: "^1.18" 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v2 28 | with: 29 | version: latest 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | permissions: 9 | contents: read 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v2 18 | with: 19 | args: "--exclude 'SA1019: strings.Title'" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode/ 3 | 4 | # Dev 5 | temp/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - binary: def 9 | flags: 10 | - -trimpath 11 | ldflags: -s -w -X main.Version={{ .Version }} -X main.CommitSHA={{ .Commit }} 12 | goos: 13 | - linux 14 | - darwin 15 | - windows 16 | goarch: 17 | - amd64 18 | - arm64 19 | - 386 20 | - arm 21 | goarm: 22 | - 6 23 | - 7 24 | 25 | archives: 26 | - format_overrides: 27 | - goos: windows 28 | format: zip 29 | 30 | nfpms: 31 | - builds: 32 | - def 33 | vendor: obscurity 34 | homepage: "https://x4.pm" 35 | maintainer: "fawn " 36 | description: "def is a dictionary client for the command line" 37 | license: ISC 38 | formats: 39 | - apk 40 | - deb 41 | - rpm 42 | bindir: /usr/bin 43 | 44 | signs: 45 | - artifacts: checksum 46 | args: 47 | [ 48 | "--batch", 49 | "-u", 50 | "{{ .Env.GPG_FINGERPRINT }}", 51 | "--output", 52 | "${signature}", 53 | "--detach-sign", 54 | "${artifact}", 55 | ] 56 | 57 | checksum: 58 | name_template: "checksums.txt" 59 | 60 | snapshot: 61 | name_template: "{{ .Tag }}-next" 62 | changelog: 63 | sort: asc 64 | filters: 65 | exclude: 66 | - "^docs:" 67 | - "^test:" 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 obscurity 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # def 2 | 3 | > A comfy dictionary navigator for the command line. ☁️ 4 | 5 | [![Latest Release](https://img.shields.io/github/release/fawni/def.svg)](https://github.com/fawni/def/releases) 6 | [![Build Status](https://img.shields.io/github/actions/workflow/status/fawni/def/build.yml?logo=github&branch=master)](https://github.com/fawni/def/actions) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/fawni/def)](https://goreportcard.com/report/github.com/fawni/def) 8 | 9 | ![scrot](assets/scrot.png) 10 | 11 | ## Installation 12 | 13 | ### Binaries 14 | 15 | Download a binary from the [releases](https://github.com/fawni/def/releases) 16 | page. 17 | 18 | ### Build from source 19 | 20 | Go 1.18 or higher required. ([install instructions](https://golang.org/doc/install.html)) 21 | 22 | go install github.com/fawni/def@latest 23 | 24 | ## Usage 25 | 26 | ``` 27 | def 28 | ``` 29 | 30 | `def -h` for more information. 31 | 32 | ### Flags 33 | 34 | - `-r`, `--related`: displays synonyms and antonyms if found 35 | - `-e`, `--examples`: displays examples if found 36 | - `-f`, `--full`: displays synonyms, antonyms and examples if found 37 | 38 | ## License 39 | 40 | [ISC](LICENSE) 41 | -------------------------------------------------------------------------------- /assets/scrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fawni/def/7c695cf369deaeb4aa4ca3584b49519f26c03a88/assets/scrot.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fawni/def 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/charmbracelet/lipgloss v0.13.1 7 | github.com/hbollon/go-edlib v1.6.0 8 | github.com/muesli/reflow v0.3.0 9 | github.com/spf13/cobra v1.8.1 10 | ) 11 | 12 | require ( 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/x/ansi v0.4.0 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 17 | github.com/mattn/go-isatty v0.0.20 // indirect 18 | github.com/mattn/go-runewidth v0.0.16 // indirect 19 | github.com/muesli/termenv v0.15.2 // indirect 20 | github.com/rivo/uniseg v0.4.7 // indirect 21 | github.com/spf13/pflag v1.0.5 // indirect 22 | golang.org/x/sys v0.26.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 | github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A= 4 | github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U= 5 | github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= 6 | github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E= 9 | github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM= 10 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 11 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 12 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 13 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 17 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 18 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 19 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 20 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 21 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 22 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 23 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 24 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 25 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 26 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 27 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 28 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 29 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 30 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 31 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 32 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 34 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /lib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/muesli/reflow/wordwrap" 8 | ) 9 | 10 | type word struct { 11 | Word string `json:"word,omitempty"` 12 | Phonetic string `json:"phonetic,omitempty"` 13 | Phonetics []phonetics `json:"phonetics,omitempty"` 14 | Meanings []meaning `json:"meanings,omitempty"` 15 | } 16 | 17 | type phonetics struct { 18 | Text string `json:"text,omitempty"` 19 | } 20 | 21 | type meaning struct { 22 | PartOfSpeech string `json:"partOfSpeech,omitempty"` 23 | Definitions []definition `json:"definitions,omitempty"` 24 | Synonyms []string `json:"synonyms,omitempty"` 25 | Antonyms []string `json:"antonyms,omitempty"` 26 | } 27 | 28 | type definition struct { 29 | Definition string `json:"definition,omitempty"` 30 | Synonyms []string `json:"synonyms,omitempty"` 31 | Antonyms []string `json:"antonyms,omitempty"` 32 | Example string `json:"example,omitempty"` 33 | } 34 | 35 | func (w word) render() { 36 | res := titleMargin.Render(titleStyle.Render(w.Word) + w.renderPhonetic()) 37 | res += w.renderMeanings() 38 | fmt.Println(res + "\n") 39 | } 40 | 41 | // the api is somewhat inconsistent for phonems so we have to search for the phonetic of a word, that is if it exists 42 | func (w word) renderPhonetic() string { 43 | if w.Phonetic != "" { 44 | return " - " + phoneticStyle.Render(w.Phonetic) 45 | } 46 | for _, phonetic := range w.Phonetics { 47 | if phonetic.Text != "" { 48 | return " - " + phoneticStyle.Render(phonetic.Text) 49 | } 50 | } 51 | return "" 52 | } 53 | 54 | func (w word) renderMeanings() (s string) { 55 | for _, meaning := range w.Meanings { 56 | s += "\n" + posStyle.Render(meaning.PartOfSpeech) 57 | s += meaning.renderMeaningSynonymAndAntonym() 58 | for i, definition := range meaning.Definitions { 59 | n := fmt.Sprintf("%d. ", i+1) 60 | s += "\n" + textStyle.Render(n+wordwrap.String(definition.Definition, 70)) 61 | s += definition.renderExample() 62 | } 63 | } 64 | return 65 | } 66 | 67 | func (m meaning) renderMeaningSynonymAndAntonym() (s string) { 68 | if !full && !related { 69 | return 70 | } 71 | 72 | const MAX = 4 73 | if len(m.Synonyms) > MAX { 74 | s += " " + synonymStyle.Render(strings.Join(m.Synonyms[:MAX], ", ")) 75 | } else { 76 | s += " " + synonymStyle.Render(strings.Join(m.Synonyms, ", ")) 77 | } 78 | 79 | if len(m.Antonyms) > MAX { 80 | s += " " + antonymStyle.Render(strings.Join(m.Antonyms[:MAX], ", ")) 81 | } else { 82 | s += " " + antonymStyle.Render(strings.Join(m.Antonyms, ", ")) 83 | } 84 | 85 | return 86 | } 87 | 88 | func (d definition) renderExample() (s string) { 89 | if !full && !examples { 90 | return 91 | } 92 | 93 | if d.Example != "" { 94 | s = exampleStyle.Render(wordwrap.String("\n\""+d.Example+"\"", 70)) 95 | } 96 | 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/hbollon/go-edlib" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | full bool 17 | related bool 18 | examples bool 19 | 20 | cmd = &cobra.Command{ 21 | Use: "def ", 22 | Short: "def is a command-line dictionary client", 23 | Args: cobra.MinimumNArgs(1), 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | return define(args) 26 | }, 27 | } 28 | ) 29 | 30 | func init() { 31 | cmd.PersistentFlags().BoolVarP(&full, "full", "f", false, "displays synonyms, antonyms and examples if found") 32 | cmd.PersistentFlags().BoolVarP(&related, "related", "r", false, "displays synonyms and antonyms if found") 33 | cmd.PersistentFlags().BoolVarP(&examples, "examples", "e", false, "displays examples if found") 34 | } 35 | 36 | func main() { 37 | if err := cmd.Execute(); err != nil { 38 | fmt.Println(errorStyle.Render(err.Error())) 39 | os.Exit(1) 40 | } 41 | } 42 | 43 | func define(args []string) error { 44 | res, err := http.Get("https://api.dictionaryapi.dev/api/v2/entries/en/" + args[0]) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | switch res.StatusCode { 50 | case 200: 51 | defer res.Body.Close() 52 | body, err := io.ReadAll(res.Body) 53 | if err != nil { 54 | return err 55 | } 56 | var words []word 57 | if err := json.Unmarshal(body, &words); err != nil { 58 | return err 59 | } 60 | for _, word := range words { 61 | word.render() 62 | } 63 | case 404: 64 | fmt.Println(errorStyle.Render("no definitions found.")) 65 | suggest(args[0]) 66 | os.Exit(1) 67 | default: 68 | fmt.Println(errorStyle.Render("failed with status " + res.Status)) 69 | os.Exit(1) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func suggest(word string) { 76 | // TODO: cache this? 77 | res, err := http.Get("https://raw.githubusercontent.com/meetDeveloper/freeDictionaryAPI/refs/heads/master/meta/wordList/english.txt") 78 | if err != nil { 79 | return 80 | } 81 | defer res.Body.Close() 82 | body, err := io.ReadAll(res.Body) 83 | if err != nil { 84 | return 85 | } 86 | var words []string 87 | for _, word := range strings.Split(string(body), "\n") { 88 | words = append(words, strings.TrimSpace(word)) 89 | } 90 | 91 | matches, err := edlib.FuzzySearchSet(word, words, 3, edlib.JaroWinkler) 92 | if err != nil { 93 | return 94 | } 95 | 96 | var mstr string 97 | for i, match := range matches { 98 | m := "'" + match + "'" 99 | if i == 0 { 100 | mstr = m 101 | continue 102 | } 103 | if i == len(matches)-1 { 104 | mstr += " or " + m 105 | break 106 | } 107 | mstr += ", " + m 108 | } 109 | 110 | fmt.Println(errorStyle.Render("did you mean: " + mstr + "?")) 111 | } 112 | -------------------------------------------------------------------------------- /styles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | lg "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | const ( 8 | BLACK = 0 9 | RED = 1 10 | GREEN = 2 11 | PURPLE = 5 12 | CYAN = 6 13 | WHITE = 7 14 | GRAY = 8 15 | ) 16 | 17 | var ( 18 | red = lg.ANSIColor(RED) 19 | cyan = lg.ANSIColor(CYAN) 20 | green = lg.ANSIColor(GREEN) 21 | purple = lg.ANSIColor(PURPLE) 22 | white = lg.ANSIColor(WHITE) 23 | gray = lg.ANSIColor(GRAY) 24 | 25 | titleMargin = lg.NewStyle(). 26 | Margin(1, 0, 1, 2) 27 | 28 | titleStyle = lg.NewStyle(). 29 | Background(red). 30 | Foreground(white). 31 | Bold(true). 32 | Padding(0, 1) 33 | 34 | phoneticStyle = lg.NewStyle(). 35 | Foreground(cyan). 36 | Italic(true) 37 | 38 | posStyle = lg.NewStyle(). 39 | Foreground(purple). 40 | Italic(true). 41 | Bold(true). 42 | MarginLeft(2) 43 | 44 | textStyle = lg.NewStyle(). 45 | MarginLeft(2) 46 | 47 | synonymStyle = lg.NewStyle(). 48 | Foreground(green) 49 | 50 | antonymStyle = lg.NewStyle(). 51 | Foreground(red) 52 | 53 | exampleStyle = lg.NewStyle(). 54 | Foreground(gray). 55 | Italic(true). 56 | MarginLeft(2) 57 | 58 | errorStyle = lg.NewStyle(). 59 | Foreground(red) 60 | ) 61 | --------------------------------------------------------------------------------