From 263c6a39d4465719689fbe8826af137c09f06ff3 Mon Sep 17 00:00:00 2001 From: Pulkit Kathuria Date: Sun, 26 May 2024 23:13:41 +0900 Subject: [PATCH] (feat) init - go progress SVGs --- .github/workflows/coveritup.yml | 74 +++++++++++++++++++ .gitignore | 23 ++++++ .golangci.yaml | 22 ++++++ README.md | 89 ++++++++++++++++++++++ bar.go | 100 +++++++++++++++++++++++++ bar_test.go | 39 ++++++++++ circular.go | 126 ++++++++++++++++++++++++++++++++ circular_test.go | 40 ++++++++++ examples/bar/main.go | 39 ++++++++++ examples/circular/main.go | 41 +++++++++++ go.mod | 3 + utils/strings.go | 17 +++++ 12 files changed, 613 insertions(+) create mode 100644 .github/workflows/coveritup.yml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 README.md create mode 100644 bar.go create mode 100644 bar_test.go create mode 100644 circular.go create mode 100644 circular_test.go create mode 100644 examples/bar/main.go create mode 100644 examples/circular/main.go create mode 100644 go.mod create mode 100644 utils/strings.go diff --git a/.github/workflows/coveritup.yml b/.github/workflows/coveritup.yml new file mode 100644 index 0000000..bfc441c --- /dev/null +++ b/.github/workflows/coveritup.yml @@ -0,0 +1,74 @@ +on: + pull_request: + push: + tags-ignore: + - '**' + branches: + - '**' + +name: "Cover It Up" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +jobs: + coveritup: + strategy: + matrix: + go-version: [mod] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: kevincobain2000/action-gobrew@v2 + with: + version: ${{ matrix.go-version }} + + - name: Install Tools + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + go install github.com/axw/gocov/gocov@latest + go install github.com/AlekSi/gocov-xml@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + curl -sLk https://raw.githubusercontent.com/kevincobain2000/cover-totalizer/master/install.sh | sh + + - run: go mod tidy + - name: Lint Errors + uses: kevincobain2000/action-coveritup@v2 + with: + type: go-lint-errors + command: golangci-lint run ./... | grep -c "\^" + + - name: Test + uses: kevincobain2000/action-coveritup@v2 + with: + type: go-test-run-time + command: go test -race -v ./... -count=1 -coverprofile=coverage.out + record: runtime + + - name: Test + uses: kevincobain2000/action-coveritup@v2 + with: + type: go-build-time + command: go build + record: runtime + + - name: Coverage + run: gocov convert coverage.out | gocov-xml > coverage.xml + - name: Coveritup + uses: kevincobain2000/action-coveritup@v2 + with: + type: coverage + command: ./cover-totalizer coverage.xml + + - name: Number of dependencies + uses: kevincobain2000/action-coveritup@v2 + with: + type: go-mod-dependencies + command: go list -m all|wc -l|awk '{$1=$1};1' + + - uses: kevincobain2000/action-coveritup@v2 + with: + pr_comment: true + - uses: kevincobain2000/action-coveritup@v2 + with: + pr_comment: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98cdc6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +# diskcache directory +cache/* +vendor/* + +tmp/ + +.env +*.pid +dist/ +bin/ +main +.DS_Store +output.svg diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..c43f66f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,22 @@ +linters: + disable-all: true + # Enable specific linter + # https://golangci-lint.run/usage/linters/#enabled-by-default + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - dupl + - errorlint + - exportloopref + - goconst + - gocritic + - gocyclo + - goprintffuncname + - gosec + - prealloc + - revive + - stylecheck + - whitespace \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc43140 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +

+ svg circle progress sample +

+

+ SVG - Circle & Bar Progress generator +
+ in Golang. +
+
+

