dstlled-diff

Stars Top Language Last Commit Latest Release

dstlled-diff (short for “distilled-diff”) is a GitHub action that processes a specific git revision range and generates a diff that only includes changes in the signatures of “code constructs” (functions, methods, classes, traits, interfaces, objects, type aliases, enums, etc.). It is powered by dstll.

The main goal of dstlled-diff is to simplify the review of large structural code changes by removing diff components that do not alter signatures.

pr-comment

📜 Languages supported

  • go
  • python
  • rust
  • scala 2
  • more to come

Δ Difference to regular git diff

👉 Consider this (fairly long) git diff:

expand
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bb98703..b1b2a5c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,35 +1,31 @@
 name: build
 
 on:
   push:
-    branches: [ "main" ]
+    branches: ["main"]
   pull_request:
     paths:
       - "go.*"
       - "**/*.go"
       - ".github/workflows/*.yml"
 
 permissions:
   contents: read
 
 env:
-  GO_VERSION: '1.23.0'
+  GO_VERSION: '1.23.1'
 
 jobs:
   build:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest]
     runs-on: ${{ matrix.os }}
     steps:
-    - uses: actions/checkout@v4
-    - name: Set up Go
-      uses: actions/setup-go@v5
-      with:
-        go-version: ${{ env.GO_VERSION }}
-    - name: go build
-      run: go build -v ./...
-    - name: golangci-lint
-      uses: golangci/golangci-lint-action@v6
-      with:
-        version: v1.60
+      - uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ env.GO_VERSION }}
+      - name: go build
+        run: go build -v ./...
diff --git a/.github/workflows/dstlled-diff.yml b/.github/workflows/dstlled-diff.yml
new file mode 100644
index 0000000..7cbfcca
--- /dev/null
+++ b/.github/workflows/dstlled-diff.yml
@@ -0,0 +1,24 @@
+name: dstlled-diff
+
+on:
+  pull_request:
+    types: [opened, reopened, synchronize, ready_for_review]
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  dstlled-diff:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - id: get-dstlled-diff
+        uses: dhth/dstlled-diff-action@v0.1.2
+        with:
+          pattern: '**.go'
+          starting-commit: ${{ github.event.pull_request.base.sha }}
+          ending-commit: ${{ github.event.pull_request.head.sha }}
+          post-comment-on-pr: 'true'
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..bbd8e5d
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,30 @@
+name: lint
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    paths:
+      - "go.*"
+      - "**/*.go"
+      - ".github/workflows/*.yml"
+
+permissions:
+  contents: read
+
+env:
+  GO_VERSION: '1.23.1'
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ env.GO_VERSION }}
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v6
+        with:
+          version: v1.60
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a9fe1c0..a2f01bb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,40 +1,39 @@
 name: release
 
 on:
   push:
     tags:
       - 'v*'
 
+permissions:
+  id-token: write
+
 env:
-  GO_VERSION: '1.23.0'
+  GO_VERSION: '1.23.1'
 
 jobs:
-  build:
+  release:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
-      with:
-        fetch-depth: 0
-    - name: Set up Go
-      uses: actions/setup-go@v5
-      with:
-        go-version: ${{ env.GO_VERSION }}
-    - name: Build
-      run: go build -v ./...
-    - name: Install Cosign
-      uses: sigstore/cosign-installer@v3
-      with:
-        cosign-release: 'v2.2.3'
-    - name: Store Cosign private key in a file
-      run: 'echo "$COSIGN_KEY" > cosign.key'
-      shell: bash
-      env:
-        COSIGN_KEY: ${{secrets.COSIGN_KEY}}
-    - name: Release Binaries
-      uses: goreleaser/goreleaser-action@v6
-      with:
-        version: latest
-        args: release --clean
-      env:
-        GITHUB_TOKEN: ${{secrets.GH_PAT}}
-        COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ env.GO_VERSION }}
+      - name: Build
+        run: go build -v ./...
+      - name: Test
+        run: go test -v ./...
+      - name: Install Cosign
+        uses: sigstore/cosign-installer@v3
+        with:
+          cosign-release: 'v2.2.3'
+      - name: Release Binaries
+        uses: goreleaser/goreleaser-action@v6
+        with:
+          version: latest
+          args: release --clean
+        env:
+          GITHUB_TOKEN: ${{secrets.GH_PAT}}
diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml
index 092ed53..db473db 100644
--- a/.github/workflows/vulncheck.yml
+++ b/.github/workflows/vulncheck.yml
@@ -1,32 +1,31 @@
 name: vulncheck
 on:
   push:
-    branches: [ "main" ]
+    branches: ["main"]
   pull_request:
     paths:
       - "go.*"
       - "**/*.go"
       - ".github/workflows/*.yml"
 
 permissions:
   contents: read
 
 env:
