From 5298428f7467714d885f2a2420f4c0a90f0a9b0c Mon Sep 17 00:00:00 2001 From: Haryo Bagas Assyafah <64161364+bearaujus@users.noreply.github.com> Date: Mon, 9 Dec 2024 05:12:46 +0700 Subject: [PATCH] feat: add interactive more and sign binaries patch --- .../workflows/build-and-upload-binaries.yml | 106 -------- .github/workflows/distribute-binaries.yml | 98 +++++++ .gitignore | 1 + go.mod | 10 +- go.sum | 58 ++++- internal/cli/cli.go | 36 --- internal/cli/library/library.go | 40 --- .../set_auto_update/set_auto_update.go | 35 --- .../set_background_downloads.go | 46 ---- internal/config/config.go | 24 +- internal/model/error.go | 5 +- internal/model/library.go | 35 +++ internal/pkg/prettier.go | 21 +- internal/pkg/process_bar.go | 47 ---- .../set_auto_update/set_auto_update.go | 123 --------- .../set_background_downloads.go | 101 -------- internal/usecase/type.go | 5 - internal/usecase/usecase.go | 112 ++++++++ internal/view/cli/cli.go | 144 ++++++++++ internal/view/interactive/interactive.go | 245 ++++++++++++++++++ internal/view/type.go | 7 + main.go | 45 ++-- pkg/steam_path/steam_path.go | 5 + 23 files changed, 749 insertions(+), 600 deletions(-) delete mode 100644 .github/workflows/build-and-upload-binaries.yml create mode 100644 .github/workflows/distribute-binaries.yml delete mode 100644 internal/cli/cli.go delete mode 100644 internal/cli/library/library.go delete mode 100644 internal/cli/library/set_auto_update/set_auto_update.go delete mode 100644 internal/cli/library/set_background_downloads/set_background_downloads.go create mode 100644 internal/model/library.go delete mode 100644 internal/pkg/process_bar.go delete mode 100644 internal/usecase/library/set_auto_update/set_auto_update.go delete mode 100644 internal/usecase/library/set_background_downloads/set_background_downloads.go delete mode 100644 internal/usecase/type.go create mode 100644 internal/usecase/usecase.go create mode 100644 internal/view/cli/cli.go create mode 100644 internal/view/interactive/interactive.go create mode 100644 internal/view/type.go diff --git a/.github/workflows/build-and-upload-binaries.yml b/.github/workflows/build-and-upload-binaries.yml deleted file mode 100644 index a26d554..0000000 --- a/.github/workflows/build-and-upload-binaries.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Build and Upload Binaries on Tag - -on: - push: - tags: - - 'v*' # Trigger the workflow for tags like v1.0, v2.1, etc. - -jobs: - build: - runs-on: ubuntu-latest - steps: - # Checkout the code - - name: Checkout code - uses: actions/checkout@v2 - - # Set up Go environment - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: '1.23' - - # Install necessary build tools for cgo - - name: Install build tools for cgo - run: | - sudo apt-get update - sudo apt-get install -y gcc g++ libc6-dev gcc-multilib g++-x86-64-linux-gnu libc6-dev-amd64-cross - - # Install necessary build tools for cgo - - name: Install build tools for cgo - run: | - sudo apt-get update - sudo apt-get install -y gcc g++ libc6-dev gcc-multilib g++-x86-64-linux-gnu libc6-dev-amd64-cross - - # Get the version from the tag - - name: Get version from tag - id: get_version - run: | - echo "VERSION=$(echo ${GITHUB_REF} | sed 's/refs\/tags\///')" >> $GITHUB_ENV - - # Get the repository name from the GitHub context (e.g., steam-utils from github.com/bearaujus/steam-utils) - - name: Get repo name - id: get_repo_name - run: | - REPO_NAME=$(echo "${GITHUB_REPOSITORY}" | cut -d'/' -f2) - echo "REPO_NAME=${REPO_NAME}" >> $GITHUB_ENV - echo "Repository name: ${REPO_NAME}" - - # Build binaries only if tag is new - - name: Build binaries - if: env.skip != 'true' - run: | - # Define a specific list of GOOS and GOARCH combinations - GOOS_ARCH_LIST=( - "windows/386" - "windows/amd64" - "windows/arm" - "windows/arm64" - "linux/386" - "linux/amd64" - "linux/arm" - "linux/arm64" - "darwin/amd64" - "darwin/arm64" - ) - - VERSION=${{ env.VERSION }} - REPO_NAME=${{ env.REPO_NAME }} - - # Create a directory for the build outputs - mkdir -p binaries - - # Loop through the GOOS and GOARCH combinations and build binaries - for GOOS_ARCH in "${GOOS_ARCH_LIST[@]}"; do - GOOS=$(echo $GOOS_ARCH | cut -d'/' -f1) - GOARCH=$(echo $GOOS_ARCH | cut -d'/' -f2) - - # Disable cgo for ARM builds - if [[ "$GOARCH" == "arm" || "$GOARCH" == "arm64" || "$GOOS" == "windows" ]]; then - export CGO_ENABLED=0 - else - export CGO_ENABLED=1 - fi - - # Set environment variables - export GOOS - export GOARCH - - # Construct the filename and add .exe for Windows - FILENAME="${REPO_NAME}-${VERSION}-${GOOS}-${GOARCH}" - if [[ "$GOOS" == "windows" ]]; then - FILENAME="${FILENAME}.exe" - fi - - # Build the binary with the required format and ldflags for version, name, arch, and goos - go build -ldflags "-X main.name=${REPO_NAME} -X main.version=${VERSION} -X main.arch=${GOARCH} -X main.goos=${GOOS} -X main.file=${FILENAME}" -o "binaries/${FILENAME}" - - echo "Built binary: ${FILENAME}" - done - - # Upload binaries to GitHub Releases - - name: Upload binaries to release - uses: softprops/action-gh-release@v1 - with: - files: binaries/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/distribute-binaries.yml b/.github/workflows/distribute-binaries.yml new file mode 100644 index 0000000..21b9703 --- /dev/null +++ b/.github/workflows/distribute-binaries.yml @@ -0,0 +1,98 @@ +name: Distribute Binaries + +on: + push: + tags: + - 'v*' # Trigger the workflow for tags like v1.0, v2.1, etc. + +jobs: + build: + runs-on: ubuntu-latest + steps: + # Checkout the code + - name: Checkout code + uses: actions/checkout@v2 + + # Set up Go environment + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.23' + + # Install necessary build tools for cgo and osslsigncode + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y gcc g++ libc6-dev gcc-multilib g++-x86-64-linux-gnu osslsigncode openssl + + # Get the version from the tag + - name: Get version from tag + id: get_version + run: | + echo "VERSION=$(echo ${GITHUB_REF} | sed 's/refs\/tags\///')" >> $GITHUB_ENV + + # Get the repository name from the GitHub context + - name: Get repo name + id: get_repo_name + run: | + REPO_NAME=$(echo "${GITHUB_REPOSITORY}" | cut -d'/' -f2) + echo "REPO_NAME=${REPO_NAME}" >> $GITHUB_ENV + echo "Repository name: ${REPO_NAME}" + + # Build binaries + - name: Build binaries + if: env.skip != 'true' + run: | + GOOS_ARCH_LIST=( "windows/386" "windows/amd64" "windows/arm" "windows/arm64" ) + VERSION=${{ env.VERSION }} + REPO_NAME=${{ env.REPO_NAME }} + mkdir -p binaries + for GOOS_ARCH in "${GOOS_ARCH_LIST[@]}"; do + GOOS=$(echo $GOOS_ARCH | cut -d'/' -f1) + GOARCH=$(echo $GOOS_ARCH | cut -d'/' -f2) + FILENAME="${REPO_NAME}-${VERSION}-${GOOS}-${GOARCH}.exe" + export GOOS GOARCH CGO_ENABLED=0 + go build -ldflags "-X main.name=${REPO_NAME} -X main.version=${VERSION}" -o "binaries/${FILENAME}" + echo "Built binary: ${FILENAME}" + done + + # Prepare the signing certificate (Generate .p12 file) + - name: Prepare signing certificate + run: | + # Decode the PRIVATE_KEY secret and save it to a file + echo "${{ secrets.PRIVATE_KEY }}" > private.key + + # Decode the REQUEST_CSR secret and save it to a file + echo "${{ secrets.REQUEST_CSR }}" > request.csr + + # Decode the SIGN_PASSWORD secret and save it to a file + echo "${{ secrets.SIGN_PASSWORD }}" > sign_password.txt + + # Create the .p12 certificate using the private key and certificate (self-signed or CA-signed) + openssl pkcs12 -export -out certificate.p12 -inkey private.key -in request.csr -passout file:sign_password.txt + + # Clean up sensitive files after use + rm private.key request.csr sign_password.txt + + # Sign Windows binaries + - name: Sign Windows binaries + run: | + mkdir -p signed + for FILE in binaries/*.exe; do + SIGNED_FILE="signed/$(basename $FILE)" + osslsigncode sign \ + -pkcs12 certificate.p12 \ + -pass "${{ secrets.SIGN_PASSWORD }}" \ + -t http://timestamp.digicert.com \ + -in "$FILE" \ + -out "$SIGNED_FILE" + echo "Signed binary: $SIGNED_FILE" + done + + # Upload signed binaries to GitHub Releases + - name: Upload signed binaries to release + uses: softprops/action-gh-release@v1 + with: + files: signed/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 04029ae..07e5501 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ **/.DS_Store dev bin +.idea # Test binary, built with `go test -c` *.test diff --git a/go.mod b/go.mod index 630496c..e6b78a3 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,21 @@ go 1.23.3 require ( github.com/bearaujus/berror v0.0.1 github.com/fatih/color v1.18.0 - github.com/schollz/progressbar/v3 v3.17.1 + github.com/gdamore/tcell/v2 v2.7.1 + github.com/inconshreveable/mousetrap v1.1.0 + github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 github.com/spf13/cobra v1.8.1 ) require ( - github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 0a0f80e..b8c8754 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,74 @@ github.com/bearaujus/berror v0.0.1 h1:O7/b0SvDQMNHjUmERiGKzvGmpudvhJFd5nuYtMCtz1g= github.com/bearaujus/berror v0.0.1/go.mod h1:gqwSzaF6jOE3pCU8x2JXt8+j5ns72G6OWunartoQJcw= -github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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= +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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 h1:YIJ+B1hePP6AgynC5TcqpO0H9k3SSoZa2BGyL6vDUzM= +github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= -github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/cli.go b/internal/cli/cli.go deleted file mode 100644 index 05b57be..0000000 --- a/internal/cli/cli.go +++ /dev/null @@ -1,36 +0,0 @@ -package cli - -import ( - "context" - "github.com/bearaujus/steam-utils/internal/cli/library" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/spf13/cobra" -) - -func NewRoot(ctx context.Context, cfg *config.Config) *cobra.Command { - var rootCmd = &cobra.Command{ - Use: cfg.LdFlags.Name, - Short: "Steam utilities", - Args: cobra.NoArgs, - CompletionOptions: cobra.CompletionOptions{ - DisableDefaultCmd: true, - DisableNoDescFlag: true, - DisableDescriptions: true, - HiddenDefaultCmd: true, - }, - } - rootCmd.AddCommand(newLibraryCmd(ctx, cfg)) - return rootCmd -} - -func newLibraryCmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "library", - Short: "Steam library utilities", - Args: cobra.NoArgs, - } - for _, childCmd := range library.New(ctx, cfg) { - cmd.AddCommand(childCmd) - } - return cmd -} diff --git a/internal/cli/library/library.go b/internal/cli/library/library.go deleted file mode 100644 index bedabdb..0000000 --- a/internal/cli/library/library.go +++ /dev/null @@ -1,40 +0,0 @@ -package library - -import ( - "context" - "github.com/bearaujus/steam-utils/internal/cli/library/set_auto_update" - "github.com/bearaujus/steam-utils/internal/cli/library/set_background_downloads" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/spf13/cobra" -) - -func New(ctx context.Context, cfg *config.Config) []*cobra.Command { - return []*cobra.Command{ - newSetAutoUpdateCmd(ctx, cfg), - newSetBackgroundDownloadsCmd(ctx, cfg), - } -} - -func newSetAutoUpdateCmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "set-auto-update", - Short: "Set auto update behavior on all collections on your Steam library", - Args: cobra.NoArgs, - } - for _, childCmd := range set_auto_update.New(ctx, cfg) { - cmd.AddCommand(childCmd) - } - return cmd -} - -func newSetBackgroundDownloadsCmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "set-background-downloads", - Short: "Set background downloads behavior on all collections on your Steam library", - Args: cobra.NoArgs, - } - for _, childCmd := range set_background_downloads.New(ctx, cfg) { - cmd.AddCommand(childCmd) - } - return cmd -} diff --git a/internal/cli/library/set_auto_update/set_auto_update.go b/internal/cli/library/set_auto_update/set_auto_update.go deleted file mode 100644 index 82f76d6..0000000 --- a/internal/cli/library/set_auto_update/set_auto_update.go +++ /dev/null @@ -1,35 +0,0 @@ -package set_auto_update - -import ( - "context" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/bearaujus/steam-utils/internal/usecase/library/set_auto_update" - "github.com/spf13/cobra" -) - -func New(ctx context.Context, cfg *config.Config) []*cobra.Command { - return []*cobra.Command{ - new0Cmd(ctx, cfg), - new1Cmd(ctx, cfg), - } -} - -func new0Cmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "0", - Short: "Always keep all games updated", - Args: cobra.NoArgs, - RunE: set_auto_update.NewCmdRunner(ctx, cfg), - } - return cmd -} - -func new1Cmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "1", - Short: "Only update a game when you launch it", - Args: cobra.NoArgs, - RunE: set_auto_update.NewCmdRunner(ctx, cfg), - } - return cmd -} diff --git a/internal/cli/library/set_background_downloads/set_background_downloads.go b/internal/cli/library/set_background_downloads/set_background_downloads.go deleted file mode 100644 index 9799a81..0000000 --- a/internal/cli/library/set_background_downloads/set_background_downloads.go +++ /dev/null @@ -1,46 +0,0 @@ -package set_background_downloads - -import ( - "context" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/bearaujus/steam-utils/internal/usecase/library/set_background_downloads" - "github.com/spf13/cobra" -) - -func New(ctx context.Context, cfg *config.Config) []*cobra.Command { - return []*cobra.Command{ - new0Cmd(ctx, cfg), - new1Cmd(ctx, cfg), - new2Cmd(ctx, cfg), - } -} - -func new0Cmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "0", - Short: "Follow your global steam settings", - Args: cobra.NoArgs, - RunE: set_background_downloads.NewCmdRunner(ctx, cfg), - } - return cmd -} - -func new1Cmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "1", - Short: "Always allow background downloads", - Args: cobra.NoArgs, - RunE: set_background_downloads.NewCmdRunner(ctx, cfg), - } - return cmd -} - -func new2Cmd(ctx context.Context, cfg *config.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "2", - Short: "Never allow background downloads", - Args: cobra.NoArgs, - RunE: set_background_downloads.NewCmdRunner(ctx, cfg), - } - return cmd -} diff --git a/internal/config/config.go b/internal/config/config.go index 2bf8705..b346271 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,18 +2,15 @@ package config import ( "fmt" - "github.com/bearaujus/steam-utils/pkg/steam_path" - "github.com/spf13/cobra" "runtime" -) -const ( - PersistentFlagSteamPath = "steam-path" + "github.com/bearaujus/steam-utils/pkg/steam_path" ) type Config struct { - SteamPath string - LdFlags *LdFlags + SteamPath steam_path.SteamPath + DefaultSteamPath steam_path.SteamPath + LdFlags *LdFlags } type LdFlags struct { @@ -43,17 +40,10 @@ func NewConfig(ldFlags *LdFlags) *Config { if ldFlags.File == "" { ldFlags.File = fmt.Sprintf("%v-%v-%v-%v", ldFlags.Name, ldFlags.Version, ldFlags.Goos, ldFlags.Arch) } - return &Config{ - LdFlags: ldFlags, - } -} - -func LoadConfig(cmd *cobra.Command, config *Config) error { - var defaultSteamPath string + cfg := &Config{LdFlags: ldFlags} sp, err := steam_path.LoadDefaultSteamPath() if err == nil { - defaultSteamPath = sp.Base() + cfg.DefaultSteamPath = sp } - cmd.PersistentFlags().StringVar(&config.SteamPath, PersistentFlagSteamPath, defaultSteamPath, "Path to steam installation directory") - return nil + return cfg } diff --git a/internal/model/error.go b/internal/model/error.go index 244c194..aaa6536 100644 --- a/internal/model/error.go +++ b/internal/model/error.go @@ -5,12 +5,11 @@ import ( ) var ( - ErrSteamPathIsNotSet = berror.NewErrDefinition("default steam installation path is not detected. please include flag '--%v'", berror.OptionErrDefinitionWithDisabledStackTrace()) - ErrSteamPathIsInvalid = berror.NewErrDefinition("%v. please update argument on flag '--%v'", berror.OptionErrDefinitionWithDisabledStackTrace()) + ErrFailToInitializeSteamPath = berror.NewErrDefinition("fail to initialize steam path: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) + ErrEmptyListLibraryMetadata = berror.NewErrDefinition("empty list library metadata: no .acf files detected in %v directory. ensure that you have installed applications in your Steam library and try again", berror.OptionErrDefinitionWithDisabledStackTrace()) ErrReadDirectory = berror.NewErrDefinition("fail to read directory: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) ErrReadFile = berror.NewErrDefinition("fail to read file: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) ErrWriteFile = berror.NewErrDefinition("fail to write file: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) ErrParseSteamACFFile = berror.NewErrDefinition("fail to parse steam acf: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) - ErrGetValueFromSteamACFFile = berror.NewErrDefinition("fail to get value from steam acf: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) ErrUpdateValueFromSteamACFFile = berror.NewErrDefinition("fail to update value to steam acf: %v", berror.OptionErrDefinitionWithDisabledStackTrace()) ) diff --git a/internal/model/library.go b/internal/model/library.go new file mode 100644 index 0000000..ec35aaf --- /dev/null +++ b/internal/model/library.go @@ -0,0 +1,35 @@ +package model + +const ( + LibraryAutoUpdateAlwaysKeepAll = "0" + LibraryAutoUpdateOnlyOnLaunch = "1" +) + +var LibraryAutoUpdate = map[string]string{ + LibraryAutoUpdateAlwaysKeepAll: "Keep all games updated automatically", + LibraryAutoUpdateOnlyOnLaunch: "Only update a game when you launch it", +} + +var LibraryAutoUpdateR = getReversedKeyValueMap(LibraryAutoUpdate) + +const ( + LibraryBackgroundDownloadsFollowGlobal = "0" + LibraryBackgroundDownloadsAlwaysAllow = "1" + LibraryBackgroundDownloadsNeverAllow = "2" +) + +var LibraryBackgroundDownloads = map[string]string{ + LibraryBackgroundDownloadsFollowGlobal: "Use the global Steam settings for background downloads", + LibraryBackgroundDownloadsAlwaysAllow: "Always allow background downloads to run", + LibraryBackgroundDownloadsNeverAllow: "Never allow background downloads to run", +} + +var LibraryBackgroundDownloadsR = getReversedKeyValueMap(LibraryBackgroundDownloads) + +func getReversedKeyValueMap(src map[string]string) map[string]string { + ret := make(map[string]string, len(src)) + for k, v := range src { + ret[v] = k + } + return ret +} diff --git a/internal/pkg/prettier.go b/internal/pkg/prettier.go index c2cbcfd..5376a7f 100644 --- a/internal/pkg/prettier.go +++ b/internal/pkg/prettier.go @@ -2,20 +2,27 @@ package pkg import ( "fmt" + "strings" + "github.com/bearaujus/steam-utils/internal/config" "github.com/fatih/color" - "strings" ) -const ( - maxWidth = 100 -) +const maxWidth = 100 -func PrintTitle(cfg *config.Config) { - PrintSep() +func GetTitle(cfg *config.Config) string { title := color.New(color.FgHiYellow, color.Bold).Sprint(strings.ToUpper(cfg.LdFlags.Name)) version := color.New(color.FgHiBlue).Sprint(cfg.LdFlags.Version) - fmt.Printf("%v (%v) - %v\n", title, version, "Sets of utilities for managing your Steam") + return fmt.Sprintf("%v (%v) - %v", title, version, "Sets of utilities for managing your Steam") +} + +func GetTitleRaw(cfg *config.Config) string { + return fmt.Sprintf("%v (%v) - %v", cfg.LdFlags.Name, cfg.LdFlags.Version, "Sets of utilities for managing your Steam") +} + +func PrintTitle(cfg *config.Config) { + PrintSep() + fmt.Println(GetTitle(cfg)) PrintSep() } diff --git a/internal/pkg/process_bar.go b/internal/pkg/process_bar.go deleted file mode 100644 index bec064a..0000000 --- a/internal/pkg/process_bar.go +++ /dev/null @@ -1,47 +0,0 @@ -package pkg - -import ( - "fmt" - "github.com/fatih/color" - "github.com/schollz/progressbar/v3" - "strings" -) - -type ProgressBar interface { - Add(msg string) - Finish() -} - -type progressBar struct { - pName string - bar *progressbar.ProgressBar -} - -func NewProgressBar(max int, pName string) ProgressBar { - return &progressBar{ - pName: pName, - bar: progressbar.NewOptions(max + 1), - } -} - -func (p *progressBar) Add(msg string) { - p.setDescription(fmt.Sprintf("Processing: %s...", msg)) - _ = p.bar.Add(1) -} - -func (p *progressBar) Finish() { - p.setDescription("All tasks completed successfully!") - _ = p.bar.Add(1) - _ = p.bar.Finish() - fmt.Println(fmt.Sprintf("\nTask: %s", color.New(color.FgHiBlue).Sprint(p.pName))) - PrintSep() -} - -func (p *progressBar) setDescription(msg string) { - m := maxWidth - 48 - if len(msg) > m { - p.bar.Describe(msg[:m]) - return - } - p.bar.Describe(msg + strings.Repeat(" ", m-len(msg))) -} diff --git a/internal/usecase/library/set_auto_update/set_auto_update.go b/internal/usecase/library/set_auto_update/set_auto_update.go deleted file mode 100644 index 6b2f6bc..0000000 --- a/internal/usecase/library/set_auto_update/set_auto_update.go +++ /dev/null @@ -1,123 +0,0 @@ -package set_auto_update - -import ( - "context" - "errors" - "fmt" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/bearaujus/steam-utils/internal/model" - "github.com/bearaujus/steam-utils/internal/pkg" - "github.com/bearaujus/steam-utils/internal/usecase" - "github.com/bearaujus/steam-utils/pkg/steam_acf" - "github.com/bearaujus/steam-utils/pkg/steam_path" - "github.com/spf13/cobra" - "os" - "path/filepath" - "strings" - "time" -) - -func NewCmdRunner(_ context.Context, cfg *config.Config) usecase.CmdRunner { - return func(cmd *cobra.Command, args []string) error { - se, err := steam_path.NewSteamPath(cfg.SteamPath) - if err != nil { - if errors.Is(err, steam_path.ErrEmptyPath) { - return model.ErrSteamPathIsNotSet.New(config.PersistentFlagSteamPath) - } - return model.ErrSteamPathIsInvalid.New(err, config.PersistentFlagSteamPath) - } - - files, err := os.ReadDir(se.SteamApps()) - if err != nil { - return model.ErrReadDirectory.New(err) - } - - var fileTargets []os.DirEntry - for _, file := range files { - if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".acf") { - fileTargets = append(fileTargets, file) - } - } - - if len(fileTargets) == 0 { - fmt.Printf("No .acf files detected in %v directory. Ensure that you have installed applications in your Steam library and try again.\n", se.SteamApps()) - return nil - } - - var ( - aubUpdate, sauUpdate int - aubTargets = []string{"AppState", "AutoUpdateBehavior"} - aubTargetsName = strings.Join(aubTargets, ".") - sauTargets = []string{"AppState", "ScheduledAutoUpdate"} - sauTargetsName = strings.Join(sauTargets, ".") - bar = pkg.NewProgressBar(len(fileTargets), cmd.Short) - ) - for _, file := range fileTargets { - time.Sleep(time.Millisecond * 50) - fileName := filepath.Join(se.SteamApps(), file.Name()) - data, err := os.ReadFile(fileName) - if err != nil { - return model.ErrReadFile.New(err) - } - - sa, err := steam_acf.Parse(data) - if err != nil { - return model.ErrParseSteamACFFile.New(err) - } - - appName, err := sa.Get([]string{"AppState", "name"}) - if err != nil { - return model.ErrGetValueFromSteamACFFile.New(err) - } - bar.Add(appName) - - var aubPrevious, sauPrevious string - aubPrevious, err = sa.Update(aubTargets, cmd.Use) - if err != nil { - return model.ErrUpdateValueFromSteamACFFile.New(err) - } - - if cmd.Use == "1" { - sauPrevious, err = sa.Update(sauTargets, "0") - if err != nil { - return model.ErrUpdateValueFromSteamACFFile.New(err) - } - } - - if aubPrevious != cmd.Use || (cmd.Use == "1" && sauPrevious != "0") { - err = os.WriteFile(fileName, sa.Serialize(), os.ModePerm) - if err != nil { - return model.ErrWriteFile.New(err) - } - if aubPrevious != cmd.Use { - aubUpdate++ - } - if cmd.Use == "1" && sauPrevious != "0" { - sauUpdate++ - } - } - } - - bar.Finish() - msg := fmt.Sprintf("Successfully updated %v: %d out of %d", aubTargetsName, aubUpdate, len(fileTargets)) - if aubUpdate == 0 { - msg = fmt.Sprintf("No files were updated for %v", aubTargetsName) - } - fmt.Println(msg) - - if cmd.Use == "1" { - msg = fmt.Sprintf("Successfully updated %v: %d out of %d", sauTargetsName, sauUpdate, len(fileTargets)) - if sauUpdate == 0 { - msg = fmt.Sprintf("No files were updated for %v", sauTargetsName) - } - fmt.Println(msg) - } - - if aubUpdate != 0 || sauUpdate != 0 { - pkg.PrintSep() - fmt.Println("To see the changes, please restart your Steam!") - } - - return nil - } -} diff --git a/internal/usecase/library/set_background_downloads/set_background_downloads.go b/internal/usecase/library/set_background_downloads/set_background_downloads.go deleted file mode 100644 index 838dd6c..0000000 --- a/internal/usecase/library/set_background_downloads/set_background_downloads.go +++ /dev/null @@ -1,101 +0,0 @@ -package set_background_downloads - -import ( - "context" - "errors" - "fmt" - "github.com/bearaujus/steam-utils/internal/config" - "github.com/bearaujus/steam-utils/internal/model" - "github.com/bearaujus/steam-utils/internal/pkg" - "github.com/bearaujus/steam-utils/internal/usecase" - "github.com/bearaujus/steam-utils/pkg/steam_acf" - "github.com/bearaujus/steam-utils/pkg/steam_path" - "github.com/spf13/cobra" - "os" - "path/filepath" - "strings" - "time" -) - -func NewCmdRunner(_ context.Context, cfg *config.Config) usecase.CmdRunner { - return func(cmd *cobra.Command, args []string) error { - se, err := steam_path.NewSteamPath(cfg.SteamPath) - if err != nil { - if errors.Is(err, steam_path.ErrEmptyPath) { - return model.ErrSteamPathIsNotSet.New(config.PersistentFlagSteamPath) - } - return model.ErrSteamPathIsInvalid.New(err, config.PersistentFlagSteamPath) - } - - files, err := os.ReadDir(se.SteamApps()) - if err != nil { - return model.ErrReadDirectory.New(err) - } - - var fileTargets []os.DirEntry - for _, file := range files { - if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".acf") { - fileTargets = append(fileTargets, file) - } - } - - if len(fileTargets) == 0 { - fmt.Printf("No .acf files detected in %v directory. Ensure that you have installed applications in your Steam library and try again.\n", se.SteamApps()) - return nil - } - - var ( - aodwrUpdate int - aodwrTargets = []string{"AppState", "AllowOtherDownloadsWhileRunning"} - aodwrTargetsName = strings.Join(aodwrTargets, ".") - bar = pkg.NewProgressBar(len(fileTargets), cmd.Short) - ) - for _, file := range fileTargets { - time.Sleep(time.Millisecond * 50) - fileName := filepath.Join(se.SteamApps(), file.Name()) - data, err := os.ReadFile(fileName) - if err != nil { - return model.ErrReadFile.New(err) - } - - sa, err := steam_acf.Parse(data) - if err != nil { - return model.ErrParseSteamACFFile.New(err) - } - - appName, err := sa.Get([]string{"AppState", "name"}) - if err != nil { - return model.ErrGetValueFromSteamACFFile.New(err) - } - bar.Add(appName) - - var aodwrPrevious string - aodwrPrevious, err = sa.Update(aodwrTargets, cmd.Use) - if err != nil { - return model.ErrUpdateValueFromSteamACFFile.New(err) - } - - if aodwrPrevious != cmd.Use { - err = os.WriteFile(fileName, sa.Serialize(), os.ModePerm) - if err != nil { - return model.ErrWriteFile.New(err) - } - aodwrUpdate++ - } - } - - bar.Finish() - msg := fmt.Sprintf("Successfully updated %v: %d out of %d", aodwrTargetsName, aodwrUpdate, len(fileTargets)) - if aodwrUpdate == 0 { - msg = fmt.Sprintf("No files were updated for %v", aodwrTargetsName) - } - fmt.Println(msg) - - if aodwrUpdate != 0 { - pkg.PrintSep() - fmt.Println("To see the changes, please restart your Steam!") - } - - return nil - } -} diff --git a/internal/usecase/type.go b/internal/usecase/type.go deleted file mode 100644 index 27582d7..0000000 --- a/internal/usecase/type.go +++ /dev/null @@ -1,5 +0,0 @@ -package usecase - -import "github.com/spf13/cobra" - -type CmdRunner func(cmd *cobra.Command, args []string) error diff --git a/internal/usecase/usecase.go b/internal/usecase/usecase.go new file mode 100644 index 0000000..cfa7467 --- /dev/null +++ b/internal/usecase/usecase.go @@ -0,0 +1,112 @@ +package usecase + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/bearaujus/steam-utils/internal/model" + "github.com/bearaujus/steam-utils/pkg/steam_acf" + "github.com/bearaujus/steam-utils/pkg/steam_path" +) + +func ListLibraryMetadata(_ context.Context, sp steam_path.SteamPath) ([]os.DirEntry, error) { + files, err := os.ReadDir(sp.SteamApps()) + if err != nil { + return nil, model.ErrReadDirectory.New(err) + } + + var ret []os.DirEntry + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".acf") { + ret = append(ret, file) + } + } + + if len(ret) == 0 { + return nil, model.ErrEmptyListLibraryMetadata.New(sp.SteamApps()) + } + + return ret, nil +} + +func SetLibraryMetadataAutoUpdate(ctx context.Context, sp steam_path.SteamPath, behaviour string) error { + fileTargets, err := ListLibraryMetadata(ctx, sp) + if err != nil { + return err + } + + var aubTargets = []string{"AppState", "AutoUpdateBehavior"} + var sauTargets = []string{"AppState", "ScheduledAutoUpdate"} + for _, file := range fileTargets { + fileName := filepath.Join(sp.SteamApps(), file.Name()) + data, err := os.ReadFile(fileName) + if err != nil { + return model.ErrReadFile.New(err) + } + + sa, err := steam_acf.Parse(data) + if err != nil { + return model.ErrParseSteamACFFile.New(err) + } + + var aubPrevious, sauPrevious string + aubPrevious, err = sa.Update(aubTargets, behaviour) + if err != nil { + return model.ErrUpdateValueFromSteamACFFile.New(err) + } + + if behaviour == model.LibraryAutoUpdateOnlyOnLaunch { + sauPrevious, err = sa.Update(sauTargets, "0") + if err != nil { + return model.ErrUpdateValueFromSteamACFFile.New(err) + } + } + + if aubPrevious != behaviour || (behaviour == model.LibraryAutoUpdateOnlyOnLaunch && sauPrevious != "0") { + err = os.WriteFile(fileName, sa.Serialize(), os.ModePerm) + if err != nil { + return model.ErrWriteFile.New(err) + } + } + } + + return nil +} + +func SetLibraryMetadataBackgroundDownloads(ctx context.Context, sp steam_path.SteamPath, behaviour string) error { + fileTargets, err := ListLibraryMetadata(ctx, sp) + if err != nil { + return err + } + + var aodwrTargets = []string{"AppState", "AllowOtherDownloadsWhileRunning"} + for _, file := range fileTargets { + fileName := filepath.Join(sp.SteamApps(), file.Name()) + data, err := os.ReadFile(fileName) + if err != nil { + return model.ErrReadFile.New(err) + } + + sa, err := steam_acf.Parse(data) + if err != nil { + return model.ErrParseSteamACFFile.New(err) + } + + var aodwrPrevious string + aodwrPrevious, err = sa.Update(aodwrTargets, behaviour) + if err != nil { + return model.ErrUpdateValueFromSteamACFFile.New(err) + } + + if aodwrPrevious != behaviour { + err = os.WriteFile(fileName, sa.Serialize(), os.ModePerm) + if err != nil { + return model.ErrWriteFile.New(err) + } + } + } + + return nil +} diff --git a/internal/view/cli/cli.go b/internal/view/cli/cli.go new file mode 100644 index 0000000..490d2fa --- /dev/null +++ b/internal/view/cli/cli.go @@ -0,0 +1,144 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/bearaujus/steam-utils/internal/config" + "github.com/bearaujus/steam-utils/internal/model" + "github.com/bearaujus/steam-utils/internal/pkg" + "github.com/bearaujus/steam-utils/internal/usecase" + "github.com/bearaujus/steam-utils/internal/view" + "github.com/bearaujus/steam-utils/pkg/steam_path" + "github.com/spf13/cobra" +) + +const ( + PersistentFlagSteamPath = "steam-path" +) + +type cli struct { + cfg *config.Config + root *cobra.Command + rawSteamPath string +} + +func New(ctx context.Context, cfg *config.Config) view.View { + app := &cli{cfg: cfg} + app.root = app.rootCmd(ctx) + var rawDefaultSteamPath string + if app.cfg.DefaultSteamPath != nil { + rawDefaultSteamPath = app.cfg.DefaultSteamPath.String() + } + app.root.PersistentFlags().StringVar(&app.rawSteamPath, PersistentFlagSteamPath, rawDefaultSteamPath, "Path to steam installation directory") + return app +} + +func (c *cli) Run(ctx context.Context) error { + pkg.PrintTitle(c.cfg) + c.root.SetContext(ctx) + if err := c.root.Execute(); err != nil { + return err + } + return nil +} + +func (c *cli) rootCmd(ctx context.Context) *cobra.Command { + root := &cobra.Command{ + Use: c.cfg.LdFlags.Name, + Args: cobra.NoArgs, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true, DisableNoDescFlag: true, DisableDescriptions: true, HiddenDefaultCmd: true}, + } + root.AddCommand(c.libraryCmd(ctx)) + return root +} + +func (c *cli) libraryCmd(ctx context.Context) *cobra.Command { + libraryCmd := &cobra.Command{ + Use: "library", + Short: "Steam library utilities", + Args: cobra.NoArgs, + } + libraryCmd.AddCommand( + c.librarySetAutoUpdateCmd(ctx), + c.librarySetBackgroundDownloadsCmd(ctx), + ) + return libraryCmd +} + +func (c *cli) librarySetAutoUpdateCmd(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "set-auto-update", + Short: "Set auto update behavior on all collections in your Steam library", + Args: cobra.NoArgs, + } + for k, v := range model.LibraryAutoUpdate { + runner := func(cmd *cobra.Command, args []string) error { + sp, err := c.getSteamPath() + if err != nil { + return err + } + err = usecase.SetLibraryMetadataAutoUpdate(ctx, sp, k) + if err != nil { + return err + } + printSuccessMessage(fmt.Sprintf("%v (%v)", cmd.CommandPath(), v)) + return nil + } + cmd.AddCommand(c.cmdRunner(k, v, runner)) + } + return cmd +} + +func (c *cli) librarySetBackgroundDownloadsCmd(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "set-background-downloads", + Short: "Set background downloads behavior on all collections in your Steam library", + Args: cobra.NoArgs, + } + for k, v := range model.LibraryBackgroundDownloads { + runner := func(cmd *cobra.Command, args []string) error { + sp, err := c.getSteamPath() + if err != nil { + return err + } + err = usecase.SetLibraryMetadataBackgroundDownloads(ctx, sp, k) + if err != nil { + return err + } + printSuccessMessage(v) + return nil + } + cmd.AddCommand(c.cmdRunner(k, v, runner)) + } + return cmd +} + +func (c *cli) cmdRunner(option string, description string, runner func(cmd *cobra.Command, args []string) error) *cobra.Command { + return &cobra.Command{ + Use: option, + Short: description, + Args: cobra.NoArgs, + RunE: runner, + } +} + +func (c *cli) getSteamPath() (steam_path.SteamPath, error) { + if c.rawSteamPath == "" && c.cfg.DefaultSteamPath != nil { + return c.cfg.DefaultSteamPath, nil + } + if c.rawSteamPath == "" && c.cfg.DefaultSteamPath != nil { + return nil, model.ErrFailToInitializeSteamPath.New(fmt.Sprintf("application is unable to determine steam path. please specify the steam path using flag '%v'", PersistentFlagSteamPath)) + } + sp, err := steam_path.NewSteamPath(c.rawSteamPath) + if err != nil { + return nil, model.ErrFailToInitializeSteamPath.New(err) + } + return sp, nil +} + +func printSuccessMessage(task string) { + fmt.Println(fmt.Sprintf("Applied: '%v'", task)) + fmt.Println("Success! To see the changes, please restart your Steam.") + fmt.Println() +} diff --git a/internal/view/interactive/interactive.go b/internal/view/interactive/interactive.go new file mode 100644 index 0000000..39a5b70 --- /dev/null +++ b/internal/view/interactive/interactive.go @@ -0,0 +1,245 @@ +package interactive + +import ( + "context" + "fmt" + + "github.com/bearaujus/steam-utils/internal/config" + "github.com/bearaujus/steam-utils/internal/model" + "github.com/bearaujus/steam-utils/internal/pkg" + "github.com/bearaujus/steam-utils/internal/usecase" + "github.com/bearaujus/steam-utils/internal/view" + "github.com/bearaujus/steam-utils/pkg/steam_path" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type interactive struct { + cfg *config.Config + app *tview.Application +} + +func New(_ context.Context, cfg *config.Config) view.View { + return &interactive{ + cfg: cfg, + app: tview.NewApplication(), + } +} + +func (it *interactive) Run(ctx context.Context) error { + if it.cfg.DefaultSteamPath != nil { + it.useDefaultSteamPathPromptCmd(ctx, nil) + } else { + it.setSteamPathCmd(ctx, nil) + } + return it.app.Run() +} + +func (it *interactive) useDefaultSteamPathPromptCmd(ctx context.Context, parent tview.Primitive) { + cmd := tview.NewModal() + cmd.SetText(fmt.Sprintf("Default Steam installation path detected at:\n%v\nDo you want to use this?", it.cfg.DefaultSteamPath.Base())) + cmd.AddButtons([]string{"Yes", "No"}) + cmd.SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonLabel == "Yes" { + it.cfg.SteamPath = it.cfg.DefaultSteamPath + it.rootCmd(ctx) + return + } + it.setSteamPathCmd(ctx, parent) + }) + it.setRoot(cmd) +} + +func (it *interactive) setSteamPathCmd(ctx context.Context, parent tview.Primitive) { + label := "Enter Steam Installation Path: " + cmd := tview.NewForm() + cmd.AddInputField(label, "", 0, nil, nil) + cmd.AddButton("Save", func() { + steamPath := cmd.GetFormItemByLabel(label).(*tview.InputField).GetText() + sp, err := steam_path.NewSteamPath(steamPath) + if err != nil { + it.showErrorModal(ctx, model.ErrFailToInitializeSteamPath.New(err).Error(), cmd) + return + } + it.cfg.SteamPath = sp + it.rootCmd(ctx) + }) + if it.cfg.DefaultSteamPath != nil { + cmd.AddButton("Use Default", func() { it.useDefaultSteamPathPromptCmd(ctx, parent) }) + } + if parent == nil { + cmd.AddButton("Quit", func() { it.app.Stop() }) + } else { + cmd.AddButton("Back", func() { it.setRoot(parent) }) + } + enableFormFocusByArrow(cmd) + it.setRoot(cmd) +} + +func (it *interactive) rootCmd(ctx context.Context) { + cmd := tview.NewList() + cmd.AddItem(it.libraryCmd(ctx, cmd)) + cmd.AddItem(it.optionsCmd(ctx, cmd)) + it.addQuit(cmd) + it.setRoot(cmd) +} + +func (it *interactive) libraryCmd(ctx context.Context, parent tview.Primitive) (string, string, rune, func()) { + return "Library", "Steam library utilities", '1', func() { + cmd := tview.NewList() + cmd.AddItem(it.librarySetAutoUpdateCmd(ctx, cmd)). + AddItem(it.librarySetBackgroundDownloadsCmd(ctx, cmd)) + it.addBack(cmd, parent) + it.setRoot(cmd) + } +} + +func (it *interactive) optionsCmd(ctx context.Context, parent tview.Primitive) (string, string, rune, func()) { + return "Options", "", 'o', func() { + cmd := tview.NewList() + cmd.AddItem("Set Steam path", it.cfg.SteamPath.String(), '1', func() { + it.setSteamPathCmd(ctx, cmd) + }) + it.addBack(cmd, parent) + it.setRoot(cmd) + } +} + +func (it *interactive) librarySetAutoUpdateCmd(ctx context.Context, parent tview.Primitive) (string, string, rune, func()) { + return "Set auto update", "Set auto update behavior on all collections in your Steam library", '1', func() { + cmd := tview.NewList() + idx := 0 + for k, v := range model.LibraryAutoUpdate { + idx++ + cmd.AddItem(v, "", rune(idx+'0'), func() { + it.showModalWithoutButton(ctx, "Processing...") + if err := usecase.SetLibraryMetadataAutoUpdate(ctx, it.cfg.SteamPath, k); err != nil { + it.showErrorModal(ctx, err.Error(), cmd) + return + } + it.showModal(ctx, "Success!\nTo see the changes, please restart your Steam.", nil) + }) + } + it.addBack(cmd, parent) + it.setRoot(cmd) + } +} + +func (it *interactive) librarySetBackgroundDownloadsCmd(ctx context.Context, parent tview.Primitive) (string, string, rune, func()) { + return "Set background downloads", "Set background downloads behavior on all collections in your Steam library", '2', func() { + cmd := tview.NewList() + idx := 0 + for k, v := range model.LibraryBackgroundDownloads { + idx++ + cmd.AddItem(v, "", rune(idx+'0'), func() { + it.showModalWithoutButton(ctx, "Processing...") + if err := usecase.SetLibraryMetadataBackgroundDownloads(ctx, it.cfg.SteamPath, k); err != nil { + it.showErrorModal(ctx, err.Error(), cmd) + return + } + it.showModal(ctx, "Success!\nTo see the changes, please restart your Steam.", nil) + }) + } + it.addBack(cmd, parent) + it.setRoot(cmd) + } +} + +func (it *interactive) setRoot(cmd tview.Primitive) { + // Wrap the cmd in a Frame for padding + frame := tview.NewFrame(cmd). + SetBorders(1, 1, 0, 0, 1, 1) // Padding: Top, Bottom, Left, Right, Border Width + + // Configure the flex container + flex := tview.NewFlex() + flex.SetTitle(fmt.Sprintf(" %v ", pkg.GetTitleRaw(it.cfg))) + flex.SetDirection(tview.FlexRow) + flex.SetBorder(true) + flex.AddItem(frame, 0, 1, false) + + // Set the root and focus + it.app.SetRoot(flex, true) + it.app.SetFocus(cmd) +} + +func (it *interactive) addBack(cmd *tview.List, backPage tview.Primitive) { + cmd.AddItem("Back", "", '0', func() { + it.setRoot(backPage) + }) +} + +func (it *interactive) addQuit(cmd *tview.List) { + cmd.AddItem("Quit", "", 'q', func() { + it.app.Stop() + }) +} + +func (it *interactive) showModalWithoutButton(_ context.Context, text string) { + cmd := tview.NewModal() + cmd.SetText(text) + it.setRoot(cmd) +} + +func (it *interactive) showModal(ctx context.Context, text string, backPage tview.Primitive) { + cmd := tview.NewModal() + cmd.SetText(text) + cmd.AddButtons([]string{"Ok"}) + cmd.SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if backPage == nil { + it.rootCmd(ctx) + return + } + it.setRoot(backPage) + }) + it.setRoot(cmd) +} + +func (it *interactive) showErrorModal(ctx context.Context, text string, backPage tview.Primitive) { + it.showModal(ctx, fmt.Sprintf("Error: %v", text), backPage) +} + +func enableFormFocusByArrow(cmd *tview.Form) { + i, fc, bc := 0, cmd.GetFormItemCount(), cmd.GetButtonCount() + t := fc + bc // Total focusable items + cmd.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyUp: + // Move focus upwards + if i > 0 { + // Skip over buttons if they're at the bottom row + if i >= fc { + i = fc - 1 // Jump to the last form item + } else { + i-- // Move up within form items + } + cmd.SetFocus(i) + } + case tcell.KeyDown: + // Move focus downwards + if i < t-1 { + // Jump to buttons if currently at the last form item + if i < fc-1 { + i++ + } else { + i = fc // Jump to the first button + } + cmd.SetFocus(i) + } + case tcell.KeyRight: + // Move right through buttons + if i >= fc && i < t-1 { + i++ + cmd.SetFocus(i) + } + case tcell.KeyLeft: + // Move left through buttons + if i > fc { + i-- + cmd.SetFocus(i) + } + default: + return event + } + return nil + }) +} diff --git a/internal/view/type.go b/internal/view/type.go new file mode 100644 index 0000000..09b346a --- /dev/null +++ b/internal/view/type.go @@ -0,0 +1,7 @@ +package view + +import "context" + +type View interface { + Run(ctx context.Context) error +} diff --git a/main.go b/main.go index 7d790d6..e8fd362 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,13 @@ package main import ( "context" "fmt" - "github.com/bearaujus/steam-utils/internal/cli" + "os" + "time" + "github.com/bearaujus/steam-utils/internal/config" - "github.com/bearaujus/steam-utils/internal/pkg" + "github.com/bearaujus/steam-utils/internal/view/cli" + "github.com/bearaujus/steam-utils/internal/view/interactive" + "github.com/inconshreveable/mousetrap" ) // these variable will be retrieved from -ldflags @@ -18,22 +22,27 @@ var ( ) func main() { - var ( - cfg = config.NewConfig(&config.LdFlags{ - Name: name, - Version: version, - Arch: arch, - Goos: goos, - File: file, - }) - ctx = context.TODO() - rootCLI = cli.NewRoot(ctx, cfg) - ) - if err := config.LoadConfig(rootCLI, cfg); err != nil { - fmt.Println(err) + ctx := context.TODO() + cfg := config.NewConfig(&config.LdFlags{ + Name: name, + Version: version, + Arch: arch, + Goos: goos, + File: file, + }) + + var err error + if cfg.LdFlags.Goos == "windows" && mousetrap.StartedByExplorer() { + err = interactive.New(ctx, cfg).Run(ctx) + } else { + err = cli.New(ctx, cfg).Run(ctx) + } + if err != nil { + fmt.Println(err.Error()) + time.Sleep(time.Second * 10) + os.Exit(1) return } - pkg.PrintTitle(cfg) - _ = rootCLI.Execute() - fmt.Println() + + os.Exit(0) } diff --git a/pkg/steam_path/steam_path.go b/pkg/steam_path/steam_path.go index ed84f5f..b69763a 100644 --- a/pkg/steam_path/steam_path.go +++ b/pkg/steam_path/steam_path.go @@ -10,6 +10,7 @@ import ( type SteamPath interface { Base() string SteamApps() string + String() string } type steamPath struct { @@ -38,6 +39,10 @@ func (sp *steamPath) SteamApps() string { return path.Join(sp.basePath, "steamapps") } +func (sp *steamPath) String() string { + return sp.basePath +} + func (sp *steamPath) validate() error { paths := []string{ sp.SteamApps(),