From e622234993bf52b3c9b22aafc2bb3c4ab2f3aba8 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Thu, 19 Jun 2025 16:48:22 +0100 Subject: [PATCH] initial commit --- .github/workflows/golang-ci.yml | 29 ++++++ .github/workflows/release.yml | 31 +++++++ .github/workflows/update-go-modules.yml | 30 +++++++ .gitignore | 29 ++++++ .golangci.yml | 54 +++++++++++ .goreleaser.yaml | 53 +++++++++++ LICENSE | 21 +++++ README.md | 56 ++++++++++++ Taskfile.yaml | 57 ++++++++++++ context.go | 22 +++++ filter.go | 19 ++++ go.mod | 45 ++++++++++ go.sum | 110 +++++++++++++++++++++++ img/promptfilter.png | Bin 0 -> 5479 bytes img/selectionprompt.png | Bin 0 -> 9808 bytes main.go | 68 ++++++++++++++ new.go | 114 ++++++++++++++++++++++++ 17 files changed, 738 insertions(+) create mode 100644 .github/workflows/golang-ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/update-go-modules.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Taskfile.yaml create mode 100644 context.go create mode 100644 filter.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 img/promptfilter.png create mode 100755 img/selectionprompt.png create mode 100644 main.go create mode 100644 new.go diff --git a/.github/workflows/golang-ci.yml b/.github/workflows/golang-ci.yml new file mode 100644 index 0000000..bd13d40 --- /dev/null +++ b/.github/workflows/golang-ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ "main" ] + paths: + - '**.go' + pull_request: + branches: [ "main" ] + paths: + - '**.go' +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 3 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: Run golangci-lint + run: golangci-lint run ./... \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4429e32 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: goreleaser + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v5 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/update-go-modules.yml b/.github/workflows/update-go-modules.yml new file mode 100644 index 0000000..830e99a --- /dev/null +++ b/.github/workflows/update-go-modules.yml @@ -0,0 +1,30 @@ +name: Auto-Update Go Modules + +on: + schedule: + - cron: "0 0 * * 1" # Runs every Monday at midnight + +jobs: + update-go-modules: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Update Dependencies + run: | + go get -u ./... + go mod tidy + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add go.mod go.sum + git commit -m "chore: auto-update Go modules" + git push \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd9ee61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Generated by gogn: github.com/onyx-and-iris/gogn + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of gogn + +.envrc diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..55de51c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,54 @@ +run: + # timeout for analysis, e.g. 30s, 3m, default is 1m + timeout: 3m + # exclude test files + tests: true + +linters: + # Set to true runs only fast linters. + # Good option for 'lint on save', pre-commit hook or CI. + fast: true + + disable-all: true + + enable: + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - gofumpt + - misspell + - unparam + - gosec + - asciicheck + - errname + - gci + - godot + - goimports + - revive + +linters-settings: + gofmt: + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' + - pattern: 'a[b:len(a)]' + replacement: 'a[b:]' + + misspell: + locale: UK + + errcheck: + check-type-assertions: true + +issues: + max-same-issues: 0 + max-issues-per-linter: 0 + exclude-use-default: false + exclude: + # gosec: Duplicated errcheck checks + - G104 + # gosec: integer overflow conversion int -> uint32 + - G115 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..1d5e192 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,53 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + +archives: + - formats: ['tar.gz'] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: ['zip'] + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +release: + footer: >- + + --- + + Released by [GoReleaser](https://github.com/goreleaser/goreleaser). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29058bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2025 onyx-and-iris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..252ed0e --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white) +![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) +![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0) + +# gogn + +Simple no-frills .gitignore generator backed by [gitignore.io][gitignoreio]. + +![Selection Prompt](./img/selectionprompt.png) + +## Install + +```console +go install github.com/onyx-and-iris/gogn@latest +``` + +## Configuration + +*flags* + +- --height/-H: Height of the selection prompt (default 10) +- --filter/-f: Type of filter to apply to the list of templates (default startswith) + - may be one of (startswith, contains) +- --start-search/-s: Start the prompt in search mode (default false) + +*environment variables* + +```bash +#!/usr/bin/env bash + +export GOGN_HEIGHT=10 +export GOGN_FILTER=startswith +export GOGN_START_SEARCH=false +``` + +## Commands + +### New + +Trigger the selection prompt. + +```console +gogn new +``` + +Search mode can be activated by pressing `/`: + +![Prompt Filter](./img/promptfilter.png) + +## Special Thanks + +- [spf13](https://github.com/spf13) for the [cobra](https://github.com/spf13/cobra) and [viper](https://github.com/spf13/viper) packages. +- [Manifold](https://github.com/manifoldco) for the [promptui](https://github.com/manifoldco/promptui) package. + + +[gitignoreio]: https://www.toptal.com/developers/gitignore \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..70527c2 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,57 @@ +version: '3' + +vars: + PROGRAM: gogn + SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}' + BIN_DIR: bin + + WINDOWS: '{{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe' + LINUX: '{{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64' + GIT_COMMIT: + sh: git log -n 1 --format=%h + +tasks: + default: + desc: Build the gogn project + cmds: + - task: build + + build: + desc: Build the gogn project + deps: [vet] + cmds: + - task: build-windows + - task: build-linux + + vet: + desc: Vet the code + deps: [fmt] + cmds: + - go vet ./... + + fmt: + desc: Fmt the code + cmds: + - go fmt ./... + + build-windows: + desc: Build the gogn project for Windows + cmds: + - GOOS=windows GOARCH=amd64 go build -o {{.WINDOWS}} -ldflags="-X main.version={{.GIT_COMMIT}}" + internal: true + + build-linux: + desc: Build the gogn project for Linux + cmds: + - GOOS=linux GOARCH=amd64 go build -o {{.LINUX}} -ldflags="-X main.version={{.GIT_COMMIT}}" + internal: true + + test: + desc: Run tests + cmds: + - go test ./... + + clean: + desc: Clean the build artifacts + cmds: + - '{{.SHELL}} rm -r {{.BIN_DIR}}' diff --git a/context.go b/context.go new file mode 100644 index 0000000..3ff85d2 --- /dev/null +++ b/context.go @@ -0,0 +1,22 @@ +package main + +import ( + "context" + + "github.com/cuonglm/gogi" +) + +type contextKey string + +var clientKey = contextKey("client") + +// WithClient adds a gogi HTTP client to the context. +func WithClient(ctx context.Context, client *gogi.Client) context.Context { + return context.WithValue(ctx, clientKey, client) +} + +// ClientFromContext retrieves the gogi client from the context. +func ClientFromContext(ctx context.Context) (*gogi.Client, bool) { + client, ok := ctx.Value(clientKey).(*gogi.Client) + return client, ok +} diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..cc9ca13 --- /dev/null +++ b/filter.go @@ -0,0 +1,19 @@ +package main + +import ( + "strings" +) + +// filterFunc returns a function that filters templates based on the specified filter type. +func filterFunc(templates []string, filterType string) func(input string, index int) bool { + switch filterType { + case "contains": + return func(input string, index int) bool { + return strings.Contains(strings.ToLower(templates[index]), strings.ToLower(input)) + } + default: + return func(input string, index int) bool { + return strings.HasPrefix(strings.ToLower(templates[index]), strings.ToLower(input)) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..be61d6e --- /dev/null +++ b/go.mod @@ -0,0 +1,45 @@ +module github.com/onyx-and-iris/gogn + +go 1.24.3 + +require ( + github.com/charmbracelet/fang v0.1.0 + github.com/cuonglm/gogi v1.0.1 + github.com/manifoldco/promptui v0.9.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.1 +) + +require ( + github.com/charmbracelet/colorprofile v0.3.0 // indirect + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-cobra v1.2.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/roff v0.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f22d424 --- /dev/null +++ b/go.sum @@ -0,0 +1,110 @@ +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= +github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= +github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg= +github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:D9AJJuYTN5pvz6mpIGO1ijLKpfTYSHOtKGgwoTQ4Gog= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cuonglm/gogi v1.0.1 h1:Jotx6uAfFK6YHFOOek37R9y3Ae9qp/nUt/3mYGCl+44= +github.com/cuonglm/gogi v1.0.1/go.mod h1:ZLU5wl3d+FSSkiYYDpmPJI2dWdAGj8q28rFjpeWv1g4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/img/promptfilter.png b/img/promptfilter.png new file mode 100755 index 0000000000000000000000000000000000000000..a0c37ae88651109cc0c1cd370832c240465729fb GIT binary patch literal 5479 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 KLY6C_g8v5?O7Wfm literal 0 HcmV?d00001 diff --git a/img/selectionprompt.png b/img/selectionprompt.png new file mode 100755 index 0000000000000000000000000000000000000000..b1967959ae6c17b93d49cd77097b39bf22219196 GIT binary patch literal 9808 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 literal 0 HcmV?d00001 diff --git a/main.go b/main.go new file mode 100644 index 0000000..3ad1a51 --- /dev/null +++ b/main.go @@ -0,0 +1,68 @@ +// Package main provides the root command for the gogn CLI application. +package main + +import ( + "context" + "fmt" + "os" + "runtime/debug" + "strings" + + "github.com/charmbracelet/fang" + "github.com/cuonglm/gogi" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var version string // Version of the CLI, set during build time + +// rootCmd represents the base command when called without any subcommands. +var rootCmd = &cobra.Command{ + Use: "gogn", + Short: "A command-line interface for generating .gitignore files", + Long: `gogn is a command-line interface for generating .gitignore files. +It allows users to easily create and manage .gitignore files for various programming languages and frameworks. +You may also list available templates and generate .gitignore files based on those templates.`, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + client, err := gogi.NewHTTPClient() + if err != nil { + return fmt.Errorf("error creating HTTP client: %w", err) + } + cmd.SetContext(WithClient(cmd.Context(), client)) + return nil + }, +} + +func init() { + rootCmd.PersistentFlags().IntP("height", "H", 10, "Height of the selection prompt") + rootCmd.PersistentFlags(). + StringP("filter", "f", "startswith", "Type of filter to apply to the list of templates (e.g., 'startswith', 'contains')") + rootCmd.PersistentFlags().BoolP("start-search", "s", false, "Start the prompt in search mode") + + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.SetEnvPrefix("GOGN") + viper.AutomaticEnv() + viper.BindPFlag("height", rootCmd.PersistentFlags().Lookup("height")) + viper.BindPFlag("filter", rootCmd.PersistentFlags().Lookup("filter")) + viper.BindPFlag("start-search", rootCmd.PersistentFlags().Lookup("start-search")) +} + +// main is the entry point of the application. +// It executes the root command and handles any errors. +func main() { + if err := fang.Execute(context.Background(), rootCmd, fang.WithVersion(versionFromBuild())); err != nil { + os.Exit(1) + } +} + +func versionFromBuild() string { + if version == "" { + info, ok := debug.ReadBuildInfo() + if !ok { + return "(unable to read version)" + } + version = strings.Split(info.Main.Version, "-")[0] + } + + return version +} diff --git a/new.go b/new.go new file mode 100644 index 0000000..e8031fe --- /dev/null +++ b/new.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "io" + "maps" + "os" + "slices" + + "github.com/cuonglm/gogi" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const gitignoreFileName string = ".gitignore" + +// newCmd represents the new command. +var newCmd = &cobra.Command{ + Use: "new", + Short: "Create a new .gitignore file", + RunE: func(cmd *cobra.Command, args []string) error { + return runNewCommand(cmd, args) + }, +} + +func init() { + rootCmd.AddCommand(newCmd) +} + +type promptConfig struct { + Height int + StartSearch bool + FilterType string +} + +func runNewCommand(cmd *cobra.Command, _ []string) error { + pc := promptConfig{ + Height: viper.GetInt("height"), + StartSearch: viper.GetBool("start-search"), + FilterType: viper.GetString("filter"), + } + + client, ok := ClientFromContext(cmd.Context()) + if !ok { + return fmt.Errorf("failed to retrieve gogi client from context") + } + + content, err := runPrompt(client, &pc) + if err != nil { + return fmt.Errorf("failed to run prompt: %w", err) + } + + f, err := os.OpenFile(gitignoreFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) + if err != nil { + return fmt.Errorf("error opening file '%s': %w", gitignoreFileName, err) + } + defer f.Close() + + if err = commitGitignore(content, f); err != nil { + return fmt.Errorf("error committing gitignore file: %w", err) + } + + return nil +} + +// runPrompt is a helper function to run the selection prompt for .gitignore templates. +func runPrompt(client *gogi.Client, pc *promptConfig) (string, error) { + data, err := client.ListJson() + if err != nil { + return "", fmt.Errorf("error retrieving gitignore templates: %w", err) + } + templateNames := slices.Sorted(maps.Keys(data)) + + selectTemplates := &promptui.SelectTemplates{ + Label: ` {{ "\U0000007C" | faint }} {{ . | magenta | bold }}`, + Active: `{{ "\U0000007C" | faint }} {{ "🌶" | red }} {{ . | cyan | italic }}`, + Inactive: `{{ "\U0000007C" | faint }} {{ . | faint }}`, + Selected: `{{ "Created" | green }} {{ . }} {{ ".gitignore file ✓" | green }}`, + } + + prompt := promptui.Select{ + Label: "Select a .gitignore template", + Items: templateNames, + Templates: selectTemplates, + Size: pc.Height, + Searcher: filterFunc(templateNames, pc.FilterType), + StartInSearchMode: pc.StartSearch, + } + + i, _, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("error running prompt: %w", err) + } + + return data[templateNames[i]].Contents, nil +} + +// commitGitignore writes the content of the selected gitignore template to the .gitignore file. +func commitGitignore(content string, w io.Writer) error { + if _, err := fmt.Fprintf(w, "# Generated by gogn: github.com/onyx-and-iris/gogn\n"); err != nil { + return fmt.Errorf("error writing header to file '%s': %w", gitignoreFileName, err) + } + + if _, err := fmt.Fprintf(w, "%s", content); err != nil { + return fmt.Errorf("error writing to file '%s': %w", gitignoreFileName, err) + } + + if _, err := fmt.Fprintf(w, "\n# End of gogn\n"); err != nil { + return fmt.Errorf("error writing footer to file '%s': %w", gitignoreFileName, err) + } + + return nil +}