commit e622234993bf52b3c9b22aafc2bb3c4ab2f3aba8 Author: onyx-and-iris Date: Thu Jun 19 16:48:22 2025 +0100 initial commit 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 0000000..a0c37ae Binary files /dev/null and b/img/promptfilter.png differ diff --git a/img/selectionprompt.png b/img/selectionprompt.png new file mode 100755 index 0000000..b196795 Binary files /dev/null and b/img/selectionprompt.png differ 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 +}