-  GO_VERSION: '1.23.0'
+  GO_VERSION: '1.23.1'
 
 jobs:
   vulncheck:
     name: vulncheck
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
-    - name: Set up Go
-      uses: actions/setup-go@v5
-      with:
-        go-version: ${{ env.GO_VERSION }}
-    - name: govulncheck
-      shell: bash
-      run: |
-        go install golang.org/x/vuln/cmd/govulncheck@latest
-        govulncheck ./...
-
+      - uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ env.GO_VERSION }}
+      - name: govulncheck
+        shell: bash
+        run: |
+          go install golang.org/x/vuln/cmd/govulncheck@latest
+          govulncheck ./...
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..12df18a
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,60 @@
+linters:
+  enable:
+    - errcheck
+    - errname
+    - errorlint
+    - goconst
+    - gofumpt
+    - gosimple
+    - govet
+    - ineffassign
+    - nilerr
+    - prealloc
+    - predeclared
+    - revive
+    - rowserrcheck
+    - sqlclosecheck
+    - staticcheck
+    - testifylint
+    - thelper
+    - unconvert
+    - unused
+    - usestdlibvars
+    - wastedassign
+linters-settings:
+  revive:
+    rules:
+      # defaults
+      - name: blank-imports
+      - name: context-as-argument
+        arguments:
+          - allowTypesBefore: "*testing.T"
+      - name: context-keys-type
+      - name: dot-imports
+      - name: empty-block
+      - name: error-naming
+      - name: error-return
+      - name: error-strings
+      - name: errorf
+      - name: exported
+      - name: if-return
+      - name: increment-decrement
+      - name: indent-error-flow
+      - name: package-comments
+      - name: range
+      - name: receiver-naming
+      - name: redefines-builtin-id
+      - name: superfluous-else
+      - name: time-naming
+      - name: unexported-return
+      - name: unreachable-code
+      - name: unused-parameter
+      - name: var-declaration
+      - name: var-naming
+      # additional
+      - name: unnecessary-stmt
+      - name: deep-exit
+      - name: confusing-naming
+      - name: unused-receiver
+      - name: unhandled-error
+        arguments: ["fmt.Print", "fmt.Println", "fmt.Printf", "fmt.Fprintf", "fmt.Fprint"]
diff --git a/.goreleaser.yaml b/.goreleaser.yml
similarity index 74%
rename from .goreleaser.yaml
rename to .goreleaser.yml
index 689a601..925536d 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yml
@@ -1,46 +1,46 @@
 version: 2
 
 before:
   hooks:
     - go mod tidy
-    - go generate ./...
 
 builds:
   - env:
       - CGO_ENABLED=0
     goos:
       - linux
       - darwin
     goarch:
       - amd64
       - arm64
+
 signs:
   - cmd: cosign
-    stdin: "{{ .Env.COSIGN_PASSWORD }}"
+    signature: "${artifact}.sig"
+    certificate: "${artifact}.pem"
     args:
       - "sign-blob"
-      - "--key=cosign.key"
+      - "--oidc-issuer=https://token.actions.githubusercontent.com"
+      - "--output-certificate=${certificate}"
       - "--output-signature=${signature}"
       - "${artifact}"
-      - "--yes" # needed on cosign 2.0.0+
-    artifacts: all
-
+      - "--yes"
+    artifacts: checksum
 
 brews:
   - name: cueitup
     repository:
       owner: dhth
       name: homebrew-tap
     directory: Formula
     license: MIT
     homepage: "https://github.com/dhth/cueitup"
     description: "Inspect messages in an AWS SQS queue in a simple and deliberate manner"
 
 changelog:
   sort: asc
   filters:
     exclude:
       - "^docs:"
       - "^test:"
       - "^ci:"
-
diff --git a/cmd/root.go b/cmd/root.go
index 9bdc5c8..8d40f9b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,89 +1,94 @@
 package cmd
 
 import (
 	"context"
+	"errors"
 	"flag"
 	"fmt"
 	"os"
 	"strings"
 
 	"github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/service/sqs"
 	"github.com/dhth/cueitup/ui"
 	"github.com/dhth/cueitup/ui/model"
 )
 
-func die(msg string, args ...any) {
-	fmt.Fprintf(os.Stderr, msg+"\n", args...)
-	os.Exit(1)
-}
+var (
+	errQueueURLEmpty        = errors.New("queue URL is empty")
+	errAWSProfileEmpty      = errors.New("AWS profile is empty")
+	errAWSRegionEmpty       = errors.New("AWS region is empty")
+	errQueueURLIncorrect    = errors.New("queue URL is incorrect")
+	errMsgFormatInvalid     = errors.New("message format is invalid")
+	errInvalidFlagUsage     = errors.New("invalid flag usage")
+	errCouldntLoadAWSConfig = errors.New("couldn't load AWS config")
+)
 
 var (
-	queueUrl   = flag.String("queue-url", "", "url of the queue to consume from")
+	queueURL   = flag.String("queue-url", "", "url of the queue to consume from")
 	awsProfile = flag.String("aws-profile", "", "aws profile to use")
 	awsRegion  = flag.String("aws-region", "", "aws region to use")
 	msgFormat  = flag.String("msg-format", "json", "message format")
 	subsetKey  = flag.String("subset-key", "", "extract a nested object inside the JSON body")
 	contextKey = flag.String("context-key", "", "the key to use as for context in the list")
 )
 
-func Execute() {
-
+func Execute() error {
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, "Inspect messages in an AWS SQS queue in a simple and deliberate manner.\n\nFlags:\n")
 		flag.PrintDefaults()
 		fmt.Fprintf(os.Stderr, "\n------\n%s", model.HelpText)
 	}
 	flag.Parse()
 
-	if *queueUrl == "" {
-		die("queue-url cannot be empty")
-	} else if !strings.HasPrefix(*queueUrl, "https://") {
-		die("queue-url must begin with https")
+	if *queueURL == "" {
+		return errQueueURLEmpty
+	}
+
+	if !strings.HasPrefix(*queueURL, "https://") {
+		return fmt.Errorf("%w: must begin with https", errQueueURLIncorrect)
 	}
 
 	if *awsProfile == "" {
-		die("aws-profile cannot be empty")
+		return errAWSProfileEmpty
 	}
 
 	if *awsRegion == "" {
-		die("aws-region cannot be empty")
+		return errAWSRegionEmpty
 	}
 
 	var msgFmt model.MsgFmt
 	switch *msgFormat {
 	case "json":
-		msgFmt = model.JsonFmt
+		msgFmt = model.JSONFmt
 	case "plaintext":
 		msgFmt = model.PlainTxtFmt
 	default:
-		die("cueitup only supports the following msg-format values: json, plaintext")
+		return fmt.Errorf("%w: supported values: json, plaintext", errMsgFormatInvalid)
 	}
 
-	if *subsetKey != "" && msgFmt != model.JsonFmt {
-		die("subset-key can only be used when msg-format=json")
+	if *subsetKey != "" && msgFmt != model.JSONFmt {
+		return fmt.Errorf("%w: subset-key can only be used when msg-format=json", errInvalidFlagUsage)
 	}
-	if *contextKey != "" && msgFmt != model.JsonFmt {
-		die("context-key can only be used when msg-format=json")
+	if *contextKey != "" && msgFmt != model.JSONFmt {
+		return fmt.Errorf("%w: context-key can only be used when msg-format=json", errInvalidFlagUsage)
 	}
 
 	msgConsumptionConf := model.MsgConsumptionConf{
 		Format:     msgFmt,
 		SubsetKey:  *subsetKey,
 		ContextKey: *contextKey,
 	}
 
 	sdkConfig, err := config.LoadDefaultConfig(context.TODO(),
 		config.WithSharedConfigProfile(*awsProfile),
 		config.WithRegion(*awsRegion),
 	)
 	if err != nil {
-		fmt.Println("Error:", err)
-		os.Exit(1)
+		return fmt.Errorf("%w: %s", errCouldntLoadAWSConfig, err.Error())
 	}
 
 	sqsClient := sqs.NewFromConfig(sdkConfig)
 
-	ui.RenderUI(sqsClient, *queueUrl, msgConsumptionConf)
-
+	return ui.RenderUI(sqsClient, *queueURL, msgConsumptionConf)
 }
diff --git a/go.mod b/go.mod
index 32d834d..4ab9ba2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,44 +1,44 @@
 module github.com/dhth/cueitup
 
-go 1.23.0
+go 1.23.1
 
 require (
 	github.com/aws/aws-sdk-go-v2 v1.30.5
-	github.com/aws/aws-sdk-go-v2/config v1.27.32
-	github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7
-	github.com/charmbracelet/bubbles v0.19.0
-	github.com/charmbracelet/bubbletea v1.1.0
+	github.com/aws/aws-sdk-go-v2/config v1.27.33
+	github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8
+	github.com/charmbracelet/bubbles v0.20.0
+	github.com/charmbracelet/bubbletea v1.1.1
 	github.com/charmbracelet/lipgloss v0.13.0
 	github.com/tidwall/pretty v1.2.1
 )
 
 require (
 	github.com/atotto/clipboard v0.1.4 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.31 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
 	github.com/aws/smithy-go v1.20.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-	github.com/charmbracelet/x/ansi v0.2.3 // indirect
+	github.com/charmbracelet/x/ansi v0.3.1 // indirect
 	github.com/charmbracelet/x/term v0.2.0 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/termenv v0.15.2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sahilm/fuzzy v0.1.1 // indirect
 	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.24.0 // indirect
-	golang.org/x/text v0.17.0 // indirect
+	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/text v0.18.0 // indirect
 )
diff --git a/go.sum b/go.sum
index d44d80e..7c908a1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,50 +1,50 @@
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
 github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
-github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0=
-github.com/aws/aws-sdk-go-v2/config v1.27.32/go.mod h1:JibtzKJoXT0M/MhoYL6qfCk7nm/MppwukDFZtdgVRoY=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.31/go.mod h1:RSgY5lfCfw+FoyKWtOpLolPlfQVdDBQWTUniAaE+NKY=
+github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
+github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
-github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7 h1:RxETYGXhRlRxL96mtab1lQ9fPVPIJFXuOI3uRL/MuHI=
-github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7/go.mod h1:zn0Oy7oNni7XIGoAd6bHBTVtX06OrnpvT1kww8jxyi8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 h1:o++HUDXlbrTl4PSal3YHtdErQxB8mDGAtkKNXBWPfIU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.6/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 h1:yCHcQCOwTfIsc8DoEhM3qXPxD+j8CbI6t1K3dNzsWV0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0wpgLT0xtc73uAd8=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8 h1:t3TzmBX0lpDNtLhl7vY97VMvLtxp/KTvjjj2X3s6SUQ=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8/go.mod h1:zn0Oy7oNni7XIGoAd6bHBTVtX06OrnpvT1kww8jxyi8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
 github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
 github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
-github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
-github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
-github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
-github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
+github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
+github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
 github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
 github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
-github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
-github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/ansi v0.3.1 h1:CRO6lc/6HCx2/D6S/GZ87jDvRvk6GtPyFP+IljkNtqI=
+github.com/charmbracelet/x/ansi v0.3.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
 github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
 github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -62,14 +62,14 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
 github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
 github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
-golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
-golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
diff --git a/cueitup.go b/main.go
similarity index 54%
rename from cueitup.go
rename to main.go
index 4a28df2..0480ca2 100644
--- a/cueitup.go
+++ b/main.go
@@ -1,9 +1,14 @@
 package main
 
 import (
+	"os"
+
 	"github.com/dhth/cueitup/cmd"
 )
 
 func main() {
-	cmd.Execute()
+	err := cmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
 }
diff --git a/ui/model/cmds.go b/ui/model/cmds.go
index 837bbeb..038dbd3 100644
--- a/ui/model/cmds.go
+++ b/ui/model/cmds.go
@@ -8,91 +8,86 @@ import (
 	"strconv"
 	"strings"
 	"time"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/sqs"
 	"github.com/aws/aws-sdk-go-v2/service/sqs/types"
 	tea "github.com/charmbracelet/bubbletea"
 )
 
-func (m model) FetchMessages(maxMessages int32, waitTime int32) tea.Cmd {
+func (m Model) FetchMessages(maxMessages int32, waitTime int32) tea.Cmd {
 	return func() tea.Msg {
-
 		var messages []types.Message
 		var messagesValues []string
 		var keyValues []string
 		result, err := m.sqsClient.ReceiveMessage(context.TODO(),
 			// WaitTimeSeconds > 0 enables long polling
 			// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html#sqs-long-polling
 			&sqs.ReceiveMessageInput{
-				QueueUrl:            aws.String(m.queueUrl),
+				QueueUrl:            aws.String(m.queueURL),
 				MaxNumberOfMessages: maxMessages,
 				WaitTimeSeconds:     waitTime,
 				VisibilityTimeout:   30,
 			})
 		if err != nil {
 			return SQSMsgFetchedMsg{
 				messages: nil,
 				err:      err,
 			}
-		} else {
-			messages = result.Messages
-			for _, message := range messages {
-				msgValue, keyValue, _ := getMessageData(&message, m.msgConsumptionConf)
-				messagesValues = append(messagesValues, msgValue)
-				keyValues = append(keyValues, keyValue)
-			}
+		}
+		messages = result.Messages
+		for _, message := range messages {
+			msgValue, keyValue, _ := getMessageData(&message, m.msgConsumptionConf)
+			messagesValues = append(messagesValues, msgValue)
+			keyValues = append(keyValues, keyValue)
 		}
 
 		return SQSMsgFetchedMsg{
 			messages:      messages,
 			messageValues: messagesValues,
 			keyValues:     keyValues,
 			err:           nil,
 		}
 	}
 }
 
-func DeleteMessages(client *sqs.Client, queueUrl string, messages []types.Message) tea.Cmd {
+func DeleteMessages(client *sqs.Client, queueURL string, messages []types.Message) tea.Cmd {
 	return func() tea.Msg {
-
 		entries := make([]types.DeleteMessageBatchRequestEntry, len(messages))
 		for msgIndex := range messages {
 			entries[msgIndex].Id = aws.String(fmt.Sprintf("%v", msgIndex))
 			entries[msgIndex].ReceiptHandle = messages[msgIndex].ReceiptHandle
 		}
 		_, err := client.DeleteMessageBatch(context.TODO(),
 			&sqs.DeleteMessageBatchInput{
 				Entries:  entries,
-				QueueUrl: aws.String(queueUrl),
+				QueueUrl: aws.String(queueURL),
 			})
 		if err != nil {
 			return SQSMsgsDeletedMsg{
 				err: err,
 			}
 		}
 
 		return SQSMsgsDeletedMsg{}
 	}
 }
 
-func GetQueueMsgCount(client *sqs.Client, queueUrl string) tea.Cmd {
+func GetQueueMsgCount(client *sqs.Client, queueURL string) tea.Cmd {
 	return func() tea.Msg {
-
 		approxMsgCountType := types.QueueAttributeNameApproximateNumberOfMessages
 		attribute, err := client.GetQueueAttributes(context.TODO(),
 			&sqs.GetQueueAttributesInput{
-				QueueUrl:       aws.String(queueUrl),
+				QueueUrl:       aws.String(queueURL),
 				AttributeNames: []types.QueueAttributeName{approxMsgCountType},
 			})
-
 		if err != nil {
 			return QueueMsgCountFetchedMsg{
 				approxMsgCount: -1,
 				err:            err,
 			}
 		}
 
 		countStr := attribute.Attributes[string(approxMsgCountType)]
 		count, err := strconv.Atoi(countStr)
 		if err != nil {
@@ -103,32 +98,32 @@ func GetQueueMsgCount(client *sqs.Client, queueUrl string) tea.Cmd {
 		}
 		return QueueMsgCountFetchedMsg{
 			approxMsgCount: count,
 		}
 	}
 }
 
 func saveRecordValueToDisk(filePath string, msgValue string, msgFmt MsgFmt) tea.Cmd {
 	return func() tea.Msg {
 		dir := filepath.Dir(filePath)
-		err := os.MkdirAll(dir, 0755)
+		err := os.MkdirAll(dir, 0o755)
 		if err != nil {
 			return RecordSavedToDiskMsg{err: err}
 		}
 		var data string
 		switch msgFmt {
-		case JsonFmt:
+		case JSONFmt:
 			data = fmt.Sprintf("json\n%s\n", msgValue)
 		case PlainTxtFmt:
 			data = msgValue
 		}
-		err = os.WriteFile(filePath, []byte(data), 0644)
+		err = os.WriteFile(filePath, []byte(data), 0o644)
 		if err != nil {
 			return RecordSavedToDiskMsg{err: err}
 		}
 		return RecordSavedToDiskMsg{path: filePath}
 	}
 }
 
 func setContextSearchValues(userInput string) tea.Cmd {
 	return func() tea.Msg {
 		valuesEls := strings.Split(userInput, ",")
diff --git a/ui/model/delegate.go b/ui/model/delegate.go
index 80805d4..be199a1 100644
--- a/ui/model/delegate.go
+++ b/ui/model/delegate.go
@@ -11,35 +11,34 @@ func newAppItemDelegate() list.DefaultDelegate {
 	d := list.NewDefaultDelegate()
 
 	d.Styles.SelectedTitle = d.Styles.
 		SelectedTitle.
 		Foreground(lipgloss.Color(listColor)).
 		BorderLeftForeground(lipgloss.Color(listColor))
 	d.Styles.SelectedDesc = d.Styles.
 		SelectedTitle
 
 	d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd {
-		switch msgType := msg.(type) {
-		case tea.KeyMsg:
-			switch {
-			case key.Matches(msgType,
-				list.DefaultKeyMap().CursorUp,
-				list.DefaultKeyMap().CursorDown,
-				list.DefaultKeyMap().GoToStart,
-				list.DefaultKeyMap().GoToEnd,
-				list.DefaultKeyMap().NextPage,
-				list.DefaultKeyMap().PrevPage,
-			):
-				selected := m.SelectedItem()
-				if selected == nil {
-					return nil
-				}
-				key := selected.FilterValue()
-				return showItemDetails(key)
+		keyMsg, keyMsgOK := msg.(tea.KeyMsg)
+		if !keyMsgOK {
+			return nil
+		}
+		if key.Matches(keyMsg,
+			list.DefaultKeyMap().CursorUp,
+			list.DefaultKeyMap().CursorDown,
+			list.DefaultKeyMap().GoToStart,
+			list.DefaultKeyMap().GoToEnd,
+			list.DefaultKeyMap().NextPage,
+			list.DefaultKeyMap().PrevPage,
+		) {
+			selected := m.SelectedItem()
+			if selected == nil {
+				return nil
 			}
-
+			key := selected.FilterValue()
+			return showItemDetails(key)
 		}
 		return nil
 	}
 
 	return d
 }
diff --git a/ui/model/help.go b/ui/model/help.go
index 76c4bc5..7f9c7d6 100644
--- a/ui/model/help.go
+++ b/ui/model/help.go
@@ -1,61 +1,59 @@
 package model
 
 import "fmt"
 
-var (
-	HelpText = fmt.Sprintf(`
+var HelpText = fmt.Sprintf(`
   %s
 %s
   %s
 
   %s
 %s
   %s
 %s
   %s
 %s
 `,
-		helpHeaderStyle.Render("cueitup Reference Manual"),
-		helpSectionStyle.Render(`
+	helpHeaderStyle.Render("cueitup Reference Manual"),
+	helpSectionStyle.Render(`
   (scroll line by line with j/k/arrow keys or by half a page with <c-d>/<c-u>)
 
   cueitup has 3 views:
   - Message List View
   - Message Value View
   - Help View (this one)
 `),
-		helpHeaderStyle.Render("Keyboard Shortcuts"),
-		helpHeaderStyle.Render("General"),
-		helpSectionStyle.Render(`
+	helpHeaderStyle.Render("Keyboard Shortcuts"),
+	helpHeaderStyle.Render("General"),
+	helpSectionStyle.Render(`
       <tab>                          Switch focus to next section
       <s-tab>                        Switch focus to previous section
       1                              Maximize message value view
       ?                              Show help view
 `),
-		helpHeaderStyle.Render("Message List View"),
-		helpSectionStyle.Render(`
+	helpHeaderStyle.Render("Message List View"),
+	helpSectionStyle.Render(`
       h/<Up>                         Move cursor up
       k/<Down>                       Move cursor down
       n                              Fetch the next message from the queue
       N                              Fetch up to 10 more messages from the queue
       }                              Fetch up to 100 more messages from the queue
       d                              Toggle deletion mode; cueitup will delete messages
                                          after reading them
       <ctrl+s>                       Toggle contextual search prompt
       <ctrl+f>                       Toggle contextual filtering ON/OFF
       <ctrl+p>                       Toggle queue message count polling ON/OFF; ON by default
       p                              Toggle persist mode (cueitup will start persisting
                                          messages, at the location
                                          messages/<topic-name>/<timestamp-when-cueitup-started>/<unix-epoch>-<message-id>.md
       s                              Toggle skipping mode; cueitup will consume messages,
                                          but not populate its internal list, effectively
                                          skipping over them
 `),
-		helpHeaderStyle.Render("Message Value View   "),
-		helpSectionStyle.Render(`
+	helpHeaderStyle.Render("Message Value View   "),
+	helpSectionStyle.Render(`
       q                              Minimize section, and return focus to list view
       [,h                            Show details for the previous entry in the list
       ],l                            Show details for the next entry in the list
 `),
-	)
 )
diff --git a/ui/model/initial.go b/ui/model/initial.go
index e037600..28dd396 100644
--- a/ui/model/initial.go
+++ b/ui/model/initial.go
@@ -5,45 +5,44 @@ import (
 	"os"
 	"strings"
 	"time"
 
 	"github.com/aws/aws-sdk-go-v2/service/sqs"
 	"github.com/charmbracelet/bubbles/list"
 	"github.com/charmbracelet/bubbles/textinput"
 	"github.com/charmbracelet/lipgloss"
 )
 
-func InitialModel(sqsClient *sqs.Client, queueUrl string, msgConsumptionConf MsgConsumptionConf) model {
-
+func InitialModel(sqsClient *sqs.Client, queueURL string, msgConsumptionConf MsgConsumptionConf) Model {
 	appDelegate := newAppItemDelegate()
 	jobItems := make([]list.Item, 0)
 
-	queueParts := strings.Split(queueUrl, "/")
+	queueParts := strings.Split(queueURL, "/")
 	queueName := queueParts[len(queueParts)-1]
 	currentTime := time.Now()
 	timeString := currentTime.Format("2006-01-02-15-04-05")
 	persistDir := fmt.Sprintf("messages/%s/%s", queueName, timeString)
 
 	ti := textinput.New()
 	ti.Prompt = fmt.Sprintf("Filter messages where %s in > ", msgConsumptionConf.ContextKey)
 	ti.Focus()
 	ti.CharLimit = 100
 	ti.Width = 100
 
 	var dbg bool
 	if len(os.Getenv("DEBUG")) > 0 {
 		dbg = true
 	}
 
-	m := model{
+	m := Model{
 		sqsClient:            sqsClient,
-		queueUrl:             queueUrl,
+		queueURL:             queueURL,
 		msgConsumptionConf:   msgConsumptionConf,
 		pollForQueueMsgCount: true,
 		msgsList:             list.New(jobItems, appDelegate, listWidth+10, 0),
 		recordValueStore:     make(map[string]string),
 		persistDir:           persistDir,
 		contextSearchInput:   ti,
 		showHelpIndicator:    true,
 		debugMode:            dbg,
 		firstFetch:           true,
 	}
diff --git a/ui/model/model.go b/ui/model/model.go
index 73f2a00..cce4fc8 100644
--- a/ui/model/model.go
+++ b/ui/model/model.go
@@ -15,36 +15,36 @@ type stateView uint
 const (
 	msgsListView stateView = iota
 	msgValueView
 	helpView
 	contextualSearchView
 )
 
 type MsgFmt uint
 
 const (
-	JsonFmt MsgFmt = iota
+	JSONFmt MsgFmt = iota
 	PlainTxtFmt
 )
 
 const msgCountTickInterval = time.Second * 3
 
 type MsgConsumptionConf struct {
 	Format     MsgFmt
 	SubsetKey  string
 	ContextKey string
 }
 
-type model struct {
+type Model struct {
 	deserializationFmt   MsgFmt
 	sqsClient            *sqs.Client
-	queueUrl             string
+	queueURL             string
 	msgConsumptionConf   MsgConsumptionConf
 	activeView           stateView
 	lastView             stateView
 	pollForQueueMsgCount bool
 	msgsList             list.Model
 	helpVP               viewport.Model
 	showHelpIndicator    bool
 	msgValueVP           viewport.Model
 	recordValueStore     map[string]string
 	contextSearchInput   textinput.Model
@@ -58,17 +58,17 @@ type model struct {
 	helpVPReady          bool
 	vpFullScreen         bool
 	terminalWidth        int
 	terminalHeight       int
 	message              string
 	errorMsg             string
 	debugMode            bool
 	firstFetch           bool
 }
 
-func (m model) Init() tea.Cmd {
+func (m Model) Init() tea.Cmd {
 	return tea.Batch(
-		GetQueueMsgCount(m.sqsClient, m.queueUrl),
+		GetQueueMsgCount(m.sqsClient, m.queueURL),
 		tickEvery(msgCountTickInterval),
 		hideHelp(time.Minute*1),
 	)
 }
diff --git a/ui/model/msgs.go b/ui/model/msgs.go
index 15ec498..e1e52a4 100644
--- a/ui/model/msgs.go
+++ b/ui/model/msgs.go
@@ -1,18 +1,20 @@
 package model
 
 import (
 	"github.com/aws/aws-sdk-go-v2/service/sqs/types"
 )
 
-type MsgCountTickMsg struct{}
-type HideHelpMsg struct{}
+type (
+	MsgCountTickMsg struct{}
+	HideHelpMsg     struct{}
+)
 
 type SQSMsgFetchedMsg struct {
 	messages      []types.Message
 	messageValues []string
 	keyValues     []string
 	err           error
 }
 
 type QueueMsgCountFetchedMsg struct {
 	approxMsgCount int
diff --git a/ui/model/types.go b/ui/model/types.go
index 565b8cb..48c5646 100644
--- a/ui/model/types.go
+++ b/ui/model/types.go
@@ -18,12 +18,12 @@ func (item msgItem) Title() string {
 }
 
 func (item msgItem) Description() string {
 	if item.contextKeyValue != "" {
 		return RightPadTrim(fmt.Sprintf("%s: %s", RightPadTrim(item.contextKeyName, 10), item.contextKeyValue), listWidth)
 	}
 	return ""
 }
 
 func (item msgItem) FilterValue() string {
-	return string(*item.message.MessageId)
+	return *item.message.MessageId
 }
diff --git a/ui/model/update.go b/ui/model/update.go
index ba99b5b..41ae3b9 100644
--- a/ui/model/update.go
+++ b/ui/model/update.go
@@ -3,23 +3,26 @@ package model
 import (
 	"fmt"
 	"time"
 
 	"github.com/charmbracelet/bubbles/list"
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/tidwall/pretty"
 )
 
-const useHighPerformanceRenderer = false
+const (
+	useHighPerformanceRenderer = false
+	fetchingIndicator          = " ..."
+)
 
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	m.message = ""
 	m.errorMsg = ""
 
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "ctrl+c", "q":
 			switch m.activeView {
 			case msgsListView:
@@ -41,31 +44,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case "enter":
 			if m.activeView == contextualSearchView {
 				m.activeView = m.lastView
 				if len(m.contextSearchInput.Value()) > 0 {
 					cmds = append(cmds, setContextSearchValues(m.contextSearchInput.Value()))
 				} else {
 					m.filterMessages = false
 				}
 			}
 		case "n", " ":
-			m.message = " ..."
+			m.message = fetchingIndicator
 			cmds = append(cmds, m.FetchMessages(1, 0))
 		case "N":
-			m.message = " ..."
+			m.message = fetchingIndicator
 			for i := 0; i < 10; i++ {
 				cmds = append(cmds,
 					m.FetchMessages(1, 0),
 				)
 			}
 		case "}":
-			m.message = " ..."
+			m.message = fetchingIndicator
 			for i := 0; i < 20; i++ {
 				cmds = append(cmds,
 					m.FetchMessages(5, 0),
 				)
 			}
 		case "?":
 			m.lastView = m.activeView
 			m.activeView = helpView
 		case "d":
 			if m.activeView == msgsListView {
@@ -96,21 +99,21 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				selected := m.msgsList.SelectedItem()
 				if selected != nil {
 					result := string(pretty.Color([]byte(m.recordValueStore[selected.FilterValue()]), nil))
 					m.msgValueVP.SetContent(result)
 				}
 			}
 		case "ctrl+p":
 			m.pollForQueueMsgCount = !m.pollForQueueMsgCount
 			if m.pollForQueueMsgCount {
 				cmds = append(cmds,
-					tea.Batch(GetQueueMsgCount(m.sqsClient, m.queueUrl),
+					tea.Batch(GetQueueMsgCount(m.sqsClient, m.queueURL),
 						tickEvery(msgCountTickInterval),
 					),
 				)
 			}
 		case "ctrl+s":
 			if m.activeView == msgsListView {
 				m.lastView = m.activeView
 				m.activeView = contextualSearchView
 			}
 		case "ctrl+f":
@@ -184,82 +187,84 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.contextSearchInput.SetValue("")
 		m.filterMessages = true
 
 	case HideHelpMsg:
 		m.showHelpIndicator = false
 
 	case SQSMsgFetchedMsg:
 		if msg.err != nil {
 			m.errorMsg = msg.err.Error()
 		} else {
-			switch m.skipRecords {
-			case false:
+			if !m.skipRecords {
 				vPresenceMap := make(map[string]bool)
 				if m.filterMessages && len(m.contextSearchValues) > 0 {
 					for _, p := range m.contextSearchValues {
 						vPresenceMap[p] = true
 					}
 				}
 				for i, message := range msg.messages {
-
 					// only save/persist values that are requested to be filtered
 					if m.filterMessages && !(msg.keyValues[i] != "" && vPresenceMap[msg.keyValues[i]]) {
 						continue
 					}
 
 					m.msgsList.InsertItem(len(m.msgsList.Items()),
-						msgItem{message: message,
+						msgItem{
+							message:         message,
 							messageValue:    msg.messageValues[i],
 							contextKeyName:  m.msgConsumptionConf.ContextKey,
 							contextKeyValue: msg.keyValues[i],
 						},
 					)
 					m.recordValueStore[*message.MessageId] = msg.messageValues[i]
+
 					if m.persistRecords {
 						prefix := time.Now().Unix()
 						filePath := fmt.Sprintf("%s/%d-%s.md", m.persistDir, prefix, *message.MessageId)
 						cmds = append(cmds,
 							saveRecordValueToDisk(
 								filePath,
 								msg.messageValues[i],
 								m.msgConsumptionConf.Format,
 							),
 						)
 					}
 				}
+
 				if m.deleteMsgs {
 					cmds = append(cmds,
 						DeleteMessages(m.sqsClient,
-							m.queueUrl,
+							m.queueURL,
 							msg.messages),
 					)
 				}
+
 				if m.firstFetch {
 					selected := m.msgsList.SelectedItem()
 					if selected != nil {
 						result := string(pretty.Color([]byte(m.recordValueStore[selected.FilterValue()]), nil))
 						m.msgValueVP.SetContent(result)
 						m.firstFetch = false
 					}
 				}
 			}
 		}
 	case KMsgChosenMsg:
 		switch m.deserializationFmt {
-		case JsonFmt:
+		case JSONFmt:
 			result := string(pretty.Color([]byte(m.recordValueStore[msg.key]), nil))
 			m.msgValueVP.SetContent(result)
 		default:
 			m.msgValueVP.SetContent(m.recordValueStore[msg.key])
 		}
 	case MsgCountTickMsg:
-		cmds = append(cmds, GetQueueMsgCount(m.sqsClient, m.queueUrl))
+		cmds = append(cmds, GetQueueMsgCount(m.sqsClient, m.queueURL))
 		if m.pollForQueueMsgCount {
 			cmds = append(cmds, tickEvery(msgCountTickInterval))
 		}
 	case QueueMsgCountFetchedMsg:
 		if msg.err != nil {
 			m.errorMsg = msg.err.Error()
 		} else {
 			m.msgsList.Title = fmt.Sprintf("Messages (%d in queue)", msg.approxMsgCount)
 		}
 	}
diff --git a/ui/model/utils.go b/ui/model/utils.go
index 21bdc83..24bb3d3 100644
--- a/ui/model/utils.go
+++ b/ui/model/utils.go
@@ -80,30 +80,29 @@ func getRecordValueJSONNested(message *types.Message, extractKey string, context
 	}
 
 	return string(nestedBytes), contextualValue.(string), nil
 }
 
 func getMessageData(message *types.Message, msgConsumptionConf MsgConsumptionConf) (string, string, error) {
 	var msgValue, keyValue string
 	var err error
 
 	switch msgConsumptionConf.Format {
-	case JsonFmt:
+	case JSONFmt:
 		if msgConsumptionConf.SubsetKey != "" {
 			msgValue,
 				keyValue,
 				err = getRecordValueJSONNested(message,
 				msgConsumptionConf.SubsetKey,
 				msgConsumptionConf.ContextKey,
 			)
 		} else {
 			msgValue, err = getRecordValueJSONFull(message)
 		}
 	case PlainTxtFmt:
 		msgValue = *message.Body
 	}
 	if err != nil {
 		return "", "", err
-	} else {
-		return msgValue, keyValue, nil
 	}
+	return msgValue, keyValue, nil
 }
diff --git a/ui/model/view.go b/ui/model/view.go
index 5de1437..5e2bf63 100644
--- a/ui/model/view.go
+++ b/ui/model/view.go
@@ -1,23 +1,21 @@
 package model
 
 import (
 	"fmt"
 
 	"github.com/charmbracelet/lipgloss"
 )
 
-var (
-	listWidth = 50
-)
+var listWidth = 50
 
-func (m model) View() string {
+func (m Model) View() string {
 	var content string
 	var footer string
 	var mode string
 	var statusBar string
 	var debugMsg string
 	var msgValVPTitleStyle lipgloss.Style
 
 	if m.message != "" {
 		statusBar = Trim(m.message, 120)
 	}
diff --git a/ui/ui.go b/ui/ui.go
index f13c6e7..381c448 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -1,27 +1,26 @@
 package ui
 
 import (
+	"errors"
 	"fmt"
-	"log"
 	"os"
 
 	"github.com/aws/aws-sdk-go-v2/service/sqs"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/dhth/cueitup/ui/model"
 )
 
-func RenderUI(sqsClient *sqs.Client, queueUrl string, msgConsumptionConf model.MsgConsumptionConf) {
+var errFailedToConfigureDebugging = errors.New("failed to configure debugging")
 
+func RenderUI(sqsClient *sqs.Client, queueURL string, msgConsumptionConf model.MsgConsumptionConf) error {
 	if len(os.Getenv("DEBUG")) > 0 {
 		f, err := tea.LogToFile("debug.log", "debug")
 		if err != nil {
-			fmt.Println("fatal:", err)
-			os.Exit(1)
+			return fmt.Errorf("%w: %s", errFailedToConfigureDebugging, err.Error())
 		}
 		defer f.Close()
 	}
-	p := tea.NewProgram(model.InitialModel(sqsClient, queueUrl, msgConsumptionConf), tea.WithAltScreen())
-	if _, err := p.Run(); err != nil {
-		log.Fatalf("Something went wrong %s", err)
-	}
+	p := tea.NewProgram(model.InitialModel(sqsClient, queueURL, msgConsumptionConf), tea.WithAltScreen())
+	_, err := p.Run()
+	return err
 }
diff --git a/yamlfmt.yml b/yamlfmt.yml
new file mode 100644
index 0000000..9d3236a
--- /dev/null
+++ b/yamlfmt.yml
@@ -0,0 +1,2 @@
+formatter:
+  retain_line_breaks_single: true

👉 The “distilled-diff” version of the same diff looks like the following:

expand
diff --git a/5c52f68/cmd/root.go b/11a0436/cmd/root.go
index 7e245d5..a1c36f6 100644
--- a/5c52f68/cmd/root.go
+++ b/11a0436/cmd/root.go
@@ -1,2 +1 @@
-func die(msg string, args ...any)
-func Execute()
+func Execute() error
diff --git a/5c52f68/cueitup.go b/11a0436/main.go
similarity index 100%
rename from 5c52f68/cueitup.go
rename to 11a0436/main.go
diff --git a/5c52f68/ui/model/cmds.go b/11a0436/ui/model/cmds.go
index 35aaf4d..e429709 100644
--- a/5c52f68/ui/model/cmds.go
+++ b/11a0436/ui/model/cmds.go
@@ -1,2 +1,2 @@
-func DeleteMessages(client *sqs.Client, queueUrl string, messages []types.Message) tea.Cmd
-func GetQueueMsgCount(client *sqs.Client, queueUrl string) tea.Cmd
+func DeleteMessages(client *sqs.Client, queueURL string, messages []types.Message) tea.Cmd
+func GetQueueMsgCount(client *sqs.Client, queueURL string) tea.Cmd
@@ -8 +8 @@ func hideHelp(interval time.Duration) tea.Cmd
-func (m model) FetchMessages(maxMessages int32, waitTime int32) tea.Cmd
+func (m Model) FetchMessages(maxMessages int32, waitTime int32) tea.Cmd
diff --git a/5c52f68/ui/model/initial.go b/11a0436/ui/model/initial.go
index 1381c76..7654528 100644
--- a/5c52f68/ui/model/initial.go
+++ b/11a0436/ui/model/initial.go
@@ -1 +1 @@
-func InitialModel(sqsClient *sqs.Client, queueUrl string, msgConsumptionConf MsgConsumptionConf) model
+func InitialModel(sqsClient *sqs.Client, queueURL string, msgConsumptionConf MsgConsumptionConf) Model
diff --git a/5c52f68/ui/model/model.go b/11a0436/ui/model/model.go
index 30e48cb..992323e 100644
--- a/5c52f68/ui/model/model.go
+++ b/11a0436/ui/model/model.go
@@ -8 +8 @@ type MsgConsumptionConf struct {
-type model struct {
+type Model struct {
@@ -11 +11 @@ type model struct {
-	queueUrl             string
+	queueURL             string
@@ -38 +38 @@ type model struct {
-func (m model) Init() tea.Cmd
+func (m Model) Init() tea.Cmd
diff --git a/5c52f68/ui/model/msgs.go b/11a0436/ui/model/msgs.go
index 38d7c0c..48a036f 100644
--- a/5c52f68/ui/model/msgs.go
+++ b/11a0436/ui/model/msgs.go
@@ -1,2 +1,4 @@
-type MsgCountTickMsg struct{}
-type HideHelpMsg struct{}
+type (
+	MsgCountTickMsg struct{}
+	HideHelpMsg     struct{}
+)
diff --git a/5c52f68/ui/model/update.go b/11a0436/ui/model/update.go
index 09d4d9e..63ebf89 100644
--- a/5c52f68/ui/model/update.go
+++ b/11a0436/ui/model/update.go
@@ -1 +1 @@
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
diff --git a/5c52f68/ui/model/view.go b/11a0436/ui/model/view.go
index c62ab48..80c398e 100644
--- a/5c52f68/ui/model/view.go
+++ b/11a0436/ui/model/view.go
@@ -1 +1 @@
-func (m model) View() string
+func (m Model) View() string
diff --git a/5c52f68/ui/ui.go b/11a0436/ui/ui.go
index f8e0ecf..ebdc12a 100644
--- a/5c52f68/ui/ui.go
+++ b/11a0436/ui/ui.go
@@ -1 +1 @@
-func RenderUI(sqsClient *sqs.Client, queueUrl string, msgConsumptionConf model.MsgConsumptionConf)
+func RenderUI(sqsClient *sqs.Client, queueURL string, msgConsumptionConf model.MsgConsumptionConf) error

As seen above, the “distilled” version only shows changes in function signatures, and type definitions, which makes maks reviewing large-scale refactors easier.

⚡️ Usage

name: dstlled-diff

on:
  pull_request:

jobs:
  dstlled-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: get-dstlled-diff
        uses: dhth/dstlled-diff-action@v0.1.0
        with:
          starting-commit: ${{ github.event.pull_request.base.sha }}
          ending-commit: ${{ github.event.pull_request.head.sha }}
          post-comment-on-pr: 'true'

The “dstlled-diff” will be available as an output of the action, via steps.get-dstlled-diff.outputs.diff.

🔡 Inputs

Following inputs can be used as step.with keys:

NameTypeDefaultDescription
starting-commitStringStarting commit for the git revision range
ending-commitStringEnding commit for the git revision range
patternString*Pattern to run dstlled-diff on
directoryString.Working directory (below repository root)
post-comment-on-prBoolfalsePost comment containing dstlled-diff to corresponding pull request
save-diff-to-fileBoolfalseSave diff to a local file called diff.patch

⚙️ Use cases

Web Interface

The output of dstlled-diff can be rendered in a web view (see it running here), as seen in the image below. Code for this can be found here.

web-demo