mirror of
https://github.com/Valkyrie00/bold-brew.git
synced 2026-03-14 14:25:53 +01:00
chore: release version 2.0.0 - Cask support and XDG compliance (#30)
* feat: add leaves filter to show explicitly installed packages (#25) Add new filter [L] to display only "leaf" packages - those installed explicitly by the user and not as dependencies of other packages. * refactor: Migrate to Podman with OCI Containerfile and enhanced Makefile (#26) * refactor: migrate from Docker to Podman with OCI Containerfile Replace Docker with Podman for better security and OCI compliance. Switch from Dockerfile to standard Containerfile format. * chore: upgrade Go from 1.24 to 1.25 Update Go version to 1.25 to support latest goreleaser v2 and benefit from improved performance and language features. * refactor: migrate to Podman and enhance Makefile Replace Docker with Podman and upgrade Makefile with help system and new developer-friendly targets. * chore: upgrade to Go 1.25 and golangci-lint v2.5.0 Update Go to 1.25 and golangci-lint to v2.5.0 for better tooling support. * feat: add security scanning with govulncheck and gosec (#27) Add comprehensive security scanning to the project with vulnerability checks and static analysis tools. * feat: Add complete Casks support with unified UI (#28) * feat(cask): add backend support for Homebrew casks Implement complete backend infrastructure for managing Homebrew casks alongside formulae, preparing for unified UI. * feat(cask): add complete Homebrew casks support with unified UI Implement full backend and UI support for managing Homebrew casks alongside formulae in a unified interface. * fix(cask): parse cask analytics correctly Fix cask analytics not being displayed (showing 0 for all casks). * feat(cask): add complete Homebrew casks support with unified UI Implement full backend and UI support for managing Homebrew casks alongside formulae in a unified interface. * fix: create copy to avoid implicit memory aliasing * feat: implement XDG Base Directory Specification with github.com/adrg/xdg (#29) Implement XDG Base Directory Specification using the github.com/adrg/xdg package for robust cross-platform support.
This commit is contained in:
parent
edae4d747e
commit
6c80585431
16 changed files with 761 additions and 185 deletions
8
.env
8
.env
|
|
@ -1,10 +1,6 @@
|
|||
APP_NAME=bbrew
|
||||
APP_VERSION=0.0.1-local
|
||||
|
||||
### Docker
|
||||
DOCKER_IMAGE_NAME=bbrew
|
||||
|
||||
### Build
|
||||
BUILD_GOVERSION=1.20
|
||||
CONTAINER_IMAGE_NAME=bbrew
|
||||
BUILD_GOVERSION=1.25
|
||||
BUILD_GOOS=darwin
|
||||
BUILD_GOARCH=arm64
|
||||
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
|
|
@ -16,11 +16,11 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
go-version: '1.25'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.1
|
||||
version: v2.5.0
|
||||
skip-cache: true
|
||||
|
||||
build:
|
||||
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
go-version: '1.25'
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
- name: Build
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
|
|
|
|||
53
.github/workflows/security.yml
vendored
Normal file
53
.github/workflows/security.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Security
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
govulncheck:
|
||||
name: Go Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
- name: Run govulncheck
|
||||
run: govulncheck ./...
|
||||
|
||||
gosec:
|
||||
name: Security Scanner
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: '-no-fail -fmt sarif -out results.sarif ./...'
|
||||
|
||||
- name: Upload SARIF file
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
11
Containerfile
Normal file
11
Containerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM golang:1.25
|
||||
|
||||
# Install dependencies
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
|
||||
RUN go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
# Install security tools
|
||||
RUN go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
RUN go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
WORKDIR /app
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
FROM golang:1.24
|
||||
|
||||
# Install dependencies
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
|
||||
RUN go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
WORKDIR /app
|
||||
140
Makefile
140
Makefile
|
|
@ -1,62 +1,148 @@
|
|||
##############################
|
||||
# VARIABLES
|
||||
##############################
|
||||
ifneq (,$(wildcard ./.env))
|
||||
include .env
|
||||
export
|
||||
endif
|
||||
%:@
|
||||
# Load .env if exists (loaded first so defaults can override if not set)
|
||||
-include .env
|
||||
|
||||
# Default values (can be overridden by .env or command line)
|
||||
APP_NAME ?= bbrew
|
||||
APP_VERSION ?= 0.0.1-local
|
||||
CONTAINER_IMAGE_NAME ?= bbrew
|
||||
BUILD_GOVERSION ?= 1.25
|
||||
BUILD_GOOS ?= $(shell go env GOOS)
|
||||
BUILD_GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
# Container runtime command
|
||||
CONTAINER_RUN = podman run --rm -v $(PWD):/app $(CONTAINER_IMAGE_NAME)
|
||||
|
||||
##############################
|
||||
# DOCKER
|
||||
# HELP
|
||||
##############################
|
||||
.PHONY: docker-build-image
|
||||
docker-build-image:
|
||||
@docker build -t $(DOCKER_IMAGE_NAME) .
|
||||
.PHONY: help
|
||||
help: ## Show this help message
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort
|
||||
|
||||
docker-build-force-recreate:
|
||||
@docker build --no-cache -t $(DOCKER_IMAGE_NAME) .
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
##############################
|
||||
# CONTAINER
|
||||
##############################
|
||||
.PHONY: container-build-image
|
||||
container-build-image: ## Build container image
|
||||
@podman build -f Containerfile -t $(CONTAINER_IMAGE_NAME) .
|
||||
|
||||
.PHONY: container-build-force
|
||||
container-build-force: ## Force rebuild container image (no cache)
|
||||
@podman build --no-cache -f Containerfile -t $(CONTAINER_IMAGE_NAME) .
|
||||
|
||||
.PHONY: container-clean
|
||||
container-clean: ## Remove container image
|
||||
@podman rmi $(CONTAINER_IMAGE_NAME) 2>/dev/null || true
|
||||
|
||||
##############################
|
||||
# RELEASE
|
||||
##############################
|
||||
.PHONY: release-snapshot
|
||||
release-snapshot: docker-build-image # Builds the project in snapshot mode and releases it [This is used for testing releases]
|
||||
@docker run --rm -v $(PWD):/app $(DOCKER_IMAGE_NAME) goreleaser release --snapshot --clean
|
||||
release-snapshot: container-build-image ## Build and release snapshot (testing)
|
||||
@$(CONTAINER_RUN) goreleaser release --snapshot --clean
|
||||
|
||||
.PHONY: build-snapshot # Builds the project in snapshot mode [This is used for testing releases]
|
||||
build-snapshot: docker-build-image
|
||||
@docker run --rm -v $(PWD):/app $(DOCKER_IMAGE_NAME) goreleaser build --snapshot --clean
|
||||
.PHONY: build-snapshot
|
||||
build-snapshot: container-build-image ## Build snapshot without release
|
||||
@$(CONTAINER_RUN) goreleaser build --snapshot --clean
|
||||
|
||||
##############################
|
||||
# BUILD
|
||||
##############################
|
||||
.PHONY: build
|
||||
build: docker-build-image
|
||||
@docker run --rm -v $(PWD):/app $(DOCKER_IMAGE_NAME) \
|
||||
env GOOS=$(BUILD_GOOS) GOARCH=$(BUILD_GOARCH) go build -o $(APP_NAME) ./cmd/$(APP_NAME)
|
||||
build: container-build-image ## Build the application binary
|
||||
@$(CONTAINER_RUN) env GOOS=$(BUILD_GOOS) GOARCH=$(BUILD_GOARCH) \
|
||||
go build -o $(APP_NAME) ./cmd/$(APP_NAME)
|
||||
|
||||
.PHONY: build-local
|
||||
build-local: ## Build locally without container (requires Go installed)
|
||||
@go build -o $(APP_NAME) ./cmd/$(APP_NAME)
|
||||
|
||||
.PHONY: run
|
||||
run: build
|
||||
./$(APP_NAME)
|
||||
run: build ## Build and run the application
|
||||
@./$(APP_NAME)
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Clean build artifacts
|
||||
@rm -f $(APP_NAME)
|
||||
@rm -rf dist/
|
||||
|
||||
##############################
|
||||
# HELPER
|
||||
# QUALITY
|
||||
##############################
|
||||
.PHONY: quality
|
||||
quality: docker-build-image
|
||||
@docker run --rm -v $(PWD):/app $(DOCKER_IMAGE_NAME) golangci-lint run
|
||||
quality: container-build-image ## Run linter checks
|
||||
@$(CONTAINER_RUN) golangci-lint run
|
||||
|
||||
.PHONY: quality-local
|
||||
quality-local: ## Run linter locally (requires golangci-lint installed)
|
||||
@golangci-lint run
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run tests
|
||||
@go test -v ./...
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## Run tests with coverage
|
||||
@go test -v -coverprofile=coverage.out ./...
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
##############################
|
||||
# SECURITY
|
||||
##############################
|
||||
.PHONY: security
|
||||
security: security-vuln security-scan ## Run all security checks
|
||||
|
||||
.PHONY: security-vuln
|
||||
security-vuln: container-build-image ## Check for known vulnerabilities
|
||||
@$(CONTAINER_RUN) govulncheck ./...
|
||||
|
||||
.PHONY: security-vuln-local
|
||||
security-vuln-local: ## Check vulnerabilities locally (requires govulncheck)
|
||||
@govulncheck ./...
|
||||
|
||||
.PHONY: security-scan
|
||||
security-scan: container-build-image ## Run security scanner
|
||||
@$(CONTAINER_RUN) gosec ./...
|
||||
|
||||
.PHONY: security-scan-local
|
||||
security-scan-local: ## Run security scanner locally (requires gosec)
|
||||
@gosec ./...
|
||||
|
||||
##############################
|
||||
# WEBSITE
|
||||
##############################
|
||||
.PHONY: build-site
|
||||
build-site:
|
||||
build-site: ## Build the static website
|
||||
@node build.js
|
||||
|
||||
.PHONY: serve-site
|
||||
serve-site:
|
||||
serve-site: ## Serve the website locally
|
||||
@npx http-server docs -p 3000
|
||||
|
||||
.PHONY: dev-site
|
||||
dev-site: build-site serve-site
|
||||
dev-site: build-site serve-site ## Build and serve the website
|
||||
|
||||
##############################
|
||||
# UTILITY
|
||||
##############################
|
||||
.PHONY: install
|
||||
install: build-local ## Install binary to $GOPATH/bin
|
||||
@go install ./cmd/$(APP_NAME)
|
||||
|
||||
.PHONY: deps
|
||||
deps: ## Download and tidy dependencies
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
.PHONY: deps-update
|
||||
deps-update: ## Update all dependencies
|
||||
@go get -u ./...
|
||||
@go mod tidy
|
||||
|
|
|
|||
9
go.mod
9
go.mod
|
|
@ -1,18 +1,19 @@
|
|||
module bbrew
|
||||
|
||||
go 1.24
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.8.1
|
||||
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb
|
||||
golang.org/x/text v0.26.0
|
||||
golang.org/x/text v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
)
|
||||
|
|
|
|||
25
go.sum
25
go.sum
|
|
@ -1,20 +1,14 @@
|
|||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc=
|
||||
github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/rivo/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE=
|
||||
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
||||
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=
|
||||
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
@ -55,36 +49,33 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
|
|||
31
internal/models/cask.go
Normal file
31
internal/models/cask.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package models
|
||||
|
||||
// Cask represents a Homebrew cask (GUI application).
|
||||
type Cask struct {
|
||||
Token string `json:"token"`
|
||||
FullToken string `json:"full_token"`
|
||||
OldTokens []string `json:"old_tokens"`
|
||||
Tap string `json:"tap"`
|
||||
Name []string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
Homepage string `json:"homepage"`
|
||||
URL string `json:"url"`
|
||||
Version string `json:"version"`
|
||||
Installed *string `json:"installed"` // Null if not installed, version string if installed
|
||||
InstalledTime *int64 `json:"installed_time"` // Unix timestamp
|
||||
Outdated bool `json:"outdated"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
DeprecationDate interface{} `json:"deprecation_date"`
|
||||
DeprecationReason interface{} `json:"deprecation_reason"`
|
||||
Disabled bool `json:"disabled"`
|
||||
DisableDate interface{} `json:"disable_date"`
|
||||
DisableReason interface{} `json:"disable_reason"`
|
||||
TapGitHead string `json:"tap_git_head"`
|
||||
RubySourcePath string `json:"ruby_source_path"`
|
||||
RubySourceChecksum RubySourceChecksum `json:"ruby_source_checksum"`
|
||||
Analytics90dRank int // Internal: Populated from analytics
|
||||
Analytics90dDownloads int // Internal: Populated from analytics
|
||||
LocallyInstalled bool `json:"-"` // Internal flag
|
||||
IsCask bool `json:"-"` // Internal flag to distinguish from formulae
|
||||
}
|
||||
|
|
@ -67,7 +67,8 @@ type Analytics struct {
|
|||
|
||||
type AnalyticsItem struct {
|
||||
Number int `json:"number"`
|
||||
Formula string `json:"formula"`
|
||||
Formula string `json:"formula"` // For formula analytics
|
||||
Cask string `json:"cask"` // For cask analytics
|
||||
Count string `json:"count"`
|
||||
Percent string `json:"percent"`
|
||||
}
|
||||
|
|
|
|||
79
internal/models/package.go
Normal file
79
internal/models/package.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package models
|
||||
|
||||
// PackageType distinguishes between formulae and casks.
|
||||
type PackageType string
|
||||
|
||||
const (
|
||||
PackageTypeFormula PackageType = "formula"
|
||||
PackageTypeCask PackageType = "cask"
|
||||
)
|
||||
|
||||
// Package represents a unified view of both Formula and Cask for UI display.
|
||||
type Package struct {
|
||||
// Common fields
|
||||
Name string // Formula.Name or Cask.Token
|
||||
DisplayName string // Formula.FullName or Cask.Name[0]
|
||||
Description string // desc
|
||||
Homepage string // homepage
|
||||
Version string // versions.stable or version
|
||||
LocallyInstalled bool // Is installed locally
|
||||
Outdated bool // Needs update
|
||||
Type PackageType // formula or cask
|
||||
Analytics90dRank int
|
||||
Analytics90dDownloads int
|
||||
|
||||
// Original data (for operations)
|
||||
Formula *Formula `json:"-"` // nil if Type == cask
|
||||
Cask *Cask `json:"-"` // nil if Type == formula
|
||||
|
||||
// For leaves filter (only meaningful for formulae)
|
||||
InstalledOnRequest bool
|
||||
}
|
||||
|
||||
// NewPackageFromFormula creates a Package from a Formula.
|
||||
func NewPackageFromFormula(f *Formula) Package {
|
||||
installedOnRequest := false
|
||||
if len(f.Installed) > 0 {
|
||||
installedOnRequest = f.Installed[0].InstalledOnRequest
|
||||
}
|
||||
|
||||
return Package{
|
||||
Name: f.Name,
|
||||
DisplayName: f.FullName,
|
||||
Description: f.Description,
|
||||
Homepage: f.Homepage,
|
||||
Version: f.Versions.Stable,
|
||||
LocallyInstalled: f.LocallyInstalled,
|
||||
Outdated: f.Outdated,
|
||||
Type: PackageTypeFormula,
|
||||
Analytics90dRank: f.Analytics90dRank,
|
||||
Analytics90dDownloads: f.Analytics90dDownloads,
|
||||
Formula: f,
|
||||
Cask: nil,
|
||||
InstalledOnRequest: installedOnRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPackageFromCask creates a Package from a Cask.
|
||||
func NewPackageFromCask(c *Cask) Package {
|
||||
displayName := c.Token
|
||||
if len(c.Name) > 0 {
|
||||
displayName = c.Name[0]
|
||||
}
|
||||
|
||||
return Package{
|
||||
Name: c.Token,
|
||||
DisplayName: displayName,
|
||||
Description: c.Description,
|
||||
Homepage: c.Homepage,
|
||||
Version: c.Version,
|
||||
LocallyInstalled: c.LocallyInstalled,
|
||||
Outdated: c.Outdated,
|
||||
Type: PackageTypeCask,
|
||||
Analytics90dRank: c.Analytics90dRank,
|
||||
Analytics90dDownloads: c.Analytics90dDownloads,
|
||||
Formula: nil,
|
||||
Cask: c,
|
||||
InstalledOnRequest: true, // Casks are always explicitly installed
|
||||
}
|
||||
}
|
||||
|
|
@ -32,10 +32,12 @@ type AppService struct {
|
|||
theme *theme.Theme
|
||||
layout ui.LayoutInterface
|
||||
|
||||
packages *[]models.Formula
|
||||
filteredPackages *[]models.Formula
|
||||
packages *[]models.Package
|
||||
filteredPackages *[]models.Package
|
||||
showOnlyInstalled bool
|
||||
showOnlyOutdated bool
|
||||
showOnlyLeaves bool
|
||||
showOnlyCasks bool
|
||||
brewVersion string
|
||||
|
||||
brewService BrewServiceInterface
|
||||
|
|
@ -54,10 +56,12 @@ var NewAppService = func() AppServiceInterface {
|
|||
theme: themeService,
|
||||
layout: layout,
|
||||
|
||||
packages: new([]models.Formula),
|
||||
filteredPackages: new([]models.Formula),
|
||||
packages: new([]models.Package),
|
||||
filteredPackages: new([]models.Package),
|
||||
showOnlyInstalled: false,
|
||||
showOnlyOutdated: false,
|
||||
showOnlyLeaves: false,
|
||||
showOnlyCasks: false,
|
||||
brewVersion: "-",
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +89,7 @@ func (s *AppService) Boot() (err error) {
|
|||
}
|
||||
|
||||
// Initialize packages and filteredPackages
|
||||
s.packages = s.brewService.GetFormulae()
|
||||
s.packages = s.brewService.GetPackages()
|
||||
*s.filteredPackages = *s.packages
|
||||
return nil
|
||||
}
|
||||
|
|
@ -104,13 +108,13 @@ func (s *AppService) updateHomeBrew() {
|
|||
|
||||
// search filters the packages based on the search text and the current filter state.
|
||||
func (s *AppService) search(searchText string, scrollToTop bool) {
|
||||
var filteredList []models.Formula
|
||||
var filteredList []models.Package
|
||||
uniquePackages := make(map[string]bool)
|
||||
|
||||
// Determine the source list based on the current filter state
|
||||
sourceList := s.packages
|
||||
if s.showOnlyInstalled && !s.showOnlyOutdated {
|
||||
sourceList = &[]models.Formula{}
|
||||
sourceList = &[]models.Package{}
|
||||
for _, info := range *s.packages {
|
||||
if info.LocallyInstalled {
|
||||
*sourceList = append(*sourceList, info)
|
||||
|
|
@ -119,7 +123,7 @@ func (s *AppService) search(searchText string, scrollToTop bool) {
|
|||
}
|
||||
|
||||
if s.showOnlyOutdated {
|
||||
sourceList = &[]models.Formula{}
|
||||
sourceList = &[]models.Package{}
|
||||
for _, info := range *s.packages {
|
||||
if info.LocallyInstalled && info.Outdated {
|
||||
*sourceList = append(*sourceList, info)
|
||||
|
|
@ -127,6 +131,24 @@ func (s *AppService) search(searchText string, scrollToTop bool) {
|
|||
}
|
||||
}
|
||||
|
||||
if s.showOnlyLeaves {
|
||||
sourceList = &[]models.Package{}
|
||||
for _, info := range *s.packages {
|
||||
if info.LocallyInstalled && info.InstalledOnRequest {
|
||||
*sourceList = append(*sourceList, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.showOnlyCasks {
|
||||
sourceList = &[]models.Package{}
|
||||
for _, info := range *s.packages {
|
||||
if info.Type == models.PackageTypeCask {
|
||||
*sourceList = append(*sourceList, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if searchText == "" {
|
||||
// Reset to the appropriate list when the search string is empty
|
||||
filteredList = *sourceList
|
||||
|
|
@ -159,10 +181,10 @@ func (s *AppService) search(searchText string, scrollToTop bool) {
|
|||
s.setResults(s.filteredPackages, scrollToTop)
|
||||
}
|
||||
|
||||
// forceRefreshResults forces a refresh of the Homebrew formulae data and updates the results in the UI.
|
||||
// forceRefreshResults forces a refresh of the Homebrew formulae and cask data and updates the results in the UI.
|
||||
func (s *AppService) forceRefreshResults() {
|
||||
_ = s.brewService.SetupData(true)
|
||||
s.packages = s.brewService.GetFormulae()
|
||||
s.packages = s.brewService.GetPackages()
|
||||
*s.filteredPackages = *s.packages
|
||||
|
||||
s.app.QueueUpdateDraw(func() {
|
||||
|
|
@ -171,37 +193,42 @@ func (s *AppService) forceRefreshResults() {
|
|||
}
|
||||
|
||||
// setResults updates the results table with the provided data and optionally scrolls to the top.
|
||||
func (s *AppService) setResults(data *[]models.Formula, scrollToTop bool) {
|
||||
func (s *AppService) setResults(data *[]models.Package, scrollToTop bool) {
|
||||
s.layout.GetTable().Clear()
|
||||
s.layout.GetTable().SetTableHeaders("Name", "Version", "Description", "↓ (90d)")
|
||||
s.layout.GetTable().SetTableHeaders("Type", "Name", "Version", "Description", "↓ (90d)")
|
||||
|
||||
for i, info := range *data {
|
||||
version := info.Versions.Stable
|
||||
if len(info.Installed) > 0 {
|
||||
// Check if the installed version is the same as the stable version (handle revisions)
|
||||
if strings.HasPrefix(info.Installed[0].Version, info.Versions.Stable) {
|
||||
version = info.Installed[0].Version
|
||||
} else if info.Installed[0].Version != info.Versions.Stable {
|
||||
version = fmt.Sprintf("%s → %s", info.Installed[0].Version, info.Versions.Stable)
|
||||
}
|
||||
// Type cell with escaped brackets
|
||||
typeTag := tview.Escape("[F]") // Formula
|
||||
if info.Type == models.PackageTypeCask {
|
||||
typeTag = tview.Escape("[C]") // Cask
|
||||
}
|
||||
typeCell := tview.NewTableCell(typeTag).SetSelectable(true).SetAlign(tview.AlignLeft)
|
||||
|
||||
// Version handling
|
||||
version := info.Version
|
||||
|
||||
// Name cell
|
||||
nameCell := tview.NewTableCell(info.Name).SetSelectable(true)
|
||||
if info.LocallyInstalled {
|
||||
nameCell.SetTextColor(tcell.ColorGreen)
|
||||
}
|
||||
|
||||
// Version cell
|
||||
versionCell := tview.NewTableCell(version).SetSelectable(true)
|
||||
if info.LocallyInstalled && info.Outdated {
|
||||
versionCell.SetTextColor(tcell.ColorOrange)
|
||||
}
|
||||
|
||||
// Downloads cell
|
||||
downloadsCell := tview.NewTableCell(fmt.Sprintf("%d", info.Analytics90dDownloads)).SetSelectable(true).SetAlign(tview.AlignRight)
|
||||
|
||||
s.layout.GetTable().View().SetCell(i+1, 0, nameCell.SetExpansion(0))
|
||||
s.layout.GetTable().View().SetCell(i+1, 1, versionCell.SetExpansion(0))
|
||||
s.layout.GetTable().View().SetCell(i+1, 2, tview.NewTableCell(info.Description).SetSelectable(true).SetExpansion(1))
|
||||
s.layout.GetTable().View().SetCell(i+1, 3, downloadsCell.SetExpansion(0))
|
||||
// Set cells with new column order: Type, Name, Version, Description, Downloads
|
||||
s.layout.GetTable().View().SetCell(i+1, 0, typeCell.SetExpansion(0))
|
||||
s.layout.GetTable().View().SetCell(i+1, 1, nameCell.SetExpansion(0))
|
||||
s.layout.GetTable().View().SetCell(i+1, 2, versionCell.SetExpansion(0))
|
||||
s.layout.GetTable().View().SetCell(i+1, 3, tview.NewTableCell(info.Description).SetSelectable(true).SetExpansion(1))
|
||||
s.layout.GetTable().View().SetCell(i+1, 4, downloadsCell.SetExpansion(0))
|
||||
}
|
||||
|
||||
// Update the details view with the first item in the list
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bbrew/internal/models"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rivo/tview"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -14,33 +13,53 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const FormulaeAPIURL = "https://formulae.brew.sh/api/formula.json"
|
||||
const CaskAPIURL = "https://formulae.brew.sh/api/cask.json"
|
||||
const AnalyticsAPIURL = "https://formulae.brew.sh/api/analytics/install-on-request/90d.json"
|
||||
const CaskAnalyticsAPIURL = "https://formulae.brew.sh/api/analytics/cask-install/90d.json"
|
||||
|
||||
// getCacheDir - returns the cache directory following XDG Base Directory Specification.
|
||||
func getCacheDir() string {
|
||||
return filepath.Join(xdg.CacheHome, "bbrew")
|
||||
}
|
||||
|
||||
type BrewServiceInterface interface {
|
||||
GetPrefixPath() (path string)
|
||||
GetFormulae() (formulae *[]models.Formula)
|
||||
GetPackages() (packages *[]models.Package)
|
||||
SetupData(forceDownload bool) (err error)
|
||||
GetBrewVersion() (version string, err error)
|
||||
|
||||
UpdateHomebrew() error
|
||||
UpdateAllPackages(app *tview.Application, outputView *tview.TextView) error
|
||||
UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
|
||||
RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
|
||||
InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
|
||||
UpdatePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
|
||||
RemovePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
|
||||
InstallPackage(info models.Package, app *tview.Application, outputView *tview.TextView) error
|
||||
}
|
||||
|
||||
// BrewService provides methods to interact with Homebrew, including
|
||||
// retrieving formulae, managing packages, and handling analytics.
|
||||
// retrieving formulae, casks, and handling analytics.
|
||||
type BrewService struct {
|
||||
// Package lists
|
||||
// Formula lists
|
||||
all *[]models.Formula
|
||||
installed *[]models.Formula
|
||||
remote *[]models.Formula
|
||||
analytics map[string]models.AnalyticsItem
|
||||
|
||||
// Cask lists
|
||||
allCasks *[]models.Cask
|
||||
installedCasks *[]models.Cask
|
||||
remoteCasks *[]models.Cask
|
||||
caskAnalytics map[string]models.AnalyticsItem
|
||||
|
||||
// Unified package list
|
||||
allPackages *[]models.Package
|
||||
|
||||
brewVersion string
|
||||
prefixPath string
|
||||
}
|
||||
|
|
@ -48,9 +67,13 @@ type BrewService struct {
|
|||
// NewBrewService creates a new instance of BrewService with initialized package lists.
|
||||
var NewBrewService = func() BrewServiceInterface {
|
||||
return &BrewService{
|
||||
all: new([]models.Formula),
|
||||
installed: new([]models.Formula),
|
||||
remote: new([]models.Formula),
|
||||
all: new([]models.Formula),
|
||||
installed: new([]models.Formula),
|
||||
remote: new([]models.Formula),
|
||||
allCasks: new([]models.Cask),
|
||||
installedCasks: new([]models.Cask),
|
||||
remoteCasks: new([]models.Cask),
|
||||
allPackages: new([]models.Package),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,18 +130,106 @@ func (s *BrewService) GetFormulae() (formulae *[]models.Formula) {
|
|||
return s.all
|
||||
}
|
||||
|
||||
// SetupData initializes the BrewService by loading installed packages, remote formulae, and analytics data.
|
||||
// GetPackages retrieves all packages (formulae + casks), merging remote and installed.
|
||||
func (s *BrewService) GetPackages() (packages *[]models.Package) {
|
||||
packageMap := make(map[string]models.Package)
|
||||
|
||||
// Add REMOTE formulae
|
||||
for _, formula := range *s.remote {
|
||||
if _, exists := packageMap[formula.Name]; !exists {
|
||||
f := formula // Create a copy to avoid implicit memory aliasing
|
||||
pkg := models.NewPackageFromFormula(&f)
|
||||
// Merge analytics data
|
||||
if a, exists := s.analytics[formula.Name]; exists && a.Number > 0 {
|
||||
downloads, _ := strconv.Atoi(strings.ReplaceAll(a.Count, ",", ""))
|
||||
pkg.Analytics90dRank = a.Number
|
||||
pkg.Analytics90dDownloads = downloads
|
||||
}
|
||||
packageMap[formula.Name] = pkg
|
||||
}
|
||||
}
|
||||
|
||||
// Add INSTALLED formulae (override remote data)
|
||||
for _, formula := range *s.installed {
|
||||
f := formula // Create a copy to avoid implicit memory aliasing
|
||||
pkg := models.NewPackageFromFormula(&f)
|
||||
// Merge analytics data
|
||||
if a, exists := s.analytics[formula.Name]; exists && a.Number > 0 {
|
||||
downloads, _ := strconv.Atoi(strings.ReplaceAll(a.Count, ",", ""))
|
||||
pkg.Analytics90dRank = a.Number
|
||||
pkg.Analytics90dDownloads = downloads
|
||||
}
|
||||
packageMap[formula.Name] = pkg
|
||||
}
|
||||
|
||||
// Add REMOTE casks
|
||||
for _, cask := range *s.remoteCasks {
|
||||
if _, exists := packageMap[cask.Token]; !exists {
|
||||
c := cask // Create a copy to avoid implicit memory aliasing
|
||||
pkg := models.NewPackageFromCask(&c)
|
||||
// Merge analytics data
|
||||
if a, exists := s.caskAnalytics[cask.Token]; exists && a.Number > 0 {
|
||||
downloads, _ := strconv.Atoi(strings.ReplaceAll(a.Count, ",", ""))
|
||||
pkg.Analytics90dRank = a.Number
|
||||
pkg.Analytics90dDownloads = downloads
|
||||
}
|
||||
packageMap[cask.Token] = pkg
|
||||
}
|
||||
}
|
||||
|
||||
// Add INSTALLED casks (override remote data)
|
||||
for _, cask := range *s.installedCasks {
|
||||
c := cask // Create a copy to avoid implicit memory aliasing
|
||||
pkg := models.NewPackageFromCask(&c)
|
||||
// Merge analytics data
|
||||
if a, exists := s.caskAnalytics[cask.Token]; exists && a.Number > 0 {
|
||||
downloads, _ := strconv.Atoi(strings.ReplaceAll(a.Count, ",", ""))
|
||||
pkg.Analytics90dRank = a.Number
|
||||
pkg.Analytics90dDownloads = downloads
|
||||
}
|
||||
packageMap[cask.Token] = pkg
|
||||
}
|
||||
|
||||
// Convert map to slice
|
||||
*s.allPackages = make([]models.Package, 0, len(packageMap))
|
||||
for _, pkg := range packageMap {
|
||||
*s.allPackages = append(*s.allPackages, pkg)
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
sort.Slice(*s.allPackages, func(i, j int) bool {
|
||||
return (*s.allPackages)[i].Name < (*s.allPackages)[j].Name
|
||||
})
|
||||
|
||||
return s.allPackages
|
||||
}
|
||||
|
||||
// SetupData initializes the BrewService by loading installed packages, remote formulae, casks, and analytics data.
|
||||
func (s *BrewService) SetupData(forceDownload bool) (err error) {
|
||||
// Load formulae
|
||||
if err = s.loadInstalled(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load installed formulae: %w", err)
|
||||
}
|
||||
|
||||
if err = s.loadRemote(forceDownload); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load remote formulae: %w", err)
|
||||
}
|
||||
|
||||
if err = s.loadAnalytics(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load formulae analytics: %w", err)
|
||||
}
|
||||
|
||||
// Load casks
|
||||
if err = s.loadInstalledCasks(); err != nil {
|
||||
return fmt.Errorf("failed to load installed casks: %w", err)
|
||||
}
|
||||
|
||||
if err = s.loadRemoteCasks(forceDownload); err != nil {
|
||||
return fmt.Errorf("failed to load remote casks: %w", err)
|
||||
}
|
||||
|
||||
if err = s.loadCaskAnalytics(); err != nil {
|
||||
return fmt.Errorf("failed to load cask analytics: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -148,17 +259,60 @@ func (s *BrewService) loadInstalled() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// loadRemote retrieves the list of remote Homebrew formulae from the API and caches them locally.
|
||||
func (s *BrewService) loadRemote(forceDownload bool) (err error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
// loadInstalledCasks retrieves the list of installed Homebrew casks.
|
||||
func (s *BrewService) loadInstalledCasks() (err error) {
|
||||
// Get list of installed cask names
|
||||
listCmd := exec.Command("brew", "list", "--cask")
|
||||
listOutput, err := listCmd.Output()
|
||||
if err != nil {
|
||||
// If no casks are installed, brew returns error - ignore it
|
||||
*s.installedCasks = make([]models.Cask, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse cask names (one per line)
|
||||
caskNames := strings.Split(strings.TrimSpace(string(listOutput)), "\n")
|
||||
if len(caskNames) == 0 || (len(caskNames) == 1 && caskNames[0] == "") {
|
||||
*s.installedCasks = make([]models.Cask, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get info for each installed cask using --json=v2 (v2 required for casks)
|
||||
args := append([]string{"info", "--json=v2", "--cask"}, caskNames...)
|
||||
infoCmd := exec.Command("brew", args...)
|
||||
infoOutput, err := infoCmd.Output()
|
||||
if err != nil {
|
||||
*s.installedCasks = make([]models.Cask, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse JSON response (v2 returns object with "formulae" and "casks" keys)
|
||||
// We only need the "casks" array since we specified --cask flag
|
||||
var response struct {
|
||||
Casks []models.Cask `json:"casks"`
|
||||
}
|
||||
err = json.Unmarshal(infoOutput, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bbrewDir := filepath.Join(homeDir, ".bbrew") // TODO: Move to config
|
||||
formulaFile := filepath.Join(bbrewDir, "formula.json")
|
||||
if _, err := os.Stat(bbrewDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(bbrewDir, 0755); err != nil {
|
||||
*s.installedCasks = response.Casks
|
||||
|
||||
// Mark all installed casks as locally installed
|
||||
for i := range *s.installedCasks {
|
||||
(*s.installedCasks)[i].LocallyInstalled = true
|
||||
(*s.installedCasks)[i].IsCask = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRemote retrieves the list of remote Homebrew formulae from the API and caches them locally.
|
||||
func (s *BrewService) loadRemote(forceDownload bool) (err error) {
|
||||
cacheDir := getCacheDir()
|
||||
formulaFile := filepath.Join(cacheDir, "formula.json")
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(cacheDir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +320,7 @@ func (s *BrewService) loadRemote(forceDownload bool) (err error) {
|
|||
// Check if we should use the cached file
|
||||
if !forceDownload {
|
||||
if _, err := os.Stat(formulaFile); err == nil {
|
||||
// #nosec G304 -- formulaFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
|
||||
data, err := os.ReadFile(formulaFile)
|
||||
if err == nil {
|
||||
*s.remote = make([]models.Formula, 0)
|
||||
|
|
@ -198,6 +353,52 @@ func (s *BrewService) loadRemote(forceDownload bool) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// loadRemoteCasks retrieves the list of remote Homebrew casks from the API and caches them locally.
|
||||
func (s *BrewService) loadRemoteCasks(forceDownload bool) (err error) {
|
||||
cacheDir := getCacheDir()
|
||||
caskFile := filepath.Join(cacheDir, "cask.json")
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(cacheDir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should use the cached file
|
||||
if !forceDownload {
|
||||
if _, err := os.Stat(caskFile); err == nil {
|
||||
// #nosec G304 -- caskFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
|
||||
data, err := os.ReadFile(caskFile)
|
||||
if err == nil {
|
||||
*s.remoteCasks = make([]models.Cask, 0)
|
||||
if err := json.Unmarshal(data, &s.remoteCasks); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(CaskAPIURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s.remoteCasks = make([]models.Cask, 0)
|
||||
err = json.Unmarshal(body, s.remoteCasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache the remote cask data
|
||||
_ = os.WriteFile(caskFile, body, 0600)
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAnalytics retrieves the analytics data for Homebrew formulae from the API.
|
||||
func (s *BrewService) loadAnalytics() (err error) {
|
||||
resp, err := http.Get(AnalyticsAPIURL)
|
||||
|
|
@ -221,6 +422,33 @@ func (s *BrewService) loadAnalytics() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// loadCaskAnalytics retrieves the analytics data for Homebrew casks from the API.
|
||||
func (s *BrewService) loadCaskAnalytics() (err error) {
|
||||
resp, err := http.Get(CaskAnalyticsAPIURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
analytics := models.Analytics{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&analytics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
analyticsByCask := map[string]models.AnalyticsItem{}
|
||||
for _, c := range analytics.Items {
|
||||
// Cask analytics use the "cask" field instead of "formula"
|
||||
caskName := c.Cask
|
||||
if caskName != "" {
|
||||
analyticsByCask[caskName] = c
|
||||
}
|
||||
}
|
||||
|
||||
s.caskAnalytics = analyticsByCask
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBrewVersion retrieves the version of Homebrew installed on the system, caching it for future calls.
|
||||
func (s *BrewService) GetBrewVersion() (version string, err error) {
|
||||
if s.brewVersion != "" {
|
||||
|
|
@ -251,18 +479,33 @@ func (s *BrewService) UpdateAllPackages(app *tview.Application, outputView *tvie
|
|||
return s.executeCommand(app, cmd, outputView)
|
||||
}
|
||||
|
||||
func (s *BrewService) UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
|
||||
cmd := exec.Command("brew", "upgrade", info.Name) // #nosec G204
|
||||
func (s *BrewService) UpdatePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
|
||||
var cmd *exec.Cmd
|
||||
if info.Type == models.PackageTypeCask {
|
||||
cmd = exec.Command("brew", "upgrade", "--cask", info.Name) // #nosec G204
|
||||
} else {
|
||||
cmd = exec.Command("brew", "upgrade", info.Name) // #nosec G204
|
||||
}
|
||||
return s.executeCommand(app, cmd, outputView)
|
||||
}
|
||||
|
||||
func (s *BrewService) RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
|
||||
cmd := exec.Command("brew", "remove", info.Name) // #nosec G204
|
||||
func (s *BrewService) RemovePackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
|
||||
var cmd *exec.Cmd
|
||||
if info.Type == models.PackageTypeCask {
|
||||
cmd = exec.Command("brew", "uninstall", "--cask", info.Name) // #nosec G204
|
||||
} else {
|
||||
cmd = exec.Command("brew", "uninstall", info.Name) // #nosec G204
|
||||
}
|
||||
return s.executeCommand(app, cmd, outputView)
|
||||
}
|
||||
|
||||
func (s *BrewService) InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error {
|
||||
cmd := exec.Command("brew", "install", info.Name) // #nosec G204
|
||||
func (s *BrewService) InstallPackage(info models.Package, app *tview.Application, outputView *tview.TextView) error {
|
||||
var cmd *exec.Cmd
|
||||
if info.Type == models.PackageTypeCask {
|
||||
cmd = exec.Command("brew", "install", "--cask", info.Name) // #nosec G204
|
||||
} else {
|
||||
cmd = exec.Command("brew", "install", info.Name) // #nosec G204
|
||||
}
|
||||
return s.executeCommand(app, cmd, outputView)
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +533,7 @@ func (s *BrewService) executeCommand(
|
|||
defer wg.Done()
|
||||
defer stdoutWriter.Close()
|
||||
defer stderrWriter.Close()
|
||||
cmd.Wait()
|
||||
_ = cmd.Wait() // #nosec G104 -- Error is handled by pipe readers below
|
||||
}()
|
||||
|
||||
// Stdout handler
|
||||
|
|
@ -304,7 +547,7 @@ func (s *BrewService) executeCommand(
|
|||
output := make([]byte, n)
|
||||
copy(output, buf[:n])
|
||||
app.QueueUpdateDraw(func() {
|
||||
outputView.Write(output)
|
||||
_, _ = outputView.Write(output) // #nosec G104
|
||||
outputView.ScrollToEnd()
|
||||
})
|
||||
}
|
||||
|
|
@ -330,7 +573,7 @@ func (s *BrewService) executeCommand(
|
|||
output := make([]byte, n)
|
||||
copy(output, buf[:n])
|
||||
app.QueueUpdateDraw(func() {
|
||||
outputView.Write(output)
|
||||
_, _ = outputView.Write(output) // #nosec G104
|
||||
outputView.ScrollToEnd()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package services
|
|||
import (
|
||||
"bbrew/internal/ui"
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
|
|
@ -11,6 +12,8 @@ type FilterType int
|
|||
const (
|
||||
FilterInstalled FilterType = iota
|
||||
FilterOutdated
|
||||
FilterLeaves
|
||||
FilterCasks
|
||||
)
|
||||
|
||||
// IOAction represents an input/output action that can be triggered by a key event.
|
||||
|
|
@ -43,6 +46,8 @@ type IOService struct {
|
|||
ActionSearch *IOAction
|
||||
ActionFilterInstalled *IOAction
|
||||
ActionFilterOutdated *IOAction
|
||||
ActionFilterLeaves *IOAction
|
||||
ActionFilterCasks *IOAction
|
||||
ActionInstall *IOAction
|
||||
ActionUpdate *IOAction
|
||||
ActionRemove *IOAction
|
||||
|
|
@ -62,6 +67,8 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionSearch = &IOAction{Key: tcell.KeyRune, Rune: '/', KeySlug: "/", Name: "Search"}
|
||||
s.ActionFilterInstalled = &IOAction{Key: tcell.KeyRune, Rune: 'f', KeySlug: "f", Name: "Filter Installed"}
|
||||
s.ActionFilterOutdated = &IOAction{Key: tcell.KeyRune, Rune: 'o', KeySlug: "o", Name: "Filter Outdated"}
|
||||
s.ActionFilterLeaves = &IOAction{Key: tcell.KeyRune, Rune: 'l', KeySlug: "l", Name: "Filter Leaves"}
|
||||
s.ActionFilterCasks = &IOAction{Key: tcell.KeyRune, Rune: 'c', KeySlug: "c", Name: "Filter Casks"}
|
||||
s.ActionInstall = &IOAction{Key: tcell.KeyRune, Rune: 'i', KeySlug: "i", Name: "Install"}
|
||||
s.ActionUpdate = &IOAction{Key: tcell.KeyRune, Rune: 'u', KeySlug: "u", Name: "Update"}
|
||||
s.ActionRemove = &IOAction{Key: tcell.KeyRune, Rune: 'r', KeySlug: "r", Name: "Remove"}
|
||||
|
|
@ -73,6 +80,8 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionSearch.SetAction(s.handleSearchFieldEvent)
|
||||
s.ActionFilterInstalled.SetAction(s.handleFilterPackagesEvent)
|
||||
s.ActionFilterOutdated.SetAction(s.handleFilterOutdatedPackagesEvent)
|
||||
s.ActionFilterLeaves.SetAction(s.handleFilterLeavesEvent)
|
||||
s.ActionFilterCasks.SetAction(s.handleFilterCasksEvent)
|
||||
s.ActionInstall.SetAction(s.handleInstallPackageEvent)
|
||||
s.ActionUpdate.SetAction(s.handleUpdatePackageEvent)
|
||||
s.ActionRemove.SetAction(s.handleRemovePackageEvent)
|
||||
|
|
@ -85,6 +94,8 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionSearch,
|
||||
s.ActionFilterInstalled,
|
||||
s.ActionFilterOutdated,
|
||||
s.ActionFilterLeaves,
|
||||
s.ActionFilterCasks,
|
||||
s.ActionInstall,
|
||||
s.ActionUpdate,
|
||||
s.ActionRemove,
|
||||
|
|
@ -149,19 +160,41 @@ func (s *IOService) handleFilterEvent(filterType FilterType) {
|
|||
|
||||
switch filterType {
|
||||
case FilterInstalled:
|
||||
if s.appService.showOnlyOutdated {
|
||||
if s.appService.showOnlyOutdated || s.appService.showOnlyLeaves || s.appService.showOnlyCasks {
|
||||
s.appService.showOnlyOutdated = false
|
||||
s.appService.showOnlyLeaves = false
|
||||
s.appService.showOnlyCasks = false
|
||||
s.appService.showOnlyInstalled = true
|
||||
} else {
|
||||
s.appService.showOnlyInstalled = !s.appService.showOnlyInstalled
|
||||
}
|
||||
case FilterOutdated:
|
||||
if s.appService.showOnlyInstalled {
|
||||
if s.appService.showOnlyInstalled || s.appService.showOnlyLeaves || s.appService.showOnlyCasks {
|
||||
s.appService.showOnlyInstalled = false
|
||||
s.appService.showOnlyLeaves = false
|
||||
s.appService.showOnlyCasks = false
|
||||
s.appService.showOnlyOutdated = true
|
||||
} else {
|
||||
s.appService.showOnlyOutdated = !s.appService.showOnlyOutdated
|
||||
}
|
||||
case FilterLeaves:
|
||||
if s.appService.showOnlyInstalled || s.appService.showOnlyOutdated || s.appService.showOnlyCasks {
|
||||
s.appService.showOnlyInstalled = false
|
||||
s.appService.showOnlyOutdated = false
|
||||
s.appService.showOnlyCasks = false
|
||||
s.appService.showOnlyLeaves = true
|
||||
} else {
|
||||
s.appService.showOnlyLeaves = !s.appService.showOnlyLeaves
|
||||
}
|
||||
case FilterCasks:
|
||||
if s.appService.showOnlyInstalled || s.appService.showOnlyOutdated || s.appService.showOnlyLeaves {
|
||||
s.appService.showOnlyInstalled = false
|
||||
s.appService.showOnlyOutdated = false
|
||||
s.appService.showOnlyLeaves = false
|
||||
s.appService.showOnlyCasks = true
|
||||
} else {
|
||||
s.appService.showOnlyCasks = !s.appService.showOnlyCasks
|
||||
}
|
||||
}
|
||||
|
||||
// Update the search field label and legend based on the current filter state
|
||||
|
|
@ -171,6 +204,12 @@ func (s *IOService) handleFilterEvent(filterType FilterType) {
|
|||
} else if s.appService.showOnlyInstalled {
|
||||
s.layout.GetSearch().Field().SetLabel("Search (Installed): ")
|
||||
s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterInstalled.KeySlug)
|
||||
} else if s.appService.showOnlyLeaves {
|
||||
s.layout.GetSearch().Field().SetLabel("Search (Leaves): ")
|
||||
s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterLeaves.KeySlug)
|
||||
} else if s.appService.showOnlyCasks {
|
||||
s.layout.GetSearch().Field().SetLabel("Search (Casks): ")
|
||||
s.layout.GetLegend().SetLegend(s.legendEntries, s.ActionFilterCasks.KeySlug)
|
||||
} else {
|
||||
s.layout.GetSearch().Field().SetLabel("Search (All): ")
|
||||
}
|
||||
|
|
@ -188,6 +227,16 @@ func (s *IOService) handleFilterOutdatedPackagesEvent() {
|
|||
s.handleFilterEvent(FilterOutdated)
|
||||
}
|
||||
|
||||
// handleFilterLeavesEvent toggles the filter for leaf packages (installed on request)
|
||||
func (s *IOService) handleFilterLeavesEvent() {
|
||||
s.handleFilterEvent(FilterLeaves)
|
||||
}
|
||||
|
||||
// handleFilterCasksEvent toggles the filter for cask packages only
|
||||
func (s *IOService) handleFilterCasksEvent() {
|
||||
s.handleFilterEvent(FilterCasks)
|
||||
}
|
||||
|
||||
// showModal displays a modal dialog with the specified text and confirmation/cancellation actions.
|
||||
// This is used for actions like installing, removing, or updating packages, invoking user confirmation.
|
||||
func (s *IOService) showModal(text string, confirmFunc func(), cancelFunc func()) {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ func NewDetails(theme *theme.Theme) *Details {
|
|||
return details
|
||||
}
|
||||
|
||||
func (d *Details) SetContent(info *models.Formula) {
|
||||
if info == nil {
|
||||
func (d *Details) SetContent(pkg *models.Package) {
|
||||
if pkg == nil {
|
||||
d.view.SetText("")
|
||||
return
|
||||
}
|
||||
|
|
@ -41,97 +41,112 @@ func (d *Details) SetContent(info *models.Formula) {
|
|||
// Installation status with colors
|
||||
installedStatus := "[red]Not installed[-]"
|
||||
installedIcon := "✗"
|
||||
if len(info.Installed) > 0 {
|
||||
if pkg.LocallyInstalled {
|
||||
installedStatus = "[green]Installed[-]"
|
||||
installedIcon = "✓"
|
||||
|
||||
if info.Outdated {
|
||||
if pkg.Outdated {
|
||||
installedStatus = "[orange]Update available[-]"
|
||||
installedIcon = "⟳"
|
||||
}
|
||||
}
|
||||
|
||||
// Basic information with icons
|
||||
// Type tag with escaped brackets
|
||||
typeTag := tview.Escape("[F]") // Formula
|
||||
typeLabel := "Formula"
|
||||
if pkg.Type == models.PackageTypeCask {
|
||||
typeTag = tview.Escape("[C]") // Cask
|
||||
typeLabel = "Cask"
|
||||
}
|
||||
|
||||
// Basic information with status
|
||||
basicInfo := fmt.Sprintf(
|
||||
"[yellow::b]%s %s[-]\n\n"+
|
||||
"[blue]• Type:[-] %s %s\n"+
|
||||
"[blue]• Name:[-] %s\n"+
|
||||
"[blue]• Display Name:[-] %s\n"+
|
||||
"[blue]• Version:[-] %s\n"+
|
||||
"[blue]• Status:[-] %s %s\n"+
|
||||
"[blue]• Tap:[-] %s\n"+
|
||||
"[blue]• License:[-] %s\n\n"+
|
||||
"[blue]• Status:[-] %s\n\n"+
|
||||
"[yellow::b]Description[-]\n%s\n\n"+
|
||||
"[blue]• Homepage:[-] %s",
|
||||
info.Name, installedIcon,
|
||||
info.FullName,
|
||||
info.Versions.Stable,
|
||||
installedStatus, d.getPackageVersionInfo(info),
|
||||
info.Tap,
|
||||
info.License,
|
||||
info.Description,
|
||||
info.Homepage,
|
||||
pkg.Name, installedIcon,
|
||||
typeTag, typeLabel,
|
||||
pkg.Name,
|
||||
pkg.DisplayName,
|
||||
pkg.Version,
|
||||
installedStatus,
|
||||
pkg.Description,
|
||||
pkg.Homepage,
|
||||
)
|
||||
|
||||
// Installation details
|
||||
installDetails := d.getPackageInstallationDetails(info)
|
||||
installDetails := d.getPackageInstallationDetails(pkg)
|
||||
|
||||
// Dependencies with improved formatting
|
||||
dependenciesInfo := d.getDependenciesInfo(info)
|
||||
|
||||
analyticsInfo := d.getAnalyticsInfo(info)
|
||||
|
||||
d.view.SetText(strings.Join([]string{basicInfo, installDetails, dependenciesInfo, analyticsInfo}, "\n\n"))
|
||||
}
|
||||
|
||||
func (d *Details) getPackageVersionInfo(info *models.Formula) string {
|
||||
if len(info.Installed) == 0 {
|
||||
return ""
|
||||
// Dependencies (only for formulae)
|
||||
dependenciesInfo := ""
|
||||
if pkg.Type == models.PackageTypeFormula && pkg.Formula != nil {
|
||||
dependenciesInfo = d.getDependenciesInfo(pkg.Formula)
|
||||
}
|
||||
|
||||
installedVersion := info.Installed[0].Version
|
||||
stableVersion := info.Versions.Stable
|
||||
analyticsInfo := d.getAnalyticsInfo(pkg)
|
||||
|
||||
// Revision version
|
||||
if strings.HasPrefix(installedVersion, stableVersion+"_") {
|
||||
return fmt.Sprintf("([green]%s[-])", installedVersion)
|
||||
} else if installedVersion == stableVersion {
|
||||
return fmt.Sprintf("([green]%s[-])", installedVersion)
|
||||
} else if installedVersion < stableVersion || info.Outdated {
|
||||
return fmt.Sprintf("([orange]%s[-] → [green]%s[-])",
|
||||
installedVersion, stableVersion)
|
||||
parts := []string{basicInfo, installDetails}
|
||||
if dependenciesInfo != "" {
|
||||
parts = append(parts, dependenciesInfo)
|
||||
}
|
||||
parts = append(parts, analyticsInfo)
|
||||
|
||||
// Other cases
|
||||
return fmt.Sprintf("([green]%s[-])", installedVersion)
|
||||
d.view.SetText(strings.Join(parts, "\n\n"))
|
||||
}
|
||||
|
||||
func (d *Details) getPackageInstallationDetails(info *models.Formula) string {
|
||||
if len(info.Installed) == 0 {
|
||||
func (d *Details) getPackageInstallationDetails(pkg *models.Package) string {
|
||||
if !pkg.LocallyInstalled {
|
||||
return "[yellow::b]Installation[-]\nNot installed"
|
||||
}
|
||||
|
||||
packagePrefix := info.LocalPath
|
||||
// For formulae, show detailed installation info
|
||||
if pkg.Type == models.PackageTypeFormula && pkg.Formula != nil && len(pkg.Formula.Installed) > 0 {
|
||||
packagePrefix := pkg.Formula.LocalPath
|
||||
|
||||
installedOnRequest := "No"
|
||||
if info.Installed[0].InstalledOnRequest {
|
||||
installedOnRequest = "Yes"
|
||||
installedOnRequest := "No"
|
||||
if pkg.Formula.Installed[0].InstalledOnRequest {
|
||||
installedOnRequest = "Yes"
|
||||
}
|
||||
|
||||
installedAsDependency := "No"
|
||||
if pkg.Formula.Installed[0].InstalledAsDependency {
|
||||
installedAsDependency = "Yes"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"[yellow::b]Installation Details[-]\n"+
|
||||
"[blue]• Path:[-] %s\n"+
|
||||
"[blue]• Installed on request:[-] %s\n"+
|
||||
"[blue]• Installed as dependency:[-] %s\n"+
|
||||
"[blue]• Installed version:[-] %s",
|
||||
packagePrefix,
|
||||
installedOnRequest,
|
||||
installedAsDependency,
|
||||
pkg.Formula.Installed[0].Version,
|
||||
)
|
||||
}
|
||||
|
||||
installedAsDependency := "No"
|
||||
if info.Installed[0].InstalledAsDependency {
|
||||
installedAsDependency = "Yes"
|
||||
// For casks, show simpler installation info
|
||||
if pkg.Type == models.PackageTypeCask && pkg.Cask != nil {
|
||||
installedVersion := "Unknown"
|
||||
if pkg.Cask.Installed != nil {
|
||||
installedVersion = *pkg.Cask.Installed
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"[yellow::b]Installation Details[-]\n"+
|
||||
"[blue]• Type:[-] macOS Application\n"+
|
||||
"[blue]• Installed version:[-] %s",
|
||||
installedVersion,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"[yellow::b]Installation Details[-]\n"+
|
||||
"[blue]• Path:[-] %s\n"+
|
||||
"[blue]• Installed on request:[-] %s\n"+
|
||||
"[blue]• Installed as dependency:[-] %s\n"+
|
||||
"[blue]• Installed version:[-] %s",
|
||||
packagePrefix,
|
||||
installedOnRequest,
|
||||
installedAsDependency,
|
||||
info.Installed[0].Version,
|
||||
)
|
||||
return "[yellow::b]Installation[-]\nInstalled"
|
||||
}
|
||||
|
||||
func (d *Details) getDependenciesInfo(info *models.Formula) string {
|
||||
|
|
@ -157,13 +172,13 @@ func (d *Details) getDependenciesInfo(info *models.Formula) string {
|
|||
return title + deps
|
||||
}
|
||||
|
||||
func (d *Details) getAnalyticsInfo(info *models.Formula) string {
|
||||
func (d *Details) getAnalyticsInfo(pkg *models.Package) string {
|
||||
title := "[yellow::b]Analytics[-]\n"
|
||||
|
||||
p := message.NewPrinter(language.English)
|
||||
|
||||
title += fmt.Sprintf("[blue]• 90d Global Rank:[-] %s\n", p.Sprintf("%d", info.Analytics90dRank))
|
||||
title += fmt.Sprintf("[blue]• 90d Downloads:[-] %s\n", p.Sprintf("%d", info.Analytics90dDownloads))
|
||||
title += fmt.Sprintf("[blue]• 90d Global Rank:[-] %s\n", p.Sprintf("%d", pkg.Analytics90dRank))
|
||||
title += fmt.Sprintf("[blue]• 90d Downloads:[-] %s\n", p.Sprintf("%d", pkg.Analytics90dDownloads))
|
||||
|
||||
return title
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue