diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml new file mode 100644 index 0000000..ceb06c9 --- /dev/null +++ b/.github/workflows/pull_request.yaml @@ -0,0 +1,46 @@ +on: + pull_request: + push: + branches: + - master + - qa + - uat + +jobs: + test: + runs-on: ubuntu-latest + container: golang:1.23-alpine + env: + ENVIRONMENT: ci + steps: + - uses: actions/checkout@v4 + - run: apk update && apk add curl openssl git openssh-client build-base && mkdir -p /root/.ssh + - run: wget -O /usr/bin/mockgen https://github.com/skynet2/mock/releases/latest/download/mockgen && chmod 777 /usr/bin/mockgen + - run: go test -json -coverprofile=/root/coverage_temp.txt -covermode=atomic ./... > /root/test.json + - run: cat /root/coverage_temp.txt | grep -v "_mock.go" | grep -v "_mocks.go" | grep -v "_mocks_test.go" | grep -v "_mock_test.go" > /root/coverage.txt || true + - name: Upload coverage report + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: /root/coverage.txt + if: (!(startsWith(github.event.pull_request.title, 'Deploy to'))) + - run: cat /root/test.json + if: always() + - run: wget https://github.com/mfridman/tparse/releases/latest/download/tparse_linux_x86_64 -O tparse && chmod 777 tparse && ./tparse -all -file=/root/test.json + if: always() + - uses: guyarb/golang-test-annotations@v0.7.0 + if: always() + with: + test-results: /root/test.json + lint: + runs-on: ubuntu-latest + container: golang:1.23-alpine + env: + ENVIRONMENT: ci + steps: + - uses: actions/checkout@v4 + - uses: golangci/golangci-lint-action@v6.1.1 + if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/qa' && github.ref != 'refs/heads/uat' + with: + version: latest + args: --timeout=5m --tests=false ./... diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..99f48bc --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,55 @@ +on: + push: + branches: + - master + - initial + +jobs: + version: + runs-on: ubuntu-latest + env: + MAJOR_VERSION: 0 + outputs: + versionSemVerOut: v${{ steps.semVer.outputs.semVersion }} + steps: + - id: semVer + run: echo "semVersion=$MAJOR_VERSION.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT" + ci: + runs-on: ubuntu-latest + container: golang:1.23-bookworm + needs: + - version + env: + SEM_VERSION: ${{needs.version.outputs.versionSemVerOut}} + steps: + - name: Install zip + uses: montudor/action-zip@v1 + - run: apt-get update && apt-get install -y build-essential git + + - uses: actions/checkout@v4 + - run: make build + + - name: release + uses: actions/create-release@v1 + id: create_release + with: + draft: false + prerelease: false + release_name: ${{ env.SEM_VERSION }} + tag_name: ${{ env.SEM_VERSION }} + env: + GITHUB_TOKEN: ${{ github.token }} + + - uses: montudor/action-zip@v1 + with: + args: zip -qq -r browser-switcher-${{ env.SEM_VERSION }}-windows-amd64.zip dist win + + - name: upload windows artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: browser-switcher-${{ env.SEM_VERSION }}-windows-amd64.zip + asset_name: browser-switcher-${{ env.SEM_VERSION }}-windows-amd64.zip + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore index 6f72f89..384cda3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,9 @@ go.work.sum # env file .env + +.idea/ + +fyne-cross/ + +dist/ diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000..b9c666c Binary files /dev/null and b/Icon.png differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..096f62f --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: build-debug +build-debug: build-win + @cp dist/win/BrowserSwitcher.exe /mnt/i/BrowserSwitcher.exe + @#GOOS=windows go build -o /mnt/i/BrowserSwitcher.exe . + @#cp scripts/register.ps1 /mnt/i/register.ps1 + @#cp scripts/reg.reg /mnt/i/reg.reg + @#fyne-cross windows -arch=amd64 -app-id com.ft-t.browser-switcher + @#cp fyne-cross/bin/windows-amd64/browser-switcher.exe /mnt/i/BrowserSwitcher.exe +## powershell -ExecutionPolicy Bypass -File register.ps1 + +.PHONY: lint +lint: + @golangci-lint run + +.PHONY: build +build: build-win build-linux + +.PHONY: build-linux +build-linux: + @mkdir -p dist + @GOOS=linux go build -buildvcs=false -o dist/BrowserSwitcher . + +.PHONY: build-win +build-win: + @mkdir -p dist/win + @GOOS=windows go build -buildvcs=false -o dist/win/BrowserSwitcher.exe . + @cp scripts/reg.reg dist/win/reg.reg diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9772108 --- /dev/null +++ b/go.mod @@ -0,0 +1,45 @@ +module github.com/ft-t/browser-switcher + +go 1.23.4 + +require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/cockroachdb/errors v1.11.3 + github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.10.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.6.0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/getsentry/sentry-go v0.30.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.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/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a54a959 --- /dev/null +++ b/go.sum @@ -0,0 +1,125 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= +github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo= +github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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/main.go b/main.go new file mode 100644 index 0000000..3a79130 --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gopkg.in/natefinch/lumberjack.v2" + + config2 "github.com/ft-t/browser-switcher/pkg/config" + "github.com/ft-t/browser-switcher/pkg/launcher" + "github.com/ft-t/browser-switcher/pkg/selector" + "github.com/ft-t/browser-switcher/pkg/ui" +) + +func main() { + logFile := &lumberjack.Logger{ + Filename: "logs/log.log", + MaxSize: 30, + MaxBackups: 3, + MaxAge: 10, + Compress: false, + } + lg := zerolog.New(zerolog.MultiLevelWriter(os.Stdout, logFile)).With().Timestamp().Logger() + log.Logger = lg + + lg.Info().Msg("Starting browser-switcher") + ctx := lg.WithContext(context.Background()) + + if len(os.Args) < 2 { + lg.Panic().Msg("No arguments provided") + return + } + + targetURL := os.Args[1] + + ctx = lg.With().Str("targetURL", targetURL).Logger().WithContext(ctx) + + browserConfig, err := config2.ReadConfig(ctx) + + if err != nil { + lg.Err(err).Msg("Failed to read config") + return + } + + browserSelector := selector.New(browserConfig) + + targetBrowser := browserSelector.SelectBrowser(ctx, targetURL) + + browserLauncher := launcher.New(targetURL) + uiRenderer, err := ui.NewUi(browserConfig, browserLauncher) + if err != nil { + lg.Panic().Err(err).Msg("Failed to create UI") + return + } + + if targetBrowser == nil { + if err = uiRenderer.ShowManualSelect(ctx); err != nil { + lg.Panic().Err(err).Msg("Failed to show manual browser selection") + return + } + } + + if err = browserLauncher.Launch(ctx, targetBrowser); err != nil { + lg.Panic().Err(err).Msg("Failed to launch browser") + return + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..bedaba4 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,52 @@ +package config + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + + "github.com/rs/zerolog" +) + +type Config struct { + Browsers []*Browser `json:"browsers"` + UI UI `json:"ui"` +} + +type UI struct { + Renderer string `json:"renderer"` +} + +type Browser struct { + ID string `json:"id"` + Name string `json:"name"` + LaunchArgs []string `json:"launch_args"` + BinaryPath string `json:"binary_path"` + CustomIconPath string `json:"custom_icon_path"` + Rules []string `json:"rules"` +} + +func ReadConfig(ctx context.Context) (*Config, error) { + targetFile := "config.json" + + if homeDir, err := os.UserHomeDir(); err == nil { + targetFile = filepath.Join(homeDir, "BrowserSwitcher", "config.json") + } else { + zerolog.Ctx(ctx).Error().Err(err).Msg("failed to get user home directory") + } + + zerolog.Ctx(ctx).Info().Msgf("loading config from %s", targetFile) + + fileData, err := os.ReadFile(targetFile) + if err != nil { + return nil, err + } + + var targetConfig Config + if err = json.Unmarshal(fileData, &targetConfig); err != nil { + return nil, err + } + + return &targetConfig, nil +} diff --git a/pkg/launcher/launcher.go b/pkg/launcher/launcher.go new file mode 100644 index 0000000..fb6a29e --- /dev/null +++ b/pkg/launcher/launcher.go @@ -0,0 +1,29 @@ +package launcher + +import ( + "context" + "os/exec" + "slices" + + "github.com/ft-t/browser-switcher/pkg/config" +) + +type Launcher struct { + targetURL string +} + +func New(targetURL string) *Launcher { + return &Launcher{ + targetURL: targetURL, + } +} + +func (l *Launcher) Launch(_ context.Context, browser *config.Browser) error { + return exec.Command( + browser.BinaryPath, + slices.Concat( + []string{l.targetURL}, + browser.LaunchArgs, + )..., + ).Start() +} diff --git a/pkg/launcher/launcher_test.go b/pkg/launcher/launcher_test.go new file mode 100644 index 0000000..a297033 --- /dev/null +++ b/pkg/launcher/launcher_test.go @@ -0,0 +1,16 @@ +package launcher_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ft-t/browser-switcher/pkg/config" + "github.com/ft-t/browser-switcher/pkg/launcher" +) + +func TestLauncher(t *testing.T) { + l := launcher.New("https://example.com") + assert.Error(t, l.Launch(context.TODO(), &config.Browser{})) +} diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go new file mode 100644 index 0000000..0ef3e5e --- /dev/null +++ b/pkg/selector/selector.go @@ -0,0 +1,43 @@ +package selector + +import ( + "context" + "regexp" + + "github.com/rs/zerolog" + + "github.com/ft-t/browser-switcher/pkg/config" +) + +type Selector struct { + cfg *config.Config +} + +func New( + cfg *config.Config, +) *Selector { + return &Selector{ + cfg: cfg, + } +} + +func (s *Selector) SelectBrowser( + ctx context.Context, + targetURL string, +) *config.Browser { + for _, browser := range s.cfg.Browsers { + for _, rule := range browser.Rules { + lg := zerolog.Ctx(ctx).With().Str("browser_id", browser.ID). + Str("rule", rule).Logger() + + if matched, err := regexp.MatchString(rule, targetURL); matched { + lg.Debug().Msg("Matched rule") + return browser + } else if err != nil { + lg.Error().Err(err).Msg("Failed to execute rule") + } + } + } + + return nil +} diff --git a/pkg/ui/interfaces.go b/pkg/ui/interfaces.go new file mode 100644 index 0000000..0f4c8c4 --- /dev/null +++ b/pkg/ui/interfaces.go @@ -0,0 +1,11 @@ +package ui + +import ( + "context" + + "github.com/ft-t/browser-switcher/pkg/config" +) + +type Launcher interface { + Launch(_ context.Context, browser *config.Browser) error +} diff --git a/pkg/ui/internal/bubbles/bubbles.go b/pkg/ui/internal/bubbles/bubbles.go new file mode 100644 index 0000000..0350aee --- /dev/null +++ b/pkg/ui/internal/bubbles/bubbles.go @@ -0,0 +1,48 @@ +package bubbles + +import ( + "context" + "fmt" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "github.com/ft-t/browser-switcher/pkg/config" +) + +type Bubbles struct { + cfg *config.Config + launcher Launcher +} + +func NewBubbles( + cfg *config.Config, + launcher Launcher, +) *Bubbles { + return &Bubbles{ + cfg: cfg, + launcher: launcher, + } +} + +func (b *Bubbles) ShowManualSelect( + ctx context.Context, +) error { + var items []list.Item + for _, browser := range b.cfg.Browsers { + items = append(items, item{ + title: browser.Name, + desc: fmt.Sprintf("Rules: %v", len(browser.Rules)), + launch: func() error { + return b.launcher.Launch(ctx, browser) + }, + }) + } + + m := model{list: list.New(items, list.NewDefaultDelegate(), 0, 0)} + m.list.Title = "Browsers" + + _, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + + return err +} diff --git a/pkg/ui/internal/bubbles/interfaces.go b/pkg/ui/internal/bubbles/interfaces.go new file mode 100644 index 0000000..2b90c3a --- /dev/null +++ b/pkg/ui/internal/bubbles/interfaces.go @@ -0,0 +1,11 @@ +package bubbles + +import ( + "context" + + "github.com/ft-t/browser-switcher/pkg/config" +) + +type Launcher interface { + Launch(_ context.Context, browser *config.Browser) error +} diff --git a/pkg/ui/internal/bubbles/item.go b/pkg/ui/internal/bubbles/item.go new file mode 100644 index 0000000..d19aea8 --- /dev/null +++ b/pkg/ui/internal/bubbles/item.go @@ -0,0 +1,11 @@ +package bubbles + +type item struct { + title string + desc string + launch func() error +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return i.desc } +func (i item) FilterValue() string { return i.title } diff --git a/pkg/ui/internal/bubbles/model.go b/pkg/ui/internal/bubbles/model.go new file mode 100644 index 0000000..0821221 --- /dev/null +++ b/pkg/ui/internal/bubbles/model.go @@ -0,0 +1,56 @@ +package bubbles + +import ( + "os" + "strconv" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/rs/zerolog" +) + +var docStyle = lipgloss.NewStyle().Margin(1, 2) + +type model struct { + list list.Model + logger zerolog.Logger +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(teamMsg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := teamMsg.(type) { + case tea.KeyMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if msg.String() == "enter" { + if err := m.list.SelectedItem().(item).launch(); err != nil { + m.logger.Panic().Err(err).Msg("failed to launch browser") + } + + os.Exit(0) + } + if parsed, _ := strconv.ParseInt(msg.String(), 10, 64); parsed > 0 { + if err := m.list.Items()[parsed-1].(item).launch(); err != nil { + m.logger.Panic().Err(err).Msg("failed to launch browser") + } + + os.Exit(0) + } + case tea.WindowSizeMsg: + h, v := docStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(teamMsg) + return m, cmd +} + +func (m model) View() string { + return docStyle.Render(m.list.View()) +} diff --git a/pkg/ui/internal/fyne/fyne.go b/pkg/ui/internal/fyne/fyne.go new file mode 100644 index 0000000..df13fe3 --- /dev/null +++ b/pkg/ui/internal/fyne/fyne.go @@ -0,0 +1,58 @@ +package fyne + +// +//type tappableContainer struct { +// *fyne.Container +// OnTapped func() +//} +// +//func (b *tappableContainer) CreateRenderer() fyne.WidgetRenderer { +// return widget.NewSimpleRenderer(b.Container) +//} +// +//func (b *tappableContainer) Tapped(ev *fyne.PointEvent) { +// b.OnTapped() +//} +// +//func showUI(cfg *config2.Config, targetURL string) { +// myApp := app.NewWithID("com.ft-t.browser-switcher") +// myWindow := myApp.NewWindow("Browser List") +// +// var elements []fyne.CanvasObject +// for _, browser := range cfg.Browsers { +// name := widget.NewLabel(browser.Name) +// image := canvas.NewImageFromResource(theme.FyneLogo()) +// image.ScaleMode = canvas.ImageScaleFastest +// image.FillMode = canvas.ImageFillOriginal +// //image.Resize(fyne.NewSize(128, 128)) +// //image.Refresh() +// +// name.Alignment = fyne.TextAlignCenter +// +// vbox := container.NewBorder(image, name, nil, nil) +// redBackground := canvas.NewRectangle(color.RGBA{R: 0, G: 0, B: 0, A: 0}) +// redBackground.StrokeWidth = 1 +// redBackground.StrokeColor = color.RGBA{R: 255, G: 255, B: 255, A: 255} +// withBorder := container.NewStack(vbox, redBackground) // White border around the red background +// +// //elements = append(elements, &tappableContainer{ +// // Container: vbox, +// // OnTapped: func() { +// // if err := runBrowser(browser, targetURL); err != nil { +// // fmt.Printf("Failed to launch %s: %v\n", browser.Name, err) +// // } +// // os.Exit(0) +// // }, +// //}) +// +// elements = append(elements, withBorder) +// } +// +// wrap := container.NewHBox(elements...) +// // Set the content of the window +// //scroll := container.NewScroll(browserList) +// myWindow.SetContent(wrap) +// +// // Set window size and show +// myWindow.ShowAndRun() +//} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go new file mode 100644 index 0000000..7dd14d0 --- /dev/null +++ b/pkg/ui/ui.go @@ -0,0 +1,28 @@ +package ui + +import ( + "context" + + "github.com/cockroachdb/errors" + + "github.com/ft-t/browser-switcher/pkg/config" + "github.com/ft-t/browser-switcher/pkg/ui/internal/bubbles" +) + +type UI interface { + ShowManualSelect( + ctx context.Context, + ) error +} + +func NewUi( + cfg *config.Config, + launcher Launcher, +) (UI, error) { + switch cfg.UI.Renderer { + case "bubbles", "": + return bubbles.NewBubbles(cfg, launcher), nil + default: + return nil, errors.Newf("unknown renderer") + } +} diff --git a/scripts/reg.reg b/scripts/reg.reg new file mode 100644 index 0000000..4b34f58 --- /dev/null +++ b/scripts/reg.reg @@ -0,0 +1,38 @@ +Windows Registry Editor Version 5.00 + +; Infamous capabilities: + +[HKEY_LOCAL_MACHINE\SOFTWARE\BrowserSwitcher\Capabilities] +"ApplicationDescription"="BrowserSwitcher" +"ApplicationIcon"="I:\\BrowserSwitcher.exe,0" +"ApplicationName"="BrowserSwitcher" + +[HKEY_LOCAL_MACHINE\SOFTWARE\BrowserSwitcher\Capabilities\FileAssociations] +".htm"="BrowserSwitcherURL" +".html"="BrowserSwitcherURL" +".shtml"="BrowserSwitcherURL" +".xht"="BrowserSwitcherURL" +".xhtml"="BrowserSwitcherURL" + +[HKEY_LOCAL_MACHINE\SOFTWARE\BrowserSwitcher\Capabilities\URLAssociations] +"ftp"="BrowserSwitcherURL" +"http"="BrowserSwitcherURL" +"https"="BrowserSwitcherURL" + +; Register to Default Programs + +[HKEY_LOCAL_MACHINE\SOFTWARE\RegisteredApplications] +"BrowserSwitcher"="Software\\BrowserSwitcher\\Capabilities" + +; BrowserSwitcherURL HANDLER: + +[HKEY_LOCAL_MACHINE\Software\Classes\BrowserSwitcherURL] +@="BrowserSwitcher Document" +"FriendlyTypeName"="BrowserSwitcher Document" + +[HKEY_LOCAL_MACHINE\Software\Classes\BrowserSwitcherURL\shell] + +[HKEY_LOCAL_MACHINE\Software\Classes\BrowserSwitcherURL\shell\open] + +[HKEY_LOCAL_MACHINE\Software\Classes\BrowserSwitcherURL\shell\open\command] +@="\"I:\\BrowserSwitcher.exe\" \"%1\""