+ + +**Circle Progress:** Generate pure SVG circle progress bar. + +**Bar Progress:** Generate pure SVG bar progress bar. + +**Supports Captions:** Add captions horizontally or vertically. + +**Customizable:** Customize with various color, width, height, background and caption options. + +**Lightweight:** No dependencies, just a single file. + +**Beautiful:** Customizable to rounded corners, different colors, and caption options. + + +## Usage + +### Circle Progress + +```go +import ( + "fmt" + "os" + + gps "github.com/kevincobain2000/go-progress-svg" +) + +func main() { + circular, _ := gps.NewCircular(func(o *gps.CircularOptions) error { + o.Size = 200 + o.CircleWidth = 16 + o.ProgressWidth = 16 + o.CircleColor = "#e0e0e0" + o.ProgressColor = "#76e5b1" + o.TextColor = "#6bdba7" + o.TextSize = 52 + o.ShowPercentage = true + o.BackgroundColor = "" + o.Caption = "" + o.CaptionPos = "bottom" + o.CaptionSize = 20 + o.CaptionColor = "#000000" + return nil + }) + + circular.SVG() +} +``` + +### Bar Progress + +```go +import ( + "fmt" + "os" + + gps "github.com/kevincobain2000/go-progress-svg" +) + +func main() { + bar, _ := gps.NewBar(func(o *gps.BarOptions) error { + o.Progress = 97 + o.Width = 200 + o.Height = 50 + o.ProgressColor = "#76e5b1" + o.TextColor = "#6bdba7" + o.TextSize = 20 + o.ShowPercentage = true + o.Caption = "" + o.CaptionSize = 16 + o.CaptionColor = "#000000" + o.BackgroundColor = "#e0e0e0" + o.CornerRadius = 10 + return nil + }) + + bar.SVG() +} +``` diff --git a/bar.go b/bar.go new file mode 100644 index 0000000..91d5363 --- /dev/null +++ b/bar.go @@ -0,0 +1,100 @@ +package gps + +import ( + "fmt" + "strings" + "text/template" + + "github.com/kevincobain2000/go-progress-svg/utils" +) + +var barTPL = ` + + + + {{if .ShowPercentage}} + {{.Progress}}% + {{end}} + {{if .Caption}} + {{.Caption}} + {{end}} + +` + +type Bar struct { + options *BarOptions + strings *utils.Strings +} + +type BarOptions struct { + Progress int + Width int + Height int + ProgressWidth string + ProgressColor string + TextColor string + TextSize int + ShowPercentage bool + Caption string + CaptionSize int + CaptionColor string + BackgroundColor string + TotalHeight int + HeightHalf int + CaptionY int + CornerRadius int +} + +type BarOption func(*BarOptions) error + +func NewBar(opts ...BarOption) (*Bar, error) { + options := &BarOptions{ + Progress: 0, + Width: 200, + Height: 50, + ProgressColor: "#76e5b1", + TextColor: "#6bdba7", + TextSize: 20, + ShowPercentage: true, + Caption: "", + CaptionSize: 16, + CaptionColor: "#000000", + BackgroundColor: "#e0e0e0", + CornerRadius: 10, + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + + options.ProgressWidth = fmt.Sprintf("%d%%", options.Progress) + options.TotalHeight = options.Height + options.CaptionSize + 10 // Additional space for caption + options.HeightHalf = options.Height / 2 + options.CaptionY = options.Height + options.CaptionSize // Position caption below the bar + + return &Bar{ + options: options, + strings: utils.NewStrings(), + }, nil +} + +func (b *Bar) SVG() string { + tpl := b.strings.StripXMLWhitespace(barTPL) + tmpl, err := template.New("svg").Parse(tpl) + if err != nil { + fmt.Println("Error parsing template:", err) + return "" + } + + var rendered strings.Builder + err = tmpl.Execute(&rendered, b.options) + if err != nil { + fmt.Println("Error rendering template:", err) + return "" + } + + return rendered.String() +} diff --git a/bar_test.go b/bar_test.go new file mode 100644 index 0000000..a7ee69f --- /dev/null +++ b/bar_test.go @@ -0,0 +1,39 @@ +package gps + +import ( + "strings" + "testing" +) + +func TestBarSVG(t *testing.T) { + // Test rendering the SVG with default options + bar, err := NewBar() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + svg := bar.SVG() + expectedStart := ` +{{if .BackgroundColor}} + +{{end}} + + + {{if .ShowPercentage}} + {{.Progress}}% + {{else}} + {{.Progress}} + {{end}} + {{if .Caption}} + {{.Caption}} + {{end}} + +` + +var circularRightTPL = ` + +{{if .BackgroundColor}} + +{{end}} + + + {{if .ShowPercentage}} + {{.Progress}}% + {{else}} + {{.Progress}} + {{end}} + {{if .Caption}} + {{.Caption}} + {{end}} + +` + +type Circular struct { + options *CircularOptions + strings *utils.Strings +} + +type CircularOptions struct { + Progress int + Size int + CircleWidth int + ProgressWidth int + CircleColor string + ProgressColor string + TextColor string + TextSize int + ShowPercentage bool + BackgroundColor string + Caption string + CaptionPos string + CaptionSize int + CaptionColor string + Offset float64 +} + +type Option func(*CircularOptions) error + +func NewCircular(opts ...Option) (*Circular, error) { + options := &CircularOptions{ + Progress: 0, + Size: 200, + CircleWidth: 16, + ProgressWidth: 16, + CircleColor: "#e0e0e0", + ProgressColor: "#76e5b1", + TextColor: "#6bdba7", + TextSize: 52, + ShowPercentage: true, + BackgroundColor: "", + Caption: "", + CaptionPos: "bottom", + CaptionSize: 20, + CaptionColor: "#000000", + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + + options.Offset = 565.48 * (1 - float64(options.Progress)/100) + + return &Circular{ + options: options, + strings: utils.NewStrings(), + }, nil +} + +func (c *Circular) SVG() string { + tpl := "" + if c.options.CaptionPos == "right" { + tpl = c.strings.StripXMLWhitespace(circularRightTPL) + } else { + tpl = c.strings.StripXMLWhitespace(circularBottomTPL) + } + + tmpl, err := template.New("svg").Parse(tpl) + if err != nil { + fmt.Println("Error parsing template:", err) + return "" + } + + var rendered strings.Builder + err = tmpl.Execute(&rendered, c.options) + if err != nil { + fmt.Println("Error rendering template:", err) + return "" + } + return rendered.String() +} diff --git a/circular_test.go b/circular_test.go new file mode 100644 index 0000000..0a661d0 --- /dev/null +++ b/circular_test.go @@ -0,0 +1,40 @@ +package gps + +import ( + "strings" + "testing" +) + +func TestCircularSVG(t *testing.T) { + // Test rendering the SVG with default options + circular, err := NewCircular() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + svg := circular.SVG() + expectedStart := `\s+`).ReplaceAllString(xml, ">"), "<")) +}