├── .build-and-release.sh ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── alfred-workflow-release.yml │ ├── markdownlint.yml │ ├── pr-title.yml │ └── stale-bot.yml ├── .gitignore ├── .markdownlint.yaml ├── .rsync-exclude ├── 0BAF56E8-B65A-478A-B848-F09FFE73A12B.png ├── 72D18DBC-72FF-40AA-B367-D4C148AEC3A6.png ├── Justfile ├── LICENSE ├── README.md ├── icon.png ├── info.plist └── scripts ├── brew-info.sh ├── brew-install.js └── brew-uninstall.js /.build-and-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | #─────────────────────────────────────────────────────────────────────────────── 3 | 4 | # goto git root 5 | cd "$(git rev-parse --show-toplevel)" || return 1 6 | 7 | # Prompt for next version number 8 | current_version=$(plutil -extract version xml1 -o - info.plist | sed -n 's/.*\(.*\)<\/string>.*/\1/p') 9 | echo "current version: $current_version" 10 | echo -n " next version: " 11 | read -r next_version 12 | echo "────────────────────────" 13 | 14 | # GUARD 15 | if [[ -z "$next_version" || "$next_version" == "$current_version" ]]; then 16 | print "\033[1;31mInvalid version number.\033[0m" 17 | return 1 18 | fi 19 | 20 | # update version number in THE REPO'S `info.plist` 21 | plutil -replace version -string "$next_version" info.plist 22 | 23 | #─────────────────────────────────────────────────────────────────────────────── 24 | # INFO this assumes the local folder is named the same as the github repo 25 | # 1. update version number in LOCAL `info.plist` 26 | # 2. convenience: copy download link for current version 27 | 28 | # update version number in LOCAL `info.plist` 29 | prefs_location=$(defaults read com.runningwithcrayons.Alfred-Preferences syncfolder | sed "s|^~|$HOME|") 30 | workflow_uid="$(basename "$PWD")" 31 | local_info_plist="$prefs_location/Alfred.alfredpreferences/workflows/$workflow_uid/info.plist" 32 | if [[ -f "$local_info_plist" ]] ; then 33 | plutil -replace version -string "$next_version" "$local_info_plist" 34 | else 35 | print "\033[1;33mCould not increment version, local \`info.plist\` not found: '$local_info_plist'\033[0m" 36 | return 1 37 | fi 38 | 39 | # copy download link for current version 40 | msg="Available in the Alfred Gallery in 1-2 days, or directly by downloading the latest release here:" 41 | github_user=$(git remote --verbose | head -n1 | sed -E 's/.*github.com[:\](.*)\/.*/\1/') 42 | url="https://github.com/$github_user/$workflow_uid/releases/download/$next_version/${workflow_uid}.alfredworkflow" 43 | echo -n "$msg $url" | pbcopy 44 | 45 | #─────────────────────────────────────────────────────────────────────────────── 46 | 47 | # commit and push 48 | git add --all && 49 | git commit -m "release: $next_version" && 50 | git pull --no-progress && 51 | git push --no-progress && 52 | git tag "$next_version" && # pushing a tag triggers the github release action 53 | git push --no-progress origin --tags 54 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository 2 | 3 | custom: https://www.paypal.me/ChrisGrieser 4 | ko_fi: pseudometa 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "Bug: " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have [updated to the latest version](./releases/latest) of this workflow. 12 | required: true 13 | - label: I am using Alfred 5. (Older versions are not supported anymore.) 14 | required: true 15 | - type: textarea 16 | id: bug-description 17 | attributes: 18 | label: Bug Description 19 | description: A clear and concise description of the bug. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: screenshot 24 | attributes: 25 | label: Relevant Screenshot 26 | description: If applicable, add screenshots or a screen recording to help explain your problem. 27 | - type: textarea 28 | id: reproduction-steps 29 | attributes: 30 | label: To Reproduce 31 | description: Steps to reproduce the problem 32 | placeholder: | 33 | For example: 34 | 1. Go to '...' 35 | 2. Click on '...' 36 | 3. Scroll down to '...' 37 | - type: textarea 38 | id: debugging-log 39 | attributes: 40 | label: Debugging Log 41 | description: 42 | "You can get a debugging log by opening the workflow in Alfred preferences and pressing `⌘ + D`. 43 | A small window will open up which will log everything happening during the execution of the 44 | Workflow. Use the malfunctioning part of the workflow once more, copy the content of the log 45 | window, and paste it here. If the debugging log is long, please attach it as file instead of 46 | pasting everything in here." 47 | render: Text 48 | validations: 49 | required: true 50 | - type: textarea 51 | id: workflow-configuration 52 | attributes: 53 | label: Workflow Configuration 54 | description: 55 | "Please add a screenshot of your [workflow 56 | configuration](https://www.alfredapp.com/help/workflows/user-configuration/)." 57 | validations: 58 | required: true 59 | - type: textarea 60 | id: homebrew-info 61 | attributes: 62 | label: Homebrew Information 63 | description: 64 | "Please run the following in your terminal and share the output: `brew update ; brew --prefix ; 65 | brew --version`." 66 | validations: 67 | required: true 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: feature-requested 8 | attributes: 9 | label: Feature Requested 10 | description: A clear and concise description of the feature. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain the request. 18 | - type: checkboxes 19 | id: checklist 20 | attributes: 21 | label: Checklist 22 | options: 23 | - label: The feature would be useful to more users than just me. 24 | required: true 25 | 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(dependabot): " 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What problem does this PR solve? 2 | 3 | ## How does the PR solve it? 4 | 5 | ## Checklist 6 | - [ ] Used only `camelCase` variable names. 7 | - [ ] If functionality is added or modified, also made respective changes to the 8 | `README.md` and the internal workflow documentation. 9 | -------------------------------------------------------------------------------- /.github/workflows/alfred-workflow-release.yml: -------------------------------------------------------------------------------- 1 | name: Alfred Workflow Release 2 | 3 | on: 4 | push: 5 | tags: ["*"] 6 | 7 | env: 8 | WORKFLOW_NAME: ${{ github.event.repository.name }} 9 | 10 | #─────────────────────────────────────────────────────────────────────────────── 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | permissions: { contents: write } 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Build .alfredworkflow 21 | run: | 22 | zip --recurse-paths --symlinks "${{ env.WORKFLOW_NAME }}.alfredworkflow" . \ 23 | --exclude "README.md" ".git*" "Justfile" ".build-and-release.sh" \ 24 | ".rsync-exclude" ".editorconfig" ".typos.toml" ".markdownlint.*" 25 | 26 | - name: Create release notes 27 | id: release_notes 28 | uses: mikepenz/release-changelog-builder-action@v5 29 | env: 30 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 31 | with: 32 | mode: "COMMIT" 33 | configurationJson: | 34 | { 35 | "label_extractor": [{ 36 | "pattern": "^(\\w+)(\\([\\w\\-\\.]+\\))?(!)?: .+", 37 | "on_property": "title", 38 | "target": "$1" 39 | }], 40 | "categories": [ 41 | { "title": "## ⚠️ Breaking changes", "labels": ["break"] }, 42 | { "title": "## 🚀 New features", "labels": ["feat", "improv"] }, 43 | { "title": "## 🛠️ Fixes", "labels": ["fix", "perf", "chore"] }, 44 | { "title": "## 👾 Other", "labels": [] } 45 | ], 46 | "ignore_labels": ["release", "bump"] 47 | } 48 | 49 | - name: Release 50 | uses: softprops/action-gh-release@v2 51 | with: 52 | token: ${{ secrets.GITHUB_TOKEN }} 53 | body: ${{ steps.release_notes.outputs.changelog }} 54 | files: ${{ env.WORKFLOW_NAME }}.alfredworkflow 55 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Markdownlint check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.md" 8 | - ".github/workflows/markdownlint.yml" 9 | - ".markdownlint.*" # markdownlint config files 10 | pull_request: 11 | paths: 12 | - "**.md" 13 | 14 | jobs: 15 | markdownlint: 16 | name: Markdownlint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: DavidAnson/markdownlint-cli2-action@v20 21 | with: 22 | globs: "**/*.md" 23 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-pull-request: 17 | name: Check PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | requireScope: false 25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital 26 | types: | # add `improv` to the list of allowed types 27 | improv 28 | fix 29 | feat 30 | refactor 31 | build 32 | ci 33 | style 34 | test 35 | chore 36 | perf 37 | docs 38 | break 39 | revert 40 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale bot 2 | on: 3 | schedule: 4 | - cron: "18 04 * * 3" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close stale issues 15 | uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | # DOCS https://github.com/actions/stale#all-options 20 | days-before-stale: 180 21 | days-before-close: 7 22 | stale-issue-label: "Stale" 23 | stale-issue-message: | 24 | This issue has been automatically marked as stale. 25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity, and will not be monitored. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Alfred 5 | prefs.plist 6 | *.alfredworkflow 7 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Defaults https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 3 | #─────────────────────────────────────────────────────────────────────────────── 4 | 5 | # MODIFIED SETTINGS 6 | blanks-around-headings: 7 | lines_below: 0 # space waster 8 | ul-style: { style: sublist } 9 | 10 | # not autofixable 11 | ol-prefix: { style: ordered } 12 | line-length: 13 | tables: false 14 | code_blocks: false 15 | no-inline-html: 16 | allowed_elements: [img, details, summary, kbd, a, br] 17 | 18 | #───────────────────────────────────────────────────────────────────────────── 19 | # DISABLED 20 | ul-indent: false # not compatible with using tabs 21 | no-hard-tabs: false # taken care of by editorconfig 22 | blanks-around-lists: false # space waster 23 | first-line-heading: false # e.g., ignore-comments 24 | no-emphasis-as-heading: false # sometimes useful 25 | -------------------------------------------------------------------------------- /.rsync-exclude: -------------------------------------------------------------------------------- 1 | # vim: ft=gitignore 2 | #─────────────────────────────────────────────────────────────────────────────── 3 | 4 | # git 5 | .git/ 6 | .gitignore 7 | 8 | # Alfred 9 | prefs.plist 10 | .rsync-exclude 11 | 12 | # docs 13 | docs/ 14 | LICENSE 15 | # INFO leading `/` -> ignore only the README in the root, not in subfolders 16 | /README.md 17 | 18 | # build 19 | Justfile 20 | .github/ 21 | .build-and-release.sh 22 | 23 | # linter & types 24 | .typos.toml 25 | .editorconfig 26 | .markdownlint.yaml 27 | jxa-globals.d.ts 28 | jsconfig.json 29 | alfred.d.ts 30 | -------------------------------------------------------------------------------- /0BAF56E8-B65A-478A-B848-F09FFE73A12B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisgrieser/alfred-homebrew/8d765220428f4767e8c90c7f29f5f352cf9ec7bc/0BAF56E8-B65A-478A-B848-F09FFE73A12B.png -------------------------------------------------------------------------------- /72D18DBC-72FF-40AA-B367-D4C148AEC3A6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisgrieser/alfred-homebrew/8d765220428f4767e8c90c7f29f5f352cf9ec7bc/72D18DBC-72FF-40AA-B367-D4C148AEC3A6.png -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set quiet := true 2 | 3 | # REQUIRED local workflow uses same folder name 4 | 5 | workflow_uid := `basename "$PWD"` 6 | prefs_location := `defaults read com.runningwithcrayons.Alfred-Preferences syncfolder | sed "s|^~|$HOME|"` 7 | local_workflow := prefs_location / "Alfred.alfredpreferences/workflows" / workflow_uid 8 | 9 | #─────────────────────────────────────────────────────────────────────────────── 10 | 11 | transfer-changes-FROM-local: 12 | #!/usr/bin/env zsh 13 | rsync --archive --delete --exclude-from="$PWD/.rsync-exclude" "{{ local_workflow }}/" "$PWD" 14 | git status --short 15 | 16 | transfer-changes-TO-local: 17 | #!/usr/bin/env zsh 18 | rsync --archive --delete --exclude-from="$PWD/.rsync-exclude" "$PWD/" "{{ local_workflow }}" 19 | cd "{{ local_workflow }}" 20 | print "\e[1;34mChanges at the local workflow:\e[0m" 21 | git status --short . 22 | 23 | [macos] 24 | open-local-workflow-in-alfred: 25 | #!/usr/bin/env zsh 26 | # using JXA and URI for redundancy, as both are not 100 % reliable https://www.alfredforum.com/topic/18390-get-currently-edited-workflow-uri/ 27 | open "alfredpreferences://navigateto/workflows>workflow>{{ workflow_uid }}" 28 | osascript -e 'tell application id "com.runningwithcrayons.Alfred" to reveal workflow "{{ workflow_uid }}"' 29 | 30 | release: 31 | ./.build-and-release.sh 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Grieser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebrew Search for Alfred 2 | ![GitHub downloads](https://img.shields.io/github/downloads/chrisgrieser/alfred-homebrew/total?label=GitHub%20Downloads&style=plastic&logo=github) 3 | ![Alfred Gallery downloads](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchrisgrieser%2F.config%2Frefs%2Fheads%2Fmain%2FAlfred.alfredpreferences%2Falfred-workflow-download-count.yaml&query=alfred-homebrew&style=plastic&logo=alfred&label=Gallery%20Downloads&color=%235C1F87) 4 | ![Latest release](https://img.shields.io/github/v/release/chrisgrieser/alfred-homebrew?label=Latest%20Release&style=plastic) 5 | 6 | Search, install, or uninstall Homebrew packages conveniently via Alfred. 7 | 8 | showcase brew install 9 | 10 | showcase brew uninstall 11 | 12 | ## Requirements 13 | - [Homebrew](https://brew.sh/) 14 | - Optional: 15 | [AlfredExtraPane](https://github.com/mr-pennyworth/alfred-extra-pane) for 16 | previews of package homepages. 17 | 18 | ## Usage 19 | - Search for a Homebrew package via the `bi` keyword. (+ The download counts 20 | refer to the number of downloads per 90 days.) 21 | + : Install the package (`brew install`) in the Terminal. (Uses 22 | the terminal app you have configured [in your Alfred 23 | settings](https://www.alfredapp.com/help/features/terminal/).) 24 | + ⌘⏎: Open the package's homepage (`brew home`). 25 | + ⌥⏎: Copy the package's homepage to the clipboard. 26 | + ⇧⏎: Show package information (`brew info`) in Text View. 27 | + ⌘Y Quick Look a screenshot of the app, if available. 28 | - Uninstall a package via the `bu` keyword (`brew uninstall`). 29 | + ⇧⏎: Show package information (`brew info`) in Text View. 30 | + ⌃⏎: Reinstall the package (`brew reinstall`). 31 | 32 | > [!NOTE] 33 | > The workflow uses the local homebrew packages cache. This means that the list 34 | > of packages is automatically updated when you run `brew update` in your 35 | > terminal. 36 | 37 | ## Mac App Store apps 38 | You might also be interested in [this workflow to search the Mac App 39 | Store](https://alfred.app/workflows/chrisgrieser/mac-app-store-search/). 40 | 41 | > [!TIP] 42 | > You can set both of them to the same keyword to simultaneously search homebrew 43 | > and the Mac App Store. 44 | 45 | ## Installation 46 | [➡️ Download the latest release.](./releases/latest) 47 | 48 | The workflow auto-updates via Alfred's workflow-update mechanism. 49 | 50 | ## Credits 51 | In my day job, I am a sociologist studying the social mechanisms underlying the 52 | digital economy. For my PhD project, I investigate the governance of the app 53 | economy and how software ecosystems manage the tension between innovation and 54 | compatibility. If you are interested in this subject, feel free to get in touch. 55 | 56 | - [Website](https://chris-grieser.de/) 57 | - [Mastodon](https://pkm.social/@pseudometa) 58 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 59 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 60 | 61 | Buy Me a Coffee at ko-fi.com 64 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisgrieser/alfred-homebrew/8d765220428f4767e8c90c7f29f5f352cf9ec7bc/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | de.chris-grieser.homebrew 7 | category 8 | ⭐️ 9 | connections 10 | 11 | 0BAF56E8-B65A-478A-B848-F09FFE73A12B 12 | 13 | 14 | destinationuid 15 | 3F95A285-6177-405E-B2B0-518A7C0276F2 16 | modifiers 17 | 131072 18 | modifiersubtext 19 | ⇧: brew info 20 | vitoclose 21 | 22 | 23 | 24 | destinationuid 25 | 72B78E7D-69B1-4E83-AB9B-26507DE2C33E 26 | modifiers 27 | 0 28 | modifiersubtext 29 | 30 | vitoclose 31 | 32 | 33 | 34 | destinationuid 35 | 6C6C9760-3FA9-453E-AD2E-E9074AB3E64A 36 | modifiers 37 | 262144 38 | modifiersubtext 39 | ⌃: reinstall 40 | vitoclose 41 | 42 | 43 | 44 | 0D984FC5-A902-42FB-80F8-A9D0310C86BB 45 | 46 | 47 | destinationuid 48 | F01BF271-72C7-44BA-9382-E133D8529E6D 49 | modifiers 50 | 0 51 | modifiersubtext 52 | 53 | vitoclose 54 | 55 | 56 | 57 | 3C20F055-7280-4028-891B-A35ACB3DE213 58 | 59 | 60 | destinationuid 61 | EDA21286-4E28-4304-A9DB-0BA74AB2C370 62 | modifiers 63 | 0 64 | modifiersubtext 65 | 66 | sourceoutputuid 67 | 29197F0C-557D-41F6-848C-DB541D29D2A2 68 | vitoclose 69 | 70 | 71 | 72 | destinationuid 73 | EDA21286-4E28-4304-A9DB-0BA74AB2C370 74 | modifiers 75 | 0 76 | modifiersubtext 77 | 78 | vitoclose 79 | 80 | 81 | 82 | destinationuid 83 | 4818B3ED-E0D3-4F66-88AF-FD15AF8A9AF4 84 | modifiers 85 | 0 86 | modifiersubtext 87 | 88 | vitoclose 89 | 90 | 91 | 92 | 3F95A285-6177-405E-B2B0-518A7C0276F2 93 | 94 | 95 | destinationuid 96 | 5C596F48-0F0D-4709-8097-75C0875E686B 97 | modifiers 98 | 0 99 | modifiersubtext 100 | 101 | vitoclose 102 | 103 | 104 | 105 | 4818B3ED-E0D3-4F66-88AF-FD15AF8A9AF4 106 | 107 | 108 | destinationuid 109 | 0D984FC5-A902-42FB-80F8-A9D0310C86BB 110 | modifiers 111 | 0 112 | modifiersubtext 113 | 114 | vitoclose 115 | 116 | 117 | 118 | 6C6C9760-3FA9-453E-AD2E-E9074AB3E64A 119 | 120 | 121 | destinationuid 122 | 0F3F996A-3FC8-4FBD-990C-9D54C43DD642 123 | modifiers 124 | 0 125 | modifiersubtext 126 | 127 | vitoclose 128 | 129 | 130 | 131 | 709C3FA7-A332-4B07-A934-6BC8C7E9DB32 132 | 133 | 134 | destinationuid 135 | 0F3F996A-3FC8-4FBD-990C-9D54C43DD642 136 | modifiers 137 | 0 138 | modifiersubtext 139 | 140 | vitoclose 141 | 142 | 143 | 144 | 72B78E7D-69B1-4E83-AB9B-26507DE2C33E 145 | 146 | 147 | destinationuid 148 | 802369B2-358A-46C9-9516-940B8E3E7940 149 | modifiers 150 | 0 151 | modifiersubtext 152 | 153 | sourceoutputuid 154 | 754F2E4D-3974-4DCD-9218-D82A0014576A 155 | vitoclose 156 | 157 | 158 | 159 | destinationuid 160 | 709C3FA7-A332-4B07-A934-6BC8C7E9DB32 161 | modifiers 162 | 0 163 | modifiersubtext 164 | 165 | vitoclose 166 | 167 | 168 | 169 | 72D18DBC-72FF-40AA-B367-D4C148AEC3A6 170 | 171 | 172 | destinationuid 173 | 3C20F055-7280-4028-891B-A35ACB3DE213 174 | modifiers 175 | 0 176 | modifiersubtext 177 | 178 | vitoclose 179 | 180 | 181 | 182 | destinationuid 183 | 0D984FC5-A902-42FB-80F8-A9D0310C86BB 184 | modifiers 185 | 524288 186 | modifiersubtext 187 | ⌥: Copy URL 188 | vitoclose 189 | 190 | 191 | 192 | destinationuid 193 | 2468D124-88C6-438D-B2C6-627ADEEEC31D 194 | modifiers 195 | 1048576 196 | modifiersubtext 197 | ⌘: Open package's homepage 198 | vitoclose 199 | 200 | 201 | 202 | destinationuid 203 | 3F95A285-6177-405E-B2B0-518A7C0276F2 204 | modifiers 205 | 131072 206 | modifiersubtext 207 | ⇧: brew info 208 | vitoclose 209 | 210 | 211 | 212 | EDA21286-4E28-4304-A9DB-0BA74AB2C370 213 | 214 | 215 | createdby 216 | Chris Grieser 217 | description 218 | Search, install, or uninstall homebrew packages conveniently 219 | disabled 220 | 221 | name 222 | Homebrew 223 | objects 224 | 225 | 226 | config 227 | 228 | alfredfiltersresults 229 | 230 | alfredfiltersresultsmatchmode 231 | 2 232 | argumenttreatemptyqueryasnil 233 | 234 | argumenttrimmode 235 | 1 236 | argumenttype 237 | 1 238 | escaping 239 | 0 240 | keyword 241 | {var:install_keyword} 242 | queuedelaycustom 243 | 3 244 | queuedelayimmediatelyinitially 245 | 246 | queuedelaymode 247 | 1 248 | queuemode 249 | 2 250 | runningsubtext 251 | loading... 252 | script 253 | 254 | scriptargtype 255 | 1 256 | scriptfile 257 | scripts/brew-install.js 258 | subtext 259 | 260 | title 261 | > brew install '{query}' 262 | type 263 | 8 264 | withspace 265 | 266 | 267 | type 268 | alfred.workflow.input.scriptfilter 269 | uid 270 | 72D18DBC-72FF-40AA-B367-D4C148AEC3A6 271 | version 272 | 3 273 | 274 | 275 | config 276 | 277 | escaping 278 | 0 279 | script 280 | brew install {query} 281 | 282 | type 283 | alfred.workflow.action.terminalcommand 284 | uid 285 | EDA21286-4E28-4304-A9DB-0BA74AB2C370 286 | version 287 | 1 288 | 289 | 290 | config 291 | 292 | conditions 293 | 294 | 295 | inputstring 296 | {var:copy_brewfile_line_on_install} 297 | matchcasesensitive 298 | 299 | matchmode 300 | 0 301 | matchstring 302 | 0 303 | outputlabel 304 | false 305 | uid 306 | 29197F0C-557D-41F6-848C-DB541D29D2A2 307 | 308 | 309 | elselabel 310 | true 311 | hideelse 312 | 313 | 314 | type 315 | alfred.workflow.utility.conditional 316 | uid 317 | 3C20F055-7280-4028-891B-A35ACB3DE213 318 | version 319 | 1 320 | 321 | 322 | config 323 | 324 | argument 325 | {var:brewfileLine} 326 | passthroughargument 327 | 328 | variables 329 | 330 | 331 | type 332 | alfred.workflow.utility.argument 333 | uid 334 | 4818B3ED-E0D3-4F66-88AF-FD15AF8A9AF4 335 | version 336 | 1 337 | 338 | 339 | config 340 | 341 | autopaste 342 | 343 | clipboardtext 344 | {query} 345 | ignoredynamicplaceholders 346 | 347 | transient 348 | 349 | 350 | type 351 | alfred.workflow.output.clipboard 352 | uid 353 | 0D984FC5-A902-42FB-80F8-A9D0310C86BB 354 | version 355 | 3 356 | 357 | 358 | config 359 | 360 | lastpathcomponent 361 | 362 | onlyshowifquerypopulated 363 | 364 | removeextension 365 | 366 | text 367 | {query} 368 | title 369 | 📋 Copied 370 | 371 | type 372 | alfred.workflow.output.notification 373 | uid 374 | F01BF271-72C7-44BA-9382-E133D8529E6D 375 | version 376 | 1 377 | 378 | 379 | config 380 | 381 | browser 382 | 383 | skipqueryencode 384 | 385 | skipvarencode 386 | 387 | spaces 388 | 389 | url 390 | 391 | 392 | type 393 | alfred.workflow.action.openurl 394 | uid 395 | 2468D124-88C6-438D-B2C6-627ADEEEC31D 396 | version 397 | 1 398 | 399 | 400 | config 401 | 402 | concurrently 403 | 404 | escaping 405 | 0 406 | script 407 | 408 | scriptargtype 409 | 1 410 | scriptfile 411 | ./scripts/brew-info.sh 412 | type 413 | 8 414 | 415 | type 416 | alfred.workflow.action.script 417 | uid 418 | 3F95A285-6177-405E-B2B0-518A7C0276F2 419 | version 420 | 2 421 | 422 | 423 | config 424 | 425 | behaviour 426 | 0 427 | fontmode 428 | 0 429 | fontsizing 430 | 4 431 | footertext 432 | 433 | inputfile 434 | 435 | inputtype 436 | 0 437 | loadingtext 438 | 439 | outputmode 440 | 0 441 | scriptinput 442 | 0 443 | spellchecking 444 | 0 445 | stackview 446 | 447 | 448 | type 449 | alfred.workflow.userinterface.text 450 | uid 451 | 5C596F48-0F0D-4709-8097-75C0875E686B 452 | version 453 | 1 454 | 455 | 456 | config 457 | 458 | openwith 459 | /System/Volumes/Data/Applications/AppCleaner.app 460 | sourcefile 461 | 462 | 463 | type 464 | alfred.workflow.action.openfile 465 | uid 466 | 802369B2-358A-46C9-9516-940B8E3E7940 467 | version 468 | 3 469 | 470 | 471 | config 472 | 473 | alfredfiltersresults 474 | 475 | alfredfiltersresultsmatchmode 476 | 2 477 | argumenttreatemptyqueryasnil 478 | 479 | argumenttrimmode 480 | 0 481 | argumenttype 482 | 1 483 | escaping 484 | 0 485 | keyword 486 | {var:uninstall_keyword} 487 | queuedelaycustom 488 | 3 489 | queuedelayimmediatelyinitially 490 | 491 | queuedelaymode 492 | 0 493 | queuemode 494 | 1 495 | runningsubtext 496 | loading... 497 | script 498 | 499 | scriptargtype 500 | 1 501 | scriptfile 502 | ./scripts/brew-uninstall.js 503 | subtext 504 | 505 | title 506 | > brew uninstall '{query}' 507 | type 508 | 8 509 | withspace 510 | 511 | 512 | type 513 | alfred.workflow.input.scriptfilter 514 | uid 515 | 0BAF56E8-B65A-478A-B848-F09FFE73A12B 516 | version 517 | 3 518 | 519 | 520 | config 521 | 522 | conditions 523 | 524 | 525 | inputstring 526 | 527 | matchcasesensitive 528 | 529 | matchmode 530 | 4 531 | matchstring 532 | \.app$ 533 | outputlabel 534 | AppCleaner 535 | uid 536 | 754F2E4D-3974-4DCD-9218-D82A0014576A 537 | 538 | 539 | elselabel 540 | homebrew 541 | hideelse 542 | 543 | 544 | type 545 | alfred.workflow.utility.conditional 546 | uid 547 | 72B78E7D-69B1-4E83-AB9B-26507DE2C33E 548 | version 549 | 1 550 | 551 | 552 | config 553 | 554 | escaping 555 | 0 556 | script 557 | {query} 558 | 559 | type 560 | alfred.workflow.action.terminalcommand 561 | uid 562 | 0F3F996A-3FC8-4FBD-990C-9D54C43DD642 563 | version 564 | 1 565 | 566 | 567 | config 568 | 569 | argument 570 | brew uninstall {query} 571 | passthroughargument 572 | 573 | variables 574 | 575 | 576 | type 577 | alfred.workflow.utility.argument 578 | uid 579 | 709C3FA7-A332-4B07-A934-6BC8C7E9DB32 580 | version 581 | 1 582 | 583 | 584 | config 585 | 586 | argument 587 | brew reinstall {query} 588 | passthroughargument 589 | 590 | variables 591 | 592 | 593 | type 594 | alfred.workflow.utility.argument 595 | uid 596 | 6C6C9760-3FA9-453E-AD2E-E9074AB3E64A 597 | version 598 | 1 599 | 600 | 601 | readme 602 | ## Requirements 603 | - [Homebrew](https://brew.sh/) 604 | - Optional: 605 | [AlfredExtraPane](https://github.com/mr-pennyworth/alfred-extra-pane) for 606 | previews of package homepages. 607 | 608 | ## Usage 609 | - Search for a Homebrew package via the `bi` keyword. (+ The download counts 610 | refer to the number of downloads per 90 days.) 611 | + <kbd>⏎</kbd>: Install the package (`brew install`) in the Terminal. (Uses 612 | the terminal app you have configured [in your Alfred 613 | settings](https://www.alfredapp.com/help/features/terminal/).) 614 | + <kbd>⌘⏎</kbd>: Open the package's homepage (`brew home`). 615 | + <kbd>⌥⏎</kbd>: Copy the package's homepage to the clipboard. 616 | + <kbd>⇧⏎</kbd>: Show package information (`brew info`) in Text View. 617 | + <kbd>⌘Y</kbd> Quick Look a screenshot of the app, if available. 618 | - Uninstall a package via the `bu` keyword (`brew uninstall`). 619 | + <kbd>⇧⏎</kbd>: Show package information (`brew info`) in Text View. 620 | + <kbd>⌃⏎</kbd>: Reinstall the package (`brew reinstall`). 621 | 622 | ## Mac App Store apps 623 | You might also be interested in [this workflow to search the Mac App 624 | Store](https://alfred.app/workflows/chrisgrieser/mac-app-store-search/). 625 | 626 | 💡 You can set both of them to the same keyword to simultaneously search homebrew 627 | and the Mac App Store. 628 | 629 | --- 630 | 631 | #### Created by [Chris Grieser](https://chris-grieser.de/) | ⭐️ [Star on GitHub](https://github.com/chrisgrieser/alfred-homebrew) 632 | uidata 633 | 634 | 0BAF56E8-B65A-478A-B848-F09FFE73A12B 635 | 636 | colorindex 637 | 1 638 | note 639 | brew uninstall 640 | xpos 641 | 30 642 | ypos 643 | 515 644 | 645 | 0D984FC5-A902-42FB-80F8-A9D0310C86BB 646 | 647 | colorindex 648 | 3 649 | xpos 650 | 475 651 | ypos 652 | 130 653 | 654 | 0F3F996A-3FC8-4FBD-990C-9D54C43DD642 655 | 656 | colorindex 657 | 1 658 | note 659 | brew uninstall 660 | xpos 661 | 470 662 | ypos 663 | 620 664 | 665 | 2468D124-88C6-438D-B2C6-627ADEEEC31D 666 | 667 | colorindex 668 | 3 669 | xpos 670 | 470 671 | ypos 672 | 245 673 | 674 | 3C20F055-7280-4028-891B-A35ACB3DE213 675 | 676 | colorindex 677 | 3 678 | note 679 | copy Brewfile line 680 | xpos 681 | 318 682 | ypos 683 | 35 684 | 685 | 3F95A285-6177-405E-B2B0-518A7C0276F2 686 | 687 | colorindex 688 | 5 689 | note 690 | brew info 691 | xpos 692 | 470 693 | ypos 694 | 365 695 | 696 | 4818B3ED-E0D3-4F66-88AF-FD15AF8A9AF4 697 | 698 | colorindex 699 | 3 700 | xpos 701 | 395 702 | ypos 703 | 115 704 | 705 | 5C596F48-0F0D-4709-8097-75C0875E686B 706 | 707 | colorindex 708 | 5 709 | xpos 710 | 615 711 | ypos 712 | 365 713 | 714 | 6C6C9760-3FA9-453E-AD2E-E9074AB3E64A 715 | 716 | colorindex 717 | 1 718 | note 719 | add `brew reinstall` 720 | xpos 721 | 340 722 | ypos 723 | 690 724 | 725 | 709C3FA7-A332-4B07-A934-6BC8C7E9DB32 726 | 727 | colorindex 728 | 1 729 | note 730 | add `brew uninstall` 731 | xpos 732 | 340 733 | ypos 734 | 620 735 | 736 | 72B78E7D-69B1-4E83-AB9B-26507DE2C33E 737 | 738 | colorindex 739 | 1 740 | xpos 741 | 245 742 | ypos 743 | 535 744 | 745 | 72D18DBC-72FF-40AA-B367-D4C148AEC3A6 746 | 747 | colorindex 748 | 3 749 | note 750 | install 751 | xpos 752 | 30 753 | ypos 754 | 15 755 | 756 | 802369B2-358A-46C9-9516-940B8E3E7940 757 | 758 | colorindex 759 | 1 760 | xpos 761 | 470 762 | ypos 763 | 500 764 | 765 | EDA21286-4E28-4304-A9DB-0BA74AB2C370 766 | 767 | colorindex 768 | 3 769 | xpos 770 | 475 771 | ypos 772 | 15 773 | 774 | F01BF271-72C7-44BA-9382-E133D8529E6D 775 | 776 | colorindex 777 | 3 778 | xpos 779 | 620 780 | ypos 781 | 130 782 | 783 | 784 | userconfigurationconfig 785 | 786 | 787 | config 788 | 789 | default 790 | bi 791 | placeholder 792 | bu 793 | required 794 | 795 | trim 796 | 797 | 798 | description 799 | installation 800 | label 801 | Keywords 802 | type 803 | textfield 804 | variable 805 | install_keyword 806 | 807 | 808 | config 809 | 810 | default 811 | bu 812 | placeholder 813 | bu 814 | required 815 | 816 | trim 817 | 818 | 819 | description 820 | uninstallation 821 | label 822 | 823 | type 824 | textfield 825 | variable 826 | uninstall_keyword 827 | 828 | 829 | config 830 | 831 | default 832 | 833 | required 834 | 835 | text 836 | copy line for Brewfile 837 | 838 | description 839 | When installing a package, also copy the line that needs to be added to a Brewfile. 840 | label 841 | install 842 | type 843 | checkbox 844 | variable 845 | copy_brewfile_line_on_install 846 | 847 | 848 | config 849 | 850 | default 851 | 852 | required 853 | 854 | text 855 | include Mac App Store apps 856 | 857 | description 858 | When using the uninstallation command, also list apps from the Mac App Store. They will be opened with `AppCleaner.app` for clean uninstallation. 859 | label 860 | uninstall 861 | type 862 | checkbox 863 | variable 864 | list_mac_app_store 865 | 866 | 867 | config 868 | 869 | default 870 | 871 | required 872 | 873 | text 874 | cleanup 875 | 876 | description 877 | When using the uninstallation command on a cask, use `--zap`. This results in homebrew also cleaning up data and cache files from the app. 878 | label 879 | 880 | type 881 | checkbox 882 | variable 883 | use_zap 884 | 885 | 886 | version 887 | 2.1.0 888 | webaddress 889 | https://chris-grieser.de/ 890 | 891 | 892 | -------------------------------------------------------------------------------- /scripts/brew-info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | type=$(echo "$*" | cut -d' ' -f1) 3 | name=$(echo "$*" | cut -d' ' -f2) 4 | 5 | brew info "$type" "$name" 6 | -------------------------------------------------------------------------------- /scripts/brew-install.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | const alfredMatcher = (/** @type {string} */ str) => str.replaceAll("-", " ") + " " + str + " "; 8 | 9 | const fileExists = (/** @type {string} */ filePath) => Application("Finder").exists(Path(filePath)); 10 | 11 | /** @param {string} path */ 12 | function readFile(path) { 13 | const data = $.NSFileManager.defaultManager.contentsAtPath(path); 14 | const str = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding); 15 | return ObjC.unwrap(str); 16 | } 17 | 18 | /** @param {string} filepath @param {string} text */ 19 | function writeToFile(filepath, text) { 20 | const str = $.NSString.alloc.initWithUTF8String(text); 21 | str.writeToFileAtomicallyEncodingError(filepath, true, $.NSUTF8StringEncoding, null); 22 | } 23 | 24 | function ensureCacheFolderExists() { 25 | const finder = Application("Finder"); 26 | const cacheDir = $.getenv("alfred_workflow_cache"); 27 | if (!finder.exists(Path(cacheDir))) { 28 | const cacheDirBasename = $.getenv("alfred_workflow_bundleid"); 29 | const cacheDirParent = cacheDir.slice(0, -cacheDirBasename.length); 30 | finder.make({ 31 | new: "folder", 32 | at: Path(cacheDirParent), 33 | withProperties: { name: cacheDirBasename }, 34 | }); 35 | } 36 | } 37 | 38 | /** @param {string} path */ 39 | function cacheIsOutdated(path) { 40 | ensureCacheFolderExists(); 41 | const cacheObj = Application("System Events").aliases[path]; 42 | if (!cacheObj.exists()) return true; 43 | const cacheAgeDays = (Date.now() - +cacheObj.creationDate()) / 1000 / 60 / 60 / 24; 44 | const cacheAgeThresholdDays = 7; 45 | return cacheAgeDays > cacheAgeThresholdDays; 46 | } 47 | 48 | /** @param {string} url @return {string} */ 49 | function httpRequest(url) { 50 | const queryURL = $.NSURL.URLWithString(url); 51 | const data = $.NSData.dataWithContentsOfURL(queryURL); 52 | const requestStr = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js; 53 | return requestStr; 54 | } 55 | 56 | // WARN do not use the `installed` field, since the data there is empty, despite 57 | // the homebrew docs suggested otherwise. 58 | /** @typedef {object} Formula 59 | * @property {string} name 60 | * @property {string} caveats 61 | * @property {string} desc 62 | * @property {string} homepage 63 | * @property {boolean} deprecated 64 | * @property {object[]} installed 65 | * @property {string[]} dependencies 66 | */ 67 | 68 | // WARN do not use the `installed` field, since the data there is empty, despite 69 | // the homebrew docs suggested otherwise. 70 | /** @typedef {object} Cask 71 | * @property {string} token 72 | * @property {string} desc 73 | * @property {string} homepage 74 | * @property {boolean} deprecated 75 | * @property {object} installed 76 | */ 77 | 78 | //────────────────────────────────────────────────────────────────────────────── 79 | 80 | /** @type {AlfredRun} */ 81 | // biome-ignore lint/correctness/noUnusedVariables: Alfred run 82 | function run() { 83 | const caskIcon = "🛢️"; 84 | const formulaIcon = "🍺"; 85 | const installedIcon = "✅"; 86 | const deprecatedIcon = "⚠️"; 87 | 88 | // 1. MAIN DATA (already cached by homebrew) 89 | // DOCS https://formulae.brew.sh/docs/api/ & https://docs.brew.sh/Querying-Brew 90 | // these files contain the API response of casks and formulas as payload; they 91 | // are updated on each `brew update`. Since they are effectively caches, 92 | // there is no need create caches of our own. 93 | const caskJson = app.pathTo("home folder") + "/Library/Caches/Homebrew/api/cask.jws.json"; 94 | const formulaJson = app.pathTo("home folder") + "/Library/Caches/Homebrew/api/formula.jws.json"; 95 | if (!fileExists(formulaJson) || !fileExists(caskJson)) app.doShellScript("brew update"); 96 | 97 | // SIC data must be parsed twice, since that is how the cache is saved by homebrew 98 | const casksData = JSON.parse(JSON.parse(readFile(caskJson)).payload); 99 | const formulaData = JSON.parse(JSON.parse(readFile(formulaJson)).payload); 100 | 101 | // 2. LOCAL INSTALLATION DATA (determined live every run) 102 | // PERF `ls` quicker than `brew list` 103 | // (and the json files miss actual installation info) 104 | const installedFormulas = app.doShellScript('ls -1 "$(brew --prefix)/Cellar"').split("\r"); 105 | const installedCasks = app.doShellScript('ls -1 "$(brew --prefix)/Caskroom"').split("\r"); 106 | 107 | // 3. DOWNLOAD COUNTS (cached by this workflow) 108 | // DOCS https://formulae.brew.sh/analytics/ 109 | // INFO separate from Alfred's caching mechanism, since the installed 110 | // packages should be determined more frequently 111 | const cask90d = $.getenv("alfred_workflow_cache") + "/caskDownloads90d.json"; 112 | const formula90d = $.getenv("alfred_workflow_cache") + "/formulaDownloads90d.json"; 113 | let caskDlRaw; 114 | let formulaDlRaw; 115 | if (cacheIsOutdated(cask90d)) { 116 | console.log("Updating download count cache."); 117 | caskDlRaw = httpRequest( 118 | "https://formulae.brew.sh/api/analytics/cask-install/homebrew-cask/90d.json", 119 | ); 120 | formulaDlRaw = httpRequest( 121 | "https://formulae.brew.sh/api/analytics/install-on-request/homebrew-core/90d.json", 122 | ); 123 | writeToFile(cask90d, caskDlRaw); 124 | writeToFile(formula90d, formulaDlRaw); 125 | } 126 | const caskDownloads = JSON.parse(caskDlRaw || readFile(cask90d)).formulae; 127 | const formulaDownloads = JSON.parse(formulaDlRaw || readFile(formula90d)).formulae; // SIC not `.casks` 128 | 129 | // 4. CREATE ALFRED ITEMS (will be cached for an hour by Alfred) 130 | /** @type{AlfredItem&{downloads:number}[]} */ 131 | const casks = casksData.map((/** @type {Cask} */ cask) => { 132 | const name = cask.token; 133 | let icons = ""; 134 | if (installedCasks.includes(name)) icons += " " + installedIcon; 135 | if (cask.deprecated) icons += ` ${deprecatedIcon}[deprecated]`; 136 | 137 | const downloads = caskDownloads[name] ? `${caskDownloads[name][0].count}↓` : ""; 138 | const desc = cask.desc || ""; 139 | 140 | return { 141 | title: name + icons, 142 | match: alfredMatcher(name) + desc, 143 | subtitle: [caskIcon, downloads, " ", desc].join(" "), 144 | arg: `--cask ${name}`, 145 | variables: { brewfileLine: `cask "${name}"` }, 146 | quicklookurl: cask.homepage, 147 | downloads: Number.parseInt(downloads.replace(/,/g, "")), // only for sorting 148 | mods: { 149 | // PERF quicker to pass here than to call `brew home` on brew-id 150 | cmd: { 151 | subtitle: "⌘: Open " + cask.homepage, 152 | arg: cask.homepage, 153 | }, 154 | alt: { 155 | subtitle: "⌥: Copy " + cask.homepage, 156 | arg: cask.homepage, 157 | }, 158 | }, 159 | uid: name, // remember selections 160 | }; 161 | }); 162 | 163 | /** @type{AlfredItem&{downloads:number}[]} */ 164 | const formulas = formulaData.map((/** @type {Formula} */ formula) => { 165 | const name = formula.name; 166 | let icons = ""; 167 | if (installedFormulas.includes(name)) icons += " " + installedIcon; 168 | if (formula.deprecated) icons += ` ${deprecatedIcon}deprecated`; 169 | 170 | const downloads = formulaDownloads[name] ? `${formulaDownloads[name][0].count}↓` : ""; 171 | const desc = formula.desc || ""; 172 | 173 | return { 174 | title: name + icons, 175 | match: alfredMatcher(name) + desc, 176 | subtitle: [formulaIcon, downloads, " ", desc].join(" "), 177 | arg: `--formula ${name}`, 178 | variables: { brewfileLine: `brew "${name}"` }, 179 | quicklookurl: formula.homepage, 180 | downloads: Number.parseInt(downloads.replaceAll(",", "")), // only for sorting 181 | mods: { 182 | // PERF quicker to pass here than to call `brew home` on brew-id 183 | cmd: { 184 | subtitle: "⌘: Open " + formula.homepage, 185 | arg: formula.homepage, 186 | }, 187 | alt: { 188 | subtitle: "⌥: Copy " + formula.homepage, 189 | arg: formula.homepage, 190 | }, 191 | }, 192 | uid: name, // remember selections 193 | }; 194 | }); 195 | 196 | // 5. MERGE & SORT BOTH LISTS 197 | // a. move shorter package names top, since short names like `sd` are otherwise ranked 198 | // further down, making them often hard to find 199 | // b. sort by download count as secondary criteria 200 | const allPackages = [...casks, ...formulas].sort((/** @type{any} */ a, /** @type{any} */ b) => { 201 | const titleLengthDiff = a.title.length - b.title.length; 202 | if (titleLengthDiff !== 0) return titleLengthDiff; 203 | const downloadCountDiff = (b.downloads || 0) - (a.downloads || 0); 204 | return downloadCountDiff; 205 | }); 206 | 207 | return JSON.stringify({ 208 | items: allPackages, 209 | cache: { seconds: 3600, loosereload: true }, // update regularly for correct identification of installed packages 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /scripts/brew-uninstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | const alfredMatcher = (/** @type {string} */ str) => str.replaceAll("-", " ") + " " + str + " "; 8 | 9 | //────────────────────────────────────────────────────────────────────────────── 10 | 11 | // biome-ignore lint/correctness/noUnusedVariables: Alfred run 12 | function run() { 13 | const includeMacAppStoreSetting = $.getenv("list_mac_app_store") === "1"; 14 | const useZap = $.getenv("use_zap") === "1"; 15 | 16 | /** @type{AlfredItem[]} */ 17 | const casks = app 18 | .doShellScript('ls -1 "$(brew --prefix)/Caskroom"') // PERF `ls` quicker than `brew list` 19 | .split("\r") 20 | .map((name) => { 21 | const zap = useZap ? "--zap" : ""; 22 | return { 23 | title: name, 24 | match: alfredMatcher(name), 25 | subtitle: "cask", 26 | arg: `--cask ${zap} ${name}`, 27 | mods: { 28 | shift: { arg: `--cask ${name}` }, 29 | }, 30 | }; 31 | }); 32 | 33 | /** @type{AlfredItem[]} */ 34 | const formulas = app 35 | // slower than `ls -1 "$(brew --prefix)/Caskroom"'`, but 36 | // --installed-on-request is relevant 37 | .doShellScript("brew leaves --installed-on-request") 38 | .split("\r") 39 | .map((name) => { 40 | return { 41 | title: name, 42 | match: alfredMatcher(name), 43 | subtitle: "formula", 44 | arg: `--formula ${name}`, 45 | }; 46 | }); 47 | 48 | const allApps = [...formulas, ...casks]; 49 | 50 | if (includeMacAppStoreSetting) { 51 | const appStoreApps = app 52 | .doShellScript("mdfind kMDItemAppStoreHasReceipt=1") // `mdfind` avoids dependency on `mas` 53 | .split("\r") 54 | .map((appPath) => { 55 | const appName = appPath.split("/")[2]; 56 | const nameNoExt = appName.slice(0, -4); 57 | 58 | return { 59 | title: nameNoExt, 60 | match: alfredMatcher(nameNoExt), 61 | subtitle: "Mac App Store", 62 | arg: appPath, 63 | mods: { 64 | ctrl: { 65 | valid: false, 66 | subtitle: "⛔ reinstall is only supported for homebrew packages", 67 | }, 68 | shift: { 69 | valid: false, 70 | subtitle: "⛔ info is only supported for homebrew packages", 71 | }, 72 | }, 73 | }; 74 | }); 75 | 76 | allApps.push(...appStoreApps); 77 | } 78 | 79 | return JSON.stringify({ 80 | items: allApps, 81 | cache: { 82 | seconds: 120, // quick since leftover apps after uninstallation would be confusing 83 | loosereload: true, 84 | }, 85 | }); 86 | } 87 | --------------------------------------------------------------------------------