├── .build-and-release.sh ├── .editorconfig ├── .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 ├── Justfile ├── LICENSE ├── README.md ├── github-api.d.ts ├── icon.png ├── info.plist └── scripts ├── clone-repo.sh ├── github-notifications.js ├── my-github-issues.js ├── my-github-prs.js ├── my-github-repos.js ├── public-github-repo-search.js └── resolve-notification.sh /.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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 100 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 3 10 | tab_width = 3 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,yaml}] 14 | indent_style = space 15 | indent_size = 2 16 | tab_width = 2 17 | 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | tab_width = 4 22 | 23 | [*.md] 24 | indent_size = 4 25 | tab_width = 4 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /.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.5. (Older versions are not supported anymore.) 14 | required: true 15 | - label: 16 | If I am using a `GITHUB_TOKEN`, I have checked that the token is still valid and has the 17 | required access rights. 18 | required: true 19 | - type: textarea 20 | id: bug-description 21 | attributes: 22 | label: Bug Description 23 | description: A clear and concise description of the bug. 24 | validations: { required: true } 25 | - type: textarea 26 | id: screenshot 27 | attributes: 28 | label: Relevant Screenshot 29 | description: 30 | If applicable, add screenshots or a screen recording to help explain your problem. 31 | - type: textarea 32 | id: reproduction-steps 33 | attributes: 34 | label: To Reproduce 35 | description: Steps to reproduce the problem 36 | placeholder: | 37 | For example: 38 | 1. Go to '...' 39 | 2. Click on '...' 40 | 3. Scroll down to '...' 41 | - type: textarea 42 | id: debugging-log 43 | attributes: 44 | label: Debugging Log 45 | description: 46 | "You can get a debugging log by opening the workflow in Alfred preferences and pressing `⌘ + 47 | D`. A small window will open up which will log everything happening during the execution of 48 | the Workflow. Use the malfunctioning part of the workflow once more, copy the content of the 49 | log window, and paste it here. If the debugging log is long, please attach it as file 50 | instead of pasting everything in here. NOTE: Your `GITHUB_TOKEN` will be included in the 51 | log, please remove it before posting here." 52 | render: Text 53 | validations: { required: true } 54 | - type: textarea 55 | id: workflow-configuration 56 | attributes: 57 | label: Workflow Configuration 58 | description: 59 | "Please add a screenshot of your [workflow 60 | configuration](https://www.alfredapp.com/help/workflows/user-configuration/)." 61 | validations: { required: true } 62 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | 2 | # Alfred GitFred 3 | 4 | ![GitHub downloads](https://img.shields.io/github/downloads/chrisgrieser/gitfred/total?label=GitHub%20Downloads&style=plastic&logo=github) 5 | ![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=gitfred&style=plastic&logo=alfred&label=Gallery%20Downloads&color=%235C1F87) 6 | ![Version number](https://img.shields.io/github/v/release/chrisgrieser/gitfred?label=Latest%20Release&style=plastic) 7 | 8 | Helpful GitHub assistant for Alfred. 9 | 10 | Showcase owned repo search 11 | 12 | Showcase public repo search 13 | 14 | Showcase issue search 15 | 16 | ## Usage 17 | - Search all public GitHub repositories via the keyword `gh`. 18 | + : Open the repo's GitHub page. 19 | + ⌘⏎: Open the repo's homepage. If it has none, opens the 20 | releases page instead. 21 | + ⌥⏎: Copy the repo URL. 22 | + ⌃⏎: Clone the repo to a local folder and open in the Terminal. 23 | - Access your repositories via `gmy`. (Limited to the 100 repos last updated.) 24 | + : If the repo is available locally on your device, open it in 25 | the terminal. Otherwise, open the repo's GitHub page. 26 | + ⌘⏎: Open the repo's GitHub page. 27 | + ⌥⏎: Copy the repo URL. 28 | + ⌃⏎: Clone the repo to a local folder and open in the Terminal. 29 | If the repo is already locally available on your device, just open the 30 | repository in the Terminal. 31 | + To include private repos, requires [GitHub 32 | Token](https://github.com/settings/tokens). 33 | - Open recent GitHub issues you are involved in with `ghi`. 34 | + : Open the issue in the browser. 35 | + ⌥⏎: Copy the issue URL. 36 | - Access PRs you have opened with the keyword `gpr`. 37 | + : Open the PR in the browser. 38 | + ⌥⏎: Copy the link to the PR. 39 | + To include private PRs, requires [GitHub 40 | Token](https://github.com/settings/tokens). 41 | - Directly open your GitHub notification via `ghn`. 42 | + Requires [GitHub Token](https://github.com/settings/tokens). 43 | + : Open the notification in the browser. In cases where no URL 44 | is available, such as deleted comments or CI activity, opens the GitHub 45 | notification inbox instead. 46 | + ⌘⏎: Mark the notification as read. 47 | + ⌥⏎: Copy the URL of the notification target. 48 | - Additionally, you can press the 49 | [hotkey](https://www.alfredapp.com/help/workflows/triggers/hotkey/) to clone 50 | a GitHub repo from the current browser tab. ([Note that Firefox is not 51 | supported.](https://www.alfredforum.com/topic/16748-how-to-do-x-in-firefox-from-alfred/)) 52 | 53 | ## Installation 54 | [➡️ Download the latest release.](https://github.com/chrisgrieser/gitfred/releases/latest) 55 | 56 | ## About the developer 57 | In my day job, I am a sociologist studying the social mechanisms underlying the 58 | digital economy. For my PhD project, I investigate the governance of the app 59 | economy and how software ecosystems manage the tension between innovation and 60 | compatibility. If you are interested in this subject, feel free to get in touch. 61 | 62 | - [Website](https://chris-grieser.de/) 63 | - [Mastodon](https://pkm.social/@pseudometa) 64 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 65 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 66 | 67 | Buy Me a Coffee at ko-fi.com 70 | -------------------------------------------------------------------------------- /github-api.d.ts: -------------------------------------------------------------------------------- 1 | // https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#list-notifications-for-the-authenticated-user 2 | declare class GithubNotif { 3 | id: string; 4 | repository: GithubRepo; 5 | subject: { 6 | title: string; 7 | url: string; 8 | // biome-ignore lint/style/useNamingConvention: not_by_me 9 | latest_comment_url: string; 10 | type: string; 11 | }; 12 | reason: string; 13 | unread: boolean; 14 | // biome-ignore lint/style/useNamingConvention: not_by_me 15 | updated_at: string; 16 | // biome-ignore lint/style/useNamingConvention: not_by_me 17 | last_read_at: string | null; 18 | url: string; 19 | // biome-ignore lint/style/useNamingConvention: not_by_me 20 | subscription_url: string; 21 | } 22 | 23 | declare class GithubIssue { 24 | title: string; 25 | user: { login: string }; 26 | state: "open" | "closed"; 27 | // biome-ignore lint/style/useNamingConvention: not_by_me 28 | state_reason?: "not_planned" | "completed"; 29 | comments: number; 30 | draft: boolean; 31 | // biome-ignore lint/style/useNamingConvention: not_by_me 32 | full_name: string; 33 | // biome-ignore lint/style/useNamingConvention: not_by_me 34 | open_issues: number; 35 | number: string; 36 | // biome-ignore lint/style/useNamingConvention: not_by_me 37 | html_url: string; 38 | // biome-ignore lint/style/useNamingConvention: not_by_me 39 | repository_url: string; 40 | // biome-ignore lint/style/useNamingConvention: not_by_me 41 | pull_request: { merged_at?: string }; 42 | // biome-ignore lint/style/useNamingConvention: not_by_me 43 | created_at: string; 44 | // biome-ignore lint/style/useNamingConvention: not_by_me 45 | updated_at: string; 46 | labels: { name: string }[]; 47 | } 48 | 49 | declare class GithubRepo { 50 | // biome-ignore lint/style/useNamingConvention: not_by_me 51 | full_name: string; 52 | name: string; 53 | description: string; 54 | owner: { 55 | login: string; 56 | }; 57 | 58 | // biome-ignore lint/style/useNamingConvention: not_by_me 59 | html_url: string; 60 | homepage: string; 61 | 62 | fork: boolean; 63 | archived: boolean; 64 | private: boolean; 65 | // biome-ignore lint/style/useNamingConvention: not_by_me 66 | is_template: boolean; 67 | 68 | // biome-ignore lint/style/useNamingConvention: not_by_me 69 | pushed_at: string; 70 | // biome-ignore lint/style/useNamingConvention: not_by_me 71 | stargazers_count: number; 72 | // biome-ignore lint/style/useNamingConvention: not_by_me 73 | open_issues: number; 74 | // biome-ignore lint/style/useNamingConvention: not_by_me 75 | forks_count: number; 76 | } 77 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisgrieser/gitfred/8a830e86574e4208aced45ebb43ad0bdbacd5545/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | de.chris-grieser.github 7 | category 8 | ⭐️ 9 | connections 10 | 11 | 0115FC58-CCEF-48EC-A08F-A405E8E30891 12 | 13 | 14 | destinationuid 15 | 129ABCE4-E5CC-4547-98BB-9A4BC845CFFA 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 08B7842B-16DA-4BFC-8F35-AE680AF1E07B 25 | 26 | 27 | destinationuid 28 | 968A454B-B9EE-46DD-8156-3630F678E31C 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | sourceoutputuid 34 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 35 | vitoclose 36 | 37 | 38 | 39 | destinationuid 40 | 122C2D04-00F3-4F54-9132-9DE395C2194A 41 | modifiers 42 | 0 43 | modifiersubtext 44 | 45 | vitoclose 46 | 47 | 48 | 49 | destinationuid 50 | 06CC5118-3CAE-427B-928D-48B78C81B6B6 51 | modifiers 52 | 0 53 | modifiersubtext 54 | 55 | vitoclose 56 | 57 | 58 | 59 | 0C28AAE6-A080-48A7-A6F0-0BB05070161D 60 | 61 | 62 | destinationuid 63 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 64 | modifiers 65 | 0 66 | modifiersubtext 67 | 68 | vitoclose 69 | 70 | 71 | 72 | 129ABCE4-E5CC-4547-98BB-9A4BC845CFFA 73 | 74 | 75 | destinationuid 76 | 9AED849C-2F6C-4741-B375-875D5613781A 77 | modifiers 78 | 0 79 | modifiersubtext 80 | 81 | vitoclose 82 | 83 | 84 | 85 | destinationuid 86 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 87 | modifiers 88 | 0 89 | modifiersubtext 90 | 91 | sourceoutputuid 92 | 629A9B2E-2F56-4A05-9F68-527094D7BCA8 93 | vitoclose 94 | 95 | 96 | 97 | 1B2789C4-ACE9-46F2-A786-B83B045166B2 98 | 99 | 100 | destinationuid 101 | 08B7842B-16DA-4BFC-8F35-AE680AF1E07B 102 | modifiers 103 | 0 104 | modifiersubtext 105 | 106 | sourceoutputuid 107 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 108 | vitoclose 109 | 110 | 111 | 112 | destinationuid 113 | F99C6054-D3E9-4D1C-960A-8E9EC9FA5CB3 114 | modifiers 115 | 0 116 | modifiersubtext 117 | 118 | sourceoutputuid 119 | D4409822-4D36-4A47-AE65-97902F001379 120 | vitoclose 121 | 122 | 123 | 124 | destinationuid 125 | 7B955092-9044-4895-864C-3532731DEDBE 126 | modifiers 127 | 0 128 | modifiersubtext 129 | 130 | vitoclose 131 | 132 | 133 | 134 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 135 | 136 | 137 | destinationuid 138 | 504CECC9-A64D-4F2A-A0E5-664B355EB529 139 | modifiers 140 | 0 141 | modifiersubtext 142 | 143 | vitoclose 144 | 145 | 146 | 147 | 378B3595-A76C-4026-AC7F-0AC288FDF625 148 | 149 | 150 | destinationuid 151 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 152 | modifiers 153 | 0 154 | modifiersubtext 155 | 156 | vitoclose 157 | 158 | 159 | 160 | destinationuid 161 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 162 | modifiers 163 | 524288 164 | modifiersubtext 165 | ⌥: Copy URL 166 | vitoclose 167 | 168 | 169 | 170 | 388AD75C-BCFB-445D-B62E-13BC0545FD5E 171 | 172 | 173 | destinationuid 174 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 175 | modifiers 176 | 1048576 177 | modifiersubtext 178 | ⌘: Open at Github 179 | vitoclose 180 | 181 | 182 | 183 | destinationuid 184 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 185 | modifiers 186 | 524288 187 | modifiersubtext 188 | ⌥: Copy URL 189 | vitoclose 190 | 191 | 192 | 193 | destinationuid 194 | B04FEC5B-79E4-407B-ACCE-EAA0C5B81AB8 195 | modifiers 196 | 0 197 | modifiersubtext 198 | 199 | vitoclose 200 | 201 | 202 | 203 | destinationuid 204 | 4DC9B99A-333B-4BC6-B2A3-13EC4B319487 205 | modifiers 206 | 262144 207 | modifiersubtext 208 | ⌃: Shallow Clone 209 | vitoclose 210 | 211 | 212 | 213 | 42255CF9-BC03-46B1-8AB6-0AD60FA92ECE 214 | 215 | 216 | destinationuid 217 | C0F5A902-C58A-4DEC-AF82-9EDD83CDFC6D 218 | modifiers 219 | 0 220 | modifiersubtext 221 | 222 | vitoclose 223 | 224 | 225 | 226 | 43F0CA76-BFE8-415E-97B3-F6B953E904FE 227 | 228 | 229 | destinationuid 230 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 231 | modifiers 232 | 0 233 | modifiersubtext 234 | 235 | vitoclose 236 | 237 | 238 | 239 | 4DC9B99A-333B-4BC6-B2A3-13EC4B319487 240 | 241 | 242 | destinationuid 243 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 244 | modifiers 245 | 0 246 | modifiersubtext 247 | 248 | sourceoutputuid 249 | 50A5BB29-16B1-4D34-9AFF-87D64D2B2F30 250 | vitoclose 251 | 252 | 253 | 254 | destinationuid 255 | 7CE3F725-5F4E-4A4B-B16B-94871083E6A2 256 | modifiers 257 | 0 258 | modifiersubtext 259 | 260 | vitoclose 261 | 262 | 263 | 264 | 504CECC9-A64D-4F2A-A0E5-664B355EB529 265 | 266 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 267 | 268 | 6952186D-DC97-4377-BE52-F76AA93A602C 269 | 270 | 271 | destinationuid 272 | 968A454B-B9EE-46DD-8156-3630F678E31C 273 | modifiers 274 | 0 275 | modifiersubtext 276 | 277 | sourceoutputuid 278 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 279 | vitoclose 280 | 281 | 282 | 283 | destinationuid 284 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 285 | modifiers 286 | 0 287 | modifiersubtext 288 | 289 | vitoclose 290 | 291 | 292 | 293 | 76991846-250A-4BDC-9CE6-01A8D691D6C1 294 | 295 | 296 | destinationuid 297 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 298 | modifiers 299 | 0 300 | modifiersubtext 301 | 302 | vitoclose 303 | 304 | 305 | 306 | destinationuid 307 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 308 | modifiers 309 | 524288 310 | modifiersubtext 311 | ⌥: Copy URL 312 | vitoclose 313 | 314 | 315 | 316 | 996236DC-1621-432A-8AA2-7B4B6C6FE2A9 317 | 318 | 319 | destinationuid 320 | F1A38BE0-D312-40D9-B5FE-09C6D51AEDA3 321 | modifiers 322 | 0 323 | modifiersubtext 324 | 325 | vitoclose 326 | 327 | 328 | 329 | B04FEC5B-79E4-407B-ACCE-EAA0C5B81AB8 330 | 331 | 332 | destinationuid 333 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 334 | modifiers 335 | 0 336 | modifiersubtext 337 | 338 | sourceoutputuid 339 | 50A5BB29-16B1-4D34-9AFF-87D64D2B2F30 340 | vitoclose 341 | 342 | 343 | 344 | destinationuid 345 | D2728240-0953-42A8-A1AF-5289F6230277 346 | modifiers 347 | 0 348 | modifiersubtext 349 | 350 | vitoclose 351 | 352 | 353 | 354 | B35052FF-0027-40CC-BC59-EA7D442A57EA 355 | 356 | 357 | destinationuid 358 | 6952186D-DC97-4377-BE52-F76AA93A602C 359 | modifiers 360 | 0 361 | modifiersubtext 362 | 363 | vitoclose 364 | 365 | 366 | 367 | destinationuid 368 | 43F0CA76-BFE8-415E-97B3-F6B953E904FE 369 | modifiers 370 | 524288 371 | modifiersubtext 372 | ⌥: Copy URL 373 | vitoclose 374 | 375 | 376 | 377 | destinationuid 378 | FA2DA973-F996-4FDC-8058-22F7CCAE04A6 379 | modifiers 380 | 1048576 381 | modifiersubtext 382 | ⌘: Mark as Read 383 | vitoclose 384 | 385 | 386 | 387 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 388 | 389 | 390 | destinationuid 391 | 1B2789C4-ACE9-46F2-A786-B83B045166B2 392 | modifiers 393 | 0 394 | modifiersubtext 395 | 396 | vitoclose 397 | 398 | 399 | 400 | C0F5A902-C58A-4DEC-AF82-9EDD83CDFC6D 401 | 402 | 403 | destinationuid 404 | 462691DA-0319-4F31-B297-CC4A75FD1DFE 405 | modifiers 406 | 0 407 | modifiersubtext 408 | 409 | sourceoutputuid 410 | 5F9B8991-2746-4AFE-9EBE-32443D153C86 411 | vitoclose 412 | 413 | 414 | 415 | destinationuid 416 | 7CE3F725-5F4E-4A4B-B16B-94871083E6A2 417 | modifiers 418 | 0 419 | modifiersubtext 420 | 421 | vitoclose 422 | 423 | 424 | 425 | C9F8D0A3-F933-4772-9CD2-E8B592A87029 426 | 427 | 428 | destinationuid 429 | 0115FC58-CCEF-48EC-A08F-A405E8E30891 430 | modifiers 431 | 0 432 | modifiersubtext 433 | 434 | vitoclose 435 | 436 | 437 | 438 | CC25EB33-36DC-49B4-B46D-C70DC2FA45FD 439 | 440 | 441 | destinationuid 442 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 443 | modifiers 444 | 0 445 | modifiersubtext 446 | 447 | vitoclose 448 | 449 | 450 | 451 | destinationuid 452 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 453 | modifiers 454 | 524288 455 | modifiersubtext 456 | ⌥: Copy URL 457 | vitoclose 458 | 459 | 460 | 461 | destinationuid 462 | 0C28AAE6-A080-48A7-A6F0-0BB05070161D 463 | modifiers 464 | 1048576 465 | modifiersubtext 466 | ⌘: Open Homepage (or Releases) 467 | vitoclose 468 | 469 | 470 | 471 | destinationuid 472 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 473 | modifiers 474 | 262144 475 | modifiersubtext 476 | ⌃: Shallow Clone (and Fork) 477 | vitoclose 478 | 479 | 480 | 481 | F1A38BE0-D312-40D9-B5FE-09C6D51AEDA3 482 | 483 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 484 | 485 | 486 | destinationuid 487 | 996236DC-1621-432A-8AA2-7B4B6C6FE2A9 488 | modifiers 489 | 0 490 | modifiersubtext 491 | 492 | vitoclose 493 | 494 | 495 | 496 | destinationuid 497 | 42255CF9-BC03-46B1-8AB6-0AD60FA92ECE 498 | modifiers 499 | 0 500 | modifiersubtext 501 | 502 | vitoclose 503 | 504 | 505 | 506 | F99C6054-D3E9-4D1C-960A-8E9EC9FA5CB3 507 | 508 | 509 | destinationuid 510 | 06CC5118-3CAE-427B-928D-48B78C81B6B6 511 | modifiers 512 | 0 513 | modifiersubtext 514 | 515 | vitoclose 516 | 517 | 518 | 519 | FA2DA973-F996-4FDC-8058-22F7CCAE04A6 520 | 521 | 522 | destinationuid 523 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 524 | modifiers 525 | 0 526 | modifiersubtext 527 | 528 | vitoclose 529 | 530 | 531 | 532 | 533 | createdby 534 | Chris Grieser 535 | description 536 | GitHub Assistant 537 | disabled 538 | 539 | name 540 | GitFred 541 | objects 542 | 543 | 544 | config 545 | 546 | action 547 | 0 548 | argument 549 | 0 550 | focusedappvariable 551 | 552 | focusedappvariablename 553 | 554 | hotkey 555 | 8 556 | hotmod 557 | 1835008 558 | hotstring 559 | C 560 | leftcursor 561 | 562 | modsmode 563 | 0 564 | relatedAppsMode 565 | 0 566 | 567 | type 568 | alfred.workflow.trigger.hotkey 569 | uid 570 | C9F8D0A3-F933-4772-9CD2-E8B592A87029 571 | version 572 | 2 573 | 574 | 575 | config 576 | 577 | tasksettings 578 | 579 | allow_empty 580 | 581 | 582 | taskuid 583 | com.alfredapp.automation.extras/web-browsers/frontmost-browser/frontmost.tabs.current 584 | 585 | type 586 | alfred.workflow.automation.task 587 | uid 588 | 0115FC58-CCEF-48EC-A08F-A405E8E30891 589 | version 590 | 1 591 | 592 | 593 | config 594 | 595 | lastpathcomponent 596 | 597 | onlyshowifquerypopulated 598 | 599 | removeextension 600 | 601 | text 602 | Current tab not a GitHub Page 603 | title 604 | ⚠️ Error 605 | 606 | type 607 | alfred.workflow.output.notification 608 | uid 609 | 9AED849C-2F6C-4741-B375-875D5613781A 610 | version 611 | 1 612 | 613 | 614 | config 615 | 616 | conditions 617 | 618 | 619 | inputstring 620 | 621 | matchcasesensitive 622 | 623 | matchmode 624 | 4 625 | matchstring 626 | .*github.com/.+?/.+ 627 | outputlabel 628 | GitHub URL 629 | uid 630 | 629A9B2E-2F56-4A05-9F68-527094D7BCA8 631 | 632 | 633 | elselabel 634 | other/empty 635 | hideelse 636 | 637 | 638 | type 639 | alfred.workflow.utility.conditional 640 | uid 641 | 129ABCE4-E5CC-4547-98BB-9A4BC845CFFA 642 | version 643 | 1 644 | 645 | 646 | config 647 | 648 | browser 649 | 650 | skipqueryencode 651 | 652 | skipvarencode 653 | 654 | spaces 655 | 656 | url 657 | 658 | 659 | type 660 | alfred.workflow.action.openurl 661 | uid 662 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 663 | version 664 | 1 665 | 666 | 667 | config 668 | 669 | alfredfiltersresults 670 | 671 | alfredfiltersresultsmatchmode 672 | 2 673 | argumenttreatemptyqueryasnil 674 | 675 | argumenttrimmode 676 | 0 677 | argumenttype 678 | 1 679 | escaping 680 | 0 681 | keyword 682 | ghi 683 | queuedelaycustom 684 | 3 685 | queuedelayimmediatelyinitially 686 | 687 | queuedelaymode 688 | 0 689 | queuemode 690 | 1 691 | runningsubtext 692 | loading… 693 | script 694 | 695 | scriptargtype 696 | 1 697 | scriptfile 698 | scripts/my-github-issues.js 699 | subtext 700 | 701 | title 702 | Search my GitHub issues 703 | type 704 | 8 705 | withspace 706 | 707 | 708 | type 709 | alfred.workflow.input.scriptfilter 710 | uid 711 | 378B3595-A76C-4026-AC7F-0AC288FDF625 712 | version 713 | 3 714 | 715 | 716 | config 717 | 718 | lastpathcomponent 719 | 720 | onlyshowifquerypopulated 721 | 722 | removeextension 723 | 724 | text 725 | {query} 726 | title 727 | ✅ Copied 728 | 729 | type 730 | alfred.workflow.output.notification 731 | uid 732 | 504CECC9-A64D-4F2A-A0E5-664B355EB529 733 | version 734 | 1 735 | 736 | 737 | config 738 | 739 | autopaste 740 | 741 | clipboardtext 742 | {query} 743 | ignoredynamicplaceholders 744 | 745 | transient 746 | 747 | 748 | type 749 | alfred.workflow.output.clipboard 750 | uid 751 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 752 | version 753 | 3 754 | 755 | 756 | config 757 | 758 | alfredfiltersresults 759 | 760 | alfredfiltersresultsmatchmode 761 | 2 762 | argumenttreatemptyqueryasnil 763 | 764 | argumenttrimmode 765 | 0 766 | argumenttype 767 | 1 768 | escaping 769 | 0 770 | keyword 771 | gpr 772 | queuedelaycustom 773 | 3 774 | queuedelayimmediatelyinitially 775 | 776 | queuedelaymode 777 | 0 778 | queuemode 779 | 1 780 | runningsubtext 781 | loading… 782 | script 783 | 784 | scriptargtype 785 | 1 786 | scriptfile 787 | scripts/my-github-prs.js 788 | subtext 789 | 790 | title 791 | Search my GitHub PRs 792 | type 793 | 8 794 | withspace 795 | 796 | 797 | type 798 | alfred.workflow.input.scriptfilter 799 | uid 800 | 76991846-250A-4BDC-9CE6-01A8D691D6C1 801 | version 802 | 3 803 | 804 | 805 | config 806 | 807 | path 808 | 809 | 810 | type 811 | alfred.workflow.action.browseinterminal 812 | uid 813 | D2728240-0953-42A8-A1AF-5289F6230277 814 | version 815 | 1 816 | 817 | 818 | config 819 | 820 | conditions 821 | 822 | 823 | inputstring 824 | 825 | matchcasesensitive 826 | 827 | matchmode 828 | 4 829 | matchstring 830 | ^http 831 | outputlabel 832 | URL 833 | uid 834 | 50A5BB29-16B1-4D34-9AFF-87D64D2B2F30 835 | 836 | 837 | elselabel 838 | local path 839 | hideelse 840 | 841 | 842 | type 843 | alfred.workflow.utility.conditional 844 | uid 845 | B04FEC5B-79E4-407B-ACCE-EAA0C5B81AB8 846 | version 847 | 1 848 | 849 | 850 | config 851 | 852 | alfredfiltersresults 853 | 854 | alfredfiltersresultsmatchmode 855 | 2 856 | argumenttreatemptyqueryasnil 857 | 858 | argumenttrimmode 859 | 0 860 | argumenttype 861 | 1 862 | escaping 863 | 0 864 | keyword 865 | {var:keyword_public_repos} 866 | queuedelaycustom 867 | 3 868 | queuedelayimmediatelyinitially 869 | 870 | queuedelaymode 871 | 1 872 | queuemode 873 | 2 874 | runningsubtext 875 | loading… 876 | script 877 | 878 | scriptargtype 879 | 1 880 | scriptfile 881 | scripts/public-github-repo-search.js 882 | subtext 883 | 884 | title 885 | Search public GitHub repos 886 | type 887 | 8 888 | withspace 889 | 890 | 891 | type 892 | alfred.workflow.input.scriptfilter 893 | uid 894 | CC25EB33-36DC-49B4-B46D-C70DC2FA45FD 895 | version 896 | 3 897 | 898 | 899 | type 900 | alfred.workflow.utility.junction 901 | uid 902 | 0C28AAE6-A080-48A7-A6F0-0BB05070161D 903 | version 904 | 1 905 | 906 | 907 | config 908 | 909 | lastpathcomponent 910 | 911 | onlyshowifquerypopulated 912 | 913 | removeextension 914 | 915 | text 916 | {query} 917 | title 918 | 🧬 Cloning… 919 | 920 | type 921 | alfred.workflow.output.notification 922 | uid 923 | F1A38BE0-D312-40D9-B5FE-09C6D51AEDA3 924 | version 925 | 1 926 | 927 | 928 | config 929 | 930 | matchmode 931 | 1 932 | matchstring 933 | .*github.com\/([^\/?]*\/[^\/?]*).* 934 | regexcaseinsensitive 935 | 936 | regexmultiline 937 | 938 | replacestring 939 | $1 940 | 941 | type 942 | alfred.workflow.utility.replace 943 | uid 944 | 996236DC-1621-432A-8AA2-7B4B6C6FE2A9 945 | version 946 | 2 947 | 948 | 949 | config 950 | 951 | lastpathcomponent 952 | 953 | onlyshowifquerypopulated 954 | 955 | removeextension 956 | 957 | text 958 | 959 | title 960 | ⚠️ {query} 961 | 962 | type 963 | alfred.workflow.output.notification 964 | uid 965 | 462691DA-0319-4F31-B297-CC4A75FD1DFE 966 | version 967 | 1 968 | 969 | 970 | config 971 | 972 | alfredfiltersresults 973 | 974 | alfredfiltersresultsmatchmode 975 | 2 976 | argumenttreatemptyqueryasnil 977 | 978 | argumenttrimmode 979 | 0 980 | argumenttype 981 | 1 982 | escaping 983 | 0 984 | keyword 985 | {var:keyword_personal_repos} 986 | queuedelaycustom 987 | 3 988 | queuedelayimmediatelyinitially 989 | 990 | queuedelaymode 991 | 0 992 | queuemode 993 | 1 994 | runningsubtext 995 | loading… 996 | script 997 | 998 | scriptargtype 999 | 1 1000 | scriptfile 1001 | ./scripts/my-github-repos.js 1002 | subtext 1003 | 1004 | title 1005 | Search my GitHub repos 1006 | type 1007 | 8 1008 | withspace 1009 | 1010 | 1011 | inboundconfig 1012 | 1013 | inputmode 1014 | 1 1015 | 1016 | type 1017 | alfred.workflow.input.scriptfilter 1018 | uid 1019 | 388AD75C-BCFB-445D-B62E-13BC0545FD5E 1020 | version 1021 | 3 1022 | 1023 | 1024 | config 1025 | 1026 | concurrently 1027 | 1028 | escaping 1029 | 0 1030 | script 1031 | 1032 | scriptargtype 1033 | 1 1034 | scriptfile 1035 | scripts/clone-repo.sh 1036 | type 1037 | 8 1038 | 1039 | type 1040 | alfred.workflow.action.script 1041 | uid 1042 | 42255CF9-BC03-46B1-8AB6-0AD60FA92ECE 1043 | version 1044 | 2 1045 | 1046 | 1047 | config 1048 | 1049 | conditions 1050 | 1051 | 1052 | inputstring 1053 | 1054 | matchcasesensitive 1055 | 1056 | matchmode 1057 | 4 1058 | matchstring 1059 | ^ERROR 1060 | outputlabel 1061 | error 1062 | uid 1063 | 5F9B8991-2746-4AFE-9EBE-32443D153C86 1064 | 1065 | 1066 | elselabel 1067 | local repo 1068 | hideelse 1069 | 1070 | 1071 | type 1072 | alfred.workflow.utility.conditional 1073 | uid 1074 | C0F5A902-C58A-4DEC-AF82-9EDD83CDFC6D 1075 | version 1076 | 1 1077 | 1078 | 1079 | type 1080 | alfred.workflow.utility.junction 1081 | uid 1082 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 1083 | version 1084 | 1 1085 | 1086 | 1087 | config 1088 | 1089 | path 1090 | 1091 | 1092 | type 1093 | alfred.workflow.action.browseinterminal 1094 | uid 1095 | 7CE3F725-5F4E-4A4B-B16B-94871083E6A2 1096 | version 1097 | 1 1098 | 1099 | 1100 | config 1101 | 1102 | conditions 1103 | 1104 | 1105 | inputstring 1106 | 1107 | matchcasesensitive 1108 | 1109 | matchmode 1110 | 4 1111 | matchstring 1112 | ^http 1113 | outputlabel 1114 | URL 1115 | uid 1116 | 50A5BB29-16B1-4D34-9AFF-87D64D2B2F30 1117 | 1118 | 1119 | elselabel 1120 | local path 1121 | hideelse 1122 | 1123 | 1124 | type 1125 | alfred.workflow.utility.conditional 1126 | uid 1127 | 4DC9B99A-333B-4BC6-B2A3-13EC4B319487 1128 | version 1129 | 1 1130 | 1131 | 1132 | config 1133 | 1134 | externaltriggerid 1135 | github-notifications 1136 | passinputasargument 1137 | 1138 | passvariables 1139 | 1140 | workflowbundleid 1141 | self 1142 | 1143 | type 1144 | alfred.workflow.output.callexternaltrigger 1145 | uid 1146 | 968A454B-B9EE-46DD-8156-3630F678E31C 1147 | version 1148 | 1 1149 | 1150 | 1151 | config 1152 | 1153 | conditions 1154 | 1155 | 1156 | inputstring 1157 | {var:mode} 1158 | matchcasesensitive 1159 | 1160 | matchmode 1161 | 0 1162 | matchstring 1163 | show-read-notifications 1164 | outputlabel 1165 | yes 1166 | uid 1167 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 1168 | 1169 | 1170 | elselabel 1171 | no 1172 | hideelse 1173 | 1174 | 1175 | type 1176 | alfred.workflow.utility.conditional 1177 | uid 1178 | 6952186D-DC97-4377-BE52-F76AA93A602C 1179 | version 1180 | 1 1181 | 1182 | 1183 | config 1184 | 1185 | conditions 1186 | 1187 | 1188 | inputstring 1189 | {var:notificationsLeft} 1190 | matchcasesensitive 1191 | 1192 | matchmode 1193 | 1 1194 | matchstring 1195 | 0 1196 | outputlabel 1197 | yes 1198 | uid 1199 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 1200 | 1201 | 1202 | elselabel 1203 | no 1204 | hideelse 1205 | 1206 | 1207 | type 1208 | alfred.workflow.utility.conditional 1209 | uid 1210 | 08B7842B-16DA-4BFC-8F35-AE680AF1E07B 1211 | version 1212 | 1 1213 | 1214 | 1215 | config 1216 | 1217 | concurrently 1218 | 1219 | escaping 1220 | 0 1221 | script 1222 | 1223 | scriptargtype 1224 | 1 1225 | scriptfile 1226 | scripts/resolve-notification.sh 1227 | type 1228 | 8 1229 | 1230 | type 1231 | alfred.workflow.action.script 1232 | uid 1233 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 1234 | version 1235 | 2 1236 | 1237 | 1238 | config 1239 | 1240 | alfredfiltersresults 1241 | 1242 | alfredfiltersresultsmatchmode 1243 | 2 1244 | argumenttreatemptyqueryasnil 1245 | 1246 | argumenttrimmode 1247 | 0 1248 | argumenttype 1249 | 1 1250 | escaping 1251 | 0 1252 | keyword 1253 | ghn 1254 | queuedelaycustom 1255 | 3 1256 | queuedelayimmediatelyinitially 1257 | 1258 | queuedelaymode 1259 | 0 1260 | queuemode 1261 | 1 1262 | runningsubtext 1263 | loading… 1264 | script 1265 | 1266 | scriptargtype 1267 | 1 1268 | scriptfile 1269 | ./scripts/github-notifications.js 1270 | subtext 1271 | 1272 | title 1273 | 🔔 GitHub Notifications 1274 | type 1275 | 8 1276 | withspace 1277 | 1278 | 1279 | inboundconfig 1280 | 1281 | externalid 1282 | github-notifications 1283 | inputmode 1284 | 1 1285 | 1286 | type 1287 | alfred.workflow.input.scriptfilter 1288 | uid 1289 | B35052FF-0027-40CC-BC59-EA7D442A57EA 1290 | version 1291 | 3 1292 | 1293 | 1294 | config 1295 | 1296 | conditions 1297 | 1298 | 1299 | inputstring 1300 | {var:mode} 1301 | matchcasesensitive 1302 | 1303 | matchmode 1304 | 0 1305 | matchstring 1306 | mark-as-read 1307 | outputlabel 1308 | mark-as-read 1309 | uid 1310 | 256FC39B-A43F-4DB1-A4E2-01DAFE87554A 1311 | 1312 | 1313 | inputstring 1314 | {var:mode} 1315 | matchcasesensitive 1316 | 1317 | matchmode 1318 | 0 1319 | matchstring 1320 | copy 1321 | outputlabel 1322 | copy 1323 | uid 1324 | D4409822-4D36-4A47-AE65-97902F001379 1325 | 1326 | 1327 | elselabel 1328 | open 1329 | hideelse 1330 | 1331 | 1332 | type 1333 | alfred.workflow.utility.conditional 1334 | uid 1335 | 1B2789C4-ACE9-46F2-A786-B83B045166B2 1336 | version 1337 | 1 1338 | 1339 | 1340 | config 1341 | 1342 | lastpathcomponent 1343 | 1344 | onlyshowifquerypopulated 1345 | 1346 | removeextension 1347 | 1348 | text 1349 | 1350 | title 1351 | ☑️ Marked as read 1352 | 1353 | type 1354 | alfred.workflow.output.notification 1355 | uid 1356 | 122C2D04-00F3-4F54-9132-9DE395C2194A 1357 | version 1358 | 1 1359 | 1360 | 1361 | type 1362 | alfred.workflow.utility.junction 1363 | uid 1364 | 43F0CA76-BFE8-415E-97B3-F6B953E904FE 1365 | version 1366 | 1 1367 | 1368 | 1369 | config 1370 | 1371 | lastpathcomponent 1372 | 1373 | onlyshowifquerypopulated 1374 | 1375 | removeextension 1376 | 1377 | text 1378 | {query} 1379 | title 1380 | 📋 Copied 1381 | 1382 | type 1383 | alfred.workflow.output.notification 1384 | uid 1385 | F99C6054-D3E9-4D1C-960A-8E9EC9FA5CB3 1386 | version 1387 | 1 1388 | 1389 | 1390 | config 1391 | 1392 | unstackview 1393 | 1394 | 1395 | type 1396 | alfred.workflow.utility.hidealfred 1397 | uid 1398 | 06CC5118-3CAE-427B-928D-48B78C81B6B6 1399 | version 1400 | 1 1401 | 1402 | 1403 | type 1404 | alfred.workflow.utility.junction 1405 | uid 1406 | FA2DA973-F996-4FDC-8058-22F7CCAE04A6 1407 | version 1408 | 1 1409 | 1410 | 1411 | config 1412 | 1413 | lastpathcomponent 1414 | 1415 | onlyshowifquerypopulated 1416 | 1417 | removeextension 1418 | 1419 | text 1420 | {query} 1421 | title 1422 | ⚠️ URL not found 1423 | 1424 | type 1425 | alfred.workflow.output.notification 1426 | uid 1427 | 7B955092-9044-4895-864C-3532731DEDBE 1428 | version 1429 | 1 1430 | 1431 | 1432 | readme 1433 | ## Usage 1434 | - Search all public GitHub repositories via the keyword `gh`. 1435 | + <kbd>⏎</kbd>: Open the repo's GitHub page. 1436 | + <kbd>⌘⏎</kbd>: Open the repo's homepage. If it has none, opens the 1437 | releases page instead. 1438 | + <kbd>⌥⏎</kbd>: Copy the repo URL. 1439 | + <kbd>⌃⏎</kbd>: Clone the repo to a local folder and open in the Terminal. 1440 | - Access your repositories via `gmy`. (Limited to the 100 repos last updated.) 1441 | + <kbd>⏎</kbd>: If the repo is available locally on your device, open it in 1442 | the terminal. Otherwise, open the repo's GitHub page. 1443 | + <kbd>⌘⏎</kbd>: Open the repo's GitHub page. 1444 | + <kbd>⌥⏎</kbd>: Copy the repo URL. 1445 | + <kbd>⌃⏎</kbd>: Clone the repo to a local folder and open in the Terminal. 1446 | If the repo is already locally available on your device, just open the 1447 | repository in the Terminal. 1448 | + To include private repos, requires [GitHub 1449 | Token](https://github.com/settings/tokens). 1450 | - Open recent GitHub issues you are involved in with `ghi`. 1451 | + <kbd>⏎</kbd>: Open the issue in the browser. 1452 | + <kbd>⌥⏎</kbd>: Copy the issue URL. 1453 | - Access PRs you have opened with the keyword `gpr`. 1454 | + <kbd>⏎</kbd>: Open the PR in the browser. 1455 | + <kbd>⌥⏎</kbd>: Copy the link to the PR. 1456 | + To include private PRs, requires [GitHub 1457 | Token](https://github.com/settings/tokens). 1458 | - Directly open your GitHub notification via `ghn`. 1459 | + Requires [GitHub Token](https://github.com/settings/tokens). 1460 | + <kbd>⏎</kbd>: Open the notification in the browser. In cases where no URL 1461 | is available, such as deleted comments or CI activity, opens the GitHub 1462 | notification inbox instead. 1463 | + <kbd>⌘⏎</kbd>: Mark the notification as read. 1464 | + <kbd>⌥⏎</kbd>: Copy the URL of the notification target. 1465 | - Additionally, you can press the 1466 | [hotkey](https://www.alfredapp.com/help/workflows/triggers/hotkey/) to clone 1467 | a GitHub repo from the current browser tab. ([Note that Firefox is not 1468 | supported.](https://www.alfredforum.com/topic/16748-how-to-do-x-in-firefox-from-alfred/)) 1469 | 1470 | --- 1471 | 1472 | #### Created by [Chris Grieser](https://chris-grieser.de/) | ⭐️ [Star on GitHub](https://github.com/chrisgrieser/gitfred) 1473 | uidata 1474 | 1475 | 0115FC58-CCEF-48EC-A08F-A405E8E30891 1476 | 1477 | colorindex 1478 | 8 1479 | xpos 1480 | 230 1481 | ypos 1482 | 15 1483 | 1484 | 06CC5118-3CAE-427B-928D-48B78C81B6B6 1485 | 1486 | colorindex 1487 | 11 1488 | xpos 1489 | 805 1490 | ypos 1491 | 1005 1492 | 1493 | 08B7842B-16DA-4BFC-8F35-AE680AF1E07B 1494 | 1495 | colorindex 1496 | 11 1497 | note 1498 | notifications left 1499 | xpos 1500 | 671 1501 | ypos 1502 | 885 1503 | 1504 | 0C28AAE6-A080-48A7-A6F0-0BB05070161D 1505 | 1506 | colorindex 1507 | 7 1508 | xpos 1509 | 290 1510 | ypos 1511 | 500 1512 | 1513 | 122C2D04-00F3-4F54-9132-9DE395C2194A 1514 | 1515 | colorindex 1516 | 11 1517 | xpos 1518 | 890 1519 | ypos 1520 | 940 1521 | 1522 | 129ABCE4-E5CC-4547-98BB-9A4BC845CFFA 1523 | 1524 | colorindex 1525 | 8 1526 | xpos 1527 | 395 1528 | ypos 1529 | 35 1530 | 1531 | 1B2789C4-ACE9-46F2-A786-B83B045166B2 1532 | 1533 | colorindex 1534 | 11 1535 | note 1536 | mode 1537 | xpos 1538 | 470 1539 | ypos 1540 | 905 1541 | 1542 | 32D387DE-B4F9-41E5-8F1C-A71E3C5644AA 1543 | 1544 | colorindex 1545 | 2 1546 | xpos 1547 | 725 1548 | ypos 1549 | 265 1550 | 1551 | 378B3595-A76C-4026-AC7F-0AC288FDF625 1552 | 1553 | colorindex 1554 | 2 1555 | note 1556 | search my issues 1557 | xpos 1558 | 30 1559 | ypos 1560 | 200 1561 | 1562 | 388AD75C-BCFB-445D-B62E-13BC0545FD5E 1563 | 1564 | colorindex 1565 | 9 1566 | note 1567 | personal repo search 1568 | xpos 1569 | 30 1570 | ypos 1571 | 610 1572 | 1573 | 42255CF9-BC03-46B1-8AB6-0AD60FA92ECE 1574 | 1575 | colorindex 1576 | 6 1577 | note 1578 | clone 1579 | xpos 1580 | 725 1581 | ypos 1582 | 620 1583 | 1584 | 43F0CA76-BFE8-415E-97B3-F6B953E904FE 1585 | 1586 | colorindex 1587 | 11 1588 | xpos 1589 | 210 1590 | ypos 1591 | 965 1592 | 1593 | 462691DA-0319-4F31-B297-CC4A75FD1DFE 1594 | 1595 | colorindex 1596 | 1 1597 | xpos 1598 | 1005 1599 | ypos 1600 | 570 1601 | 1602 | 4DC9B99A-333B-4BC6-B2A3-13EC4B319487 1603 | 1604 | colorindex 1605 | 6 1606 | xpos 1607 | 475 1608 | ypos 1609 | 700 1610 | 1611 | 504CECC9-A64D-4F2A-A0E5-664B355EB529 1612 | 1613 | colorindex 1614 | 2 1615 | xpos 1616 | 885 1617 | ypos 1618 | 265 1619 | 1620 | 5AF52A7B-9FA9-4D8B-9784-4864B0A9AF06 1621 | 1622 | colorindex 1623 | 2 1624 | xpos 1625 | 725 1626 | ypos 1627 | 150 1628 | 1629 | 6952186D-DC97-4377-BE52-F76AA93A602C 1630 | 1631 | colorindex 1632 | 11 1633 | note 1634 | show-read-notifs 1635 | xpos 1636 | 211 1637 | ypos 1638 | 855 1639 | 1640 | 76991846-250A-4BDC-9CE6-01A8D691D6C1 1641 | 1642 | colorindex 1643 | 2 1644 | note 1645 | search my open PRs 1646 | xpos 1647 | 30 1648 | ypos 1649 | 335 1650 | 1651 | 7B955092-9044-4895-864C-3532731DEDBE 1652 | 1653 | colorindex 1654 | 11 1655 | xpos 1656 | 635 1657 | ypos 1658 | 1095 1659 | 1660 | 7CE3F725-5F4E-4A4B-B16B-94871083E6A2 1661 | 1662 | colorindex 1663 | 6 1664 | xpos 1665 | 1005 1666 | ypos 1667 | 695 1668 | 1669 | 968A454B-B9EE-46DD-8156-3630F678E31C 1670 | 1671 | colorindex 1672 | 11 1673 | xpos 1674 | 890 1675 | ypos 1676 | 820 1677 | 1678 | 996236DC-1621-432A-8AA2-7B4B6C6FE2A9 1679 | 1680 | colorindex 1681 | 6 1682 | note 1683 | get reponame 1684 | xpos 1685 | 660 1686 | ypos 1687 | 535 1688 | 1689 | 9AED849C-2F6C-4741-B375-875D5613781A 1690 | 1691 | colorindex 1692 | 1 1693 | xpos 1694 | 725 1695 | ypos 1696 | 30 1697 | 1698 | B04FEC5B-79E4-407B-ACCE-EAA0C5B81AB8 1699 | 1700 | colorindex 1701 | 9 1702 | xpos 1703 | 610 1704 | ypos 1705 | 390 1706 | 1707 | B35052FF-0027-40CC-BC59-EA7D442A57EA 1708 | 1709 | colorindex 1710 | 11 1711 | note 1712 | github notifications 1713 | xpos 1714 | 30 1715 | ypos 1716 | 900 1717 | 1718 | BFABA717-A81E-4D69-A8C2-4ED2042B4588 1719 | 1720 | colorindex 1721 | 11 1722 | note 1723 | action on thread ID 1724 | xpos 1725 | 305 1726 | ypos 1727 | 900 1728 | 1729 | C0F5A902-C58A-4DEC-AF82-9EDD83CDFC6D 1730 | 1731 | colorindex 1732 | 6 1733 | xpos 1734 | 890 1735 | ypos 1736 | 640 1737 | 1738 | C9F8D0A3-F933-4772-9CD2-E8B592A87029 1739 | 1740 | colorindex 1741 | 8 1742 | note 1743 | DOUBLE-CLICK THIS 1744 | 1745 | to set a hotkey to clone the repo in the frontmost browser tab 1746 | xpos 1747 | 30 1748 | ypos 1749 | 15 1750 | 1751 | CC25EB33-36DC-49B4-B46D-C70DC2FA45FD 1752 | 1753 | colorindex 1754 | 7 1755 | note 1756 | public repo search 1757 | xpos 1758 | 30 1759 | ypos 1760 | 470 1761 | 1762 | D2728240-0953-42A8-A1AF-5289F6230277 1763 | 1764 | colorindex 1765 | 9 1766 | xpos 1767 | 725 1768 | ypos 1769 | 385 1770 | 1771 | F1A38BE0-D312-40D9-B5FE-09C6D51AEDA3 1772 | 1773 | colorindex 1774 | 6 1775 | xpos 1776 | 725 1777 | ypos 1778 | 505 1779 | 1780 | F1ACCD95-5E8C-469E-AFD1-D184923540FB 1781 | 1782 | colorindex 1783 | 6 1784 | xpos 1785 | 585 1786 | ypos 1787 | 650 1788 | 1789 | F99C6054-D3E9-4D1C-960A-8E9EC9FA5CB3 1790 | 1791 | colorindex 1792 | 11 1793 | xpos 1794 | 635 1795 | ypos 1796 | 975 1797 | 1798 | FA2DA973-F996-4FDC-8058-22F7CCAE04A6 1799 | 1800 | colorindex 1801 | 11 1802 | xpos 1803 | 210 1804 | ypos 1805 | 1015 1806 | 1807 | 1808 | userconfigurationconfig 1809 | 1810 | 1811 | config 1812 | 1813 | default 1814 | 1815 | placeholder 1816 | 1817 | required 1818 | 1819 | trim 1820 | 1821 | 1822 | description 1823 | 1824 | label 1825 | GitHub username 1826 | type 1827 | textfield 1828 | variable 1829 | github_username 1830 | 1831 | 1832 | config 1833 | 1834 | default 1835 | 1836 | placeholder 1837 | ghp_… 1838 | required 1839 | 1840 | trim 1841 | 1842 | 1843 | description 1844 | Optional. Only required for showing notifications, private PRs, and private repos. If empty, will fallback to `$GITHUB_TOKEN` exported in `.zshenv`. The token requires accesses to private repos and to notifications. 1845 | label 1846 | GitHub Token 1847 | type 1848 | textfield 1849 | variable 1850 | github_token_from_alfred_prefs 1851 | 1852 | 1853 | config 1854 | 1855 | default 1856 | gh 1857 | placeholder 1858 | gh 1859 | required 1860 | 1861 | trim 1862 | 1863 | 1864 | description 1865 | Search all public repos 1866 | label 1867 | Keywords 1868 | type 1869 | textfield 1870 | variable 1871 | keyword_public_repos 1872 | 1873 | 1874 | config 1875 | 1876 | default 1877 | gmy 1878 | placeholder 1879 | gmy 1880 | required 1881 | 1882 | trim 1883 | 1884 | 1885 | description 1886 | Search your personal repos 1887 | label 1888 | 1889 | type 1890 | textfield 1891 | variable 1892 | keyword_personal_repos 1893 | 1894 | 1895 | config 1896 | 1897 | default 1898 | 1899 | required 1900 | 1901 | text 1902 | use Alfred knowledge 1903 | 1904 | description 1905 | The search lists your 100 most recent repos, sorted by last updated time. If enabled, will also use Alfred knowledge (recent & frequent use) to sort the repos. 1906 | label 1907 | Personal repos order 1908 | type 1909 | checkbox 1910 | variable 1911 | use_alfred_frecency 1912 | 1913 | 1914 | config 1915 | 1916 | default 1917 | ~/repos 1918 | filtermode 1919 | 1 1920 | placeholder 1921 | ~/repos 1922 | required 1923 | 1924 | 1925 | description 1926 | Local folder where repositories should be cloned to. 1927 | label 1928 | Cloning 1929 | type 1930 | filepicker 1931 | variable 1932 | local_repo_folder 1933 | 1934 | 1935 | config 1936 | 1937 | defaultvalue 1938 | 5 1939 | markercount 1940 | 11 1941 | maxvalue 1942 | 50 1943 | minvalue 1944 | 0 1945 | onlystoponmarkers 1946 | 1947 | showmarkers 1948 | 1949 | 1950 | description 1951 | Depth of the clone (`--shallow=n`). Set to 0 to not perform shallow cloning. 1952 | label 1953 | 1954 | type 1955 | slider 1956 | variable 1957 | clone_depth 1958 | 1959 | 1960 | config 1961 | 1962 | default 1963 | 1964 | placeholder 1965 | dev 1966 | required 1967 | 1968 | trim 1969 | 1970 | 1971 | description 1972 | Switch to this branch after cloning, if it exists. Leave empty to disable. 1973 | label 1974 | 1975 | type 1976 | textfield 1977 | variable 1978 | branch_on_clone 1979 | 1980 | 1981 | config 1982 | 1983 | default 1984 | disabled 1985 | pairs 1986 | 1987 | 1988 | disabled 1989 | disabled 1990 | 1991 | 1992 | partial (quick) 1993 | quick-partial 1994 | 1995 | 1996 | full (slow) 1997 | slow-full 1998 | 1999 | 2000 | 2001 | description 2002 | Restores the modification time of files that git erases when cloning. "full" restores the mtime for every file, but is very slow on large repos. "partial" is magnitudes quicker, but restores the mtime only for files touched in the last x commits. When using cloning shallowly (clone depth > 0), will always use "partial". 2003 | label 2004 | restore mtime 2005 | type 2006 | popupbutton 2007 | variable 2008 | restore_mtime 2009 | 2010 | 2011 | config 2012 | 2013 | default 2014 | 2015 | required 2016 | 2017 | text 2018 | fork when cloning 2019 | 2020 | description 2021 | Forking requires the `gh` cli. 2022 | label 2023 | Forking 2024 | type 2025 | checkbox 2026 | variable 2027 | fork_on_clone 2028 | 2029 | 2030 | config 2031 | 2032 | default 2033 | 2034 | required 2035 | 2036 | text 2037 | setup remotes 2038 | 2039 | description 2040 | Point `upstream` to the source, and `origin` to the fork. This is useful for quick PRs. 2041 | label 2042 | 2043 | type 2044 | checkbox 2045 | variable 2046 | setup_remotes_on_fork 2047 | 2048 | 2049 | config 2050 | 2051 | default 2052 | 2053 | placeholder 2054 | dev 2055 | required 2056 | 2057 | trim 2058 | 2059 | 2060 | description 2061 | After forking, create this branch and switch to it. Leave empty to disable. 2062 | label 2063 | 2064 | type 2065 | textfield 2066 | variable 2067 | on_fork_branch 2068 | 2069 | 2070 | version 2071 | 1.10.4 2072 | webaddress 2073 | https://chris-grieser.de/ 2074 | 2075 | 2076 | -------------------------------------------------------------------------------- /scripts/clone-repo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | # shellcheck disable=2154 3 | #─────────────────────────────────────────────────────────────────────────────── 4 | 5 | # VARIABLES 6 | https_url="$1" 7 | source_repo=$(echo "$https_url" | sed -E 's_.*github.com/([^/?]*/[^/?]*).*_\1_') 8 | reponame=$(echo "$source_repo" | cut -d '/' -f2) 9 | owner=$(echo "$source_repo" | cut -d '/' -f1) 10 | ssh_url="git@github.com:$source_repo" 11 | 12 | [[ ! -e "$local_repo_folder" ]] && mkdir -p "$local_repo_folder" 13 | cd "$local_repo_folder" || return 1 14 | 15 | #─────────────────────────────────────────────────────────────────────────────── 16 | # CLONE 17 | 18 | # if multiple repos of same name, add owner to directory name of both the 19 | # existing and the to-be-cloned repo (see https://github.com/chrisgrieser/gitfred/issues/5) 20 | # (uses `__` as separator, since that string normally does not occur in reponames) 21 | clone_dir="$reponame" 22 | if [[ -d "$reponame" ]]; then 23 | clone_dir="${owner}__$reponame" 24 | # rename existing repo 25 | owner_of_existing_repo=$(git -C "$reponame" remote --verbose | tail -n1 | sed -Ee 's|.*:(.*)/.*|\1|') 26 | if [[ "$owner_of_existing_repo" == "$owner" ]]; then 27 | echo "ERROR: $source_repo already exists." 28 | return 1 29 | fi 30 | mv "$reponame" "${owner_of_existing_repo}__$reponame" 31 | elif [[ -n $(find . -type directory -maxdepth 1 -name "*__$reponame") ]]; then 32 | clone_dir="${owner}__$reponame" 33 | fi 34 | 35 | # clone with depth 36 | if [[ $clone_depth -eq 0 ]]; then 37 | msg=$(git clone "$ssh_url" --no-single-branch --no-tags "$clone_dir" 2>&1) 38 | else 39 | # WARN depth=1 is dangerous, as amending such a commit does result in a 40 | # new commit without parent, effectively destroying git history (!!) 41 | [[ $clone_depth -eq 1 ]] && clone_depth=2 42 | msg=$(git clone "$ssh_url" --depth="$clone_depth" --no-single-branch --no-tags "$clone_dir" 2>&1) 43 | fi 44 | 45 | success=$? 46 | if [[ $success -ne 0 ]]; then 47 | echo "ERROR: Clone failed. $msg" 48 | return 1 49 | fi 50 | 51 | # Open in terminal via Alfred 52 | abs_path="$local_repo_folder/$clone_dir" 53 | echo -n "$abs_path" 54 | 55 | cd "$clone_dir" || return 1 56 | 57 | #─────────────────────────────────────────────────────────────────────────────── 58 | 59 | # BRANCH ON CLONE 60 | if [[ -n "$branch_on_clone" ]]; then 61 | # `git switch` fails silently if the branch does not exist 62 | git switch "$branch_on_clone" &> /dev/null 63 | fi 64 | 65 | # RESTORE MTIME 66 | # PERF `partial` checks only files touched in the last x commits (with x being clone 67 | # depth or 50), while `full` checks every single file. `partial` is magnitudes 68 | # quicker, but does not restore the mtime for all files correctly, since only 69 | # some commits are considered. If using shallow clones (`clone_depth` > 0), will 70 | # automatically use `partial`, since there is not enough git history for correct 71 | # mtime restoring, so `partial` and `full` have the same result, and using 72 | # `partial` is then always preferable due to being quicker. 73 | if [[ "$restore_mtime" == "quick-partial" || $clone_depth -ne 0 ]]; then 74 | how_far=$([[ $clone_depth -eq 0 ]] && echo 50 || echo $((clone_depth - 1))) 75 | 76 | # set date for all files to x+1 commits ago 77 | oldest_commit=$(git log -1 --format="%h" HEAD~"$how_far") 78 | old_timestamp=$(git log -1 --format="%cd" --date="format:%Y%m%d%H%M.%S" "$oldest_commit") 79 | git ls-tree -z -t -r --name-only HEAD | xargs -0 touch -t "$old_timestamp" 80 | 81 | # set mtime for all files touched in last x commits 82 | # (reverse with `tail -r` so most recent commit comes last) 83 | last_commits=$(git log --format="%h" --max-count="$((how_far - 1))" | tail -r) 84 | echo "$last_commits" | while read -r hash; do 85 | timestamp=$(git log -1 --format="%cd" --date="format:%Y%m%d%H%M.%S" "$hash") 86 | changed_files=$(git log -1 --name-only --format="" "$hash") 87 | echo "$changed_files" | while read -r file; do 88 | # check for file existence, since a file could have been deleted/moved 89 | [[ -f "$file" ]] && touch -t "$timestamp" "$file" 90 | done 91 | done 92 | elif [[ "$restore_mtime" == "slow-full" ]]; then 93 | # https://stackoverflow.com/a/36243002/22114136 94 | git ls-tree -t -r --name-only HEAD | while read -r file; do 95 | timestamp=$(git log --format="%cd" --date="format:%Y%m%d%H%M.%S" -1 HEAD -- "$file") 96 | touch -t "$timestamp" "$file" 97 | done 98 | fi 99 | 100 | #─────────────────────────────────────────────────────────────────────────────── 101 | # FORKING 102 | 103 | # INFO Alfred stores checkbox settings as `"1"` or `"0"`, and variables in stringified form. 104 | if [[ "$ownerOfRepo" != "true" && "$fork_on_clone" == "1" ]]; then 105 | 106 | if [[ -x "$(command -v gh)" ]]; then 107 | gh repo fork --remote=false &> /dev/null 108 | else 109 | echo "ERROR: Cannot fork, \`gh\` not installed." 110 | fi 111 | 112 | if [[ "$setup_remotes_on_fork" == "1" ]]; then 113 | git remote rename origin upstream 114 | git remote add origin "git@github.com:$github_username/$reponame.git" 115 | fi 116 | 117 | if [[ -n "$on_fork_branch" ]]; then 118 | git switch --create "$on_fork_branch" &> /dev/null 119 | fi 120 | fi 121 | -------------------------------------------------------------------------------- /scripts/github-notifications.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | /** 8 | * @param {string} url 9 | * @param {string[]} header 10 | * @return {string} response 11 | */ 12 | function httpRequestWithHeaders(url, header) { 13 | let allHeaders = ""; 14 | for (const line of header) { 15 | allHeaders += ` -H "${line}"`; 16 | } 17 | const curlRequest = `curl --silent --location ${allHeaders} "${url}" || true`; 18 | console.log("curl command:", curlRequest); 19 | return app.doShellScript(curlRequest); 20 | } 21 | 22 | /** @param {string} isoDateStr */ 23 | function humanRelativeDate(isoDateStr) { 24 | const deltaMins = (Date.now() - +new Date(isoDateStr)) / 1000 / 60; 25 | /** @type {"year"|"month"|"week"|"day"|"hour"|"minute"} */ 26 | let unit; 27 | let delta; 28 | if (deltaMins < 60) { 29 | unit = "minute"; 30 | delta = Math.floor(deltaMins); 31 | } else if (deltaMins < 60 * 24) { 32 | unit = "hour"; 33 | delta = Math.floor(deltaMins / 60); 34 | } else if (deltaMins < 60 * 24 * 7) { 35 | unit = "day"; 36 | delta = Math.floor(deltaMins / 60 / 24); 37 | } else if (deltaMins < 60 * 24 * 7 * 4) { 38 | unit = "week"; 39 | delta = Math.floor(deltaMins / 60 / 24 / 7); 40 | } else if (deltaMins < 60 * 24 * 7 * 4 * 12) { 41 | unit = "month"; 42 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4); 43 | } else { 44 | unit = "year"; 45 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4 / 12); 46 | } 47 | const formatter = new Intl.RelativeTimeFormat("en", { style: "narrow", numeric: "auto" }); 48 | const str = formatter.format(-delta, unit); 49 | return str.replace(/m(?= ago$)/, "min"); // "m" -> "min" (more distinguishable from "month") 50 | } 51 | 52 | //────────────────────────────────────────────────────────────────────────────── 53 | 54 | /** @type {AlfredRun} */ 55 | // biome-ignore lint/correctness/noUnusedVariables: Alfred run 56 | function run() { 57 | const tokenShellCmd = "test -e $HOME/.zshenv && source $HOME/.zshenv ; echo $GITHUB_TOKEN"; 58 | const githubToken = 59 | $.getenv("github_token_from_alfred_prefs").trim() || app.doShellScript(tokenShellCmd).trim(); 60 | const showReadNotifs = 61 | $.NSProcessInfo.processInfo.environment.objectForKey("mode").js === "show-read-notifications"; 62 | 63 | // GUARD 64 | if (!githubToken) { 65 | return JSON.stringify({ 66 | items: [ 67 | { 68 | title: "⚠️ No $GITHUB_TOKEN found.", 69 | subtitle: "Neither the Workflow Configuration nor the `.zshenv` have a token.", 70 | valid: false, 71 | }, 72 | ], 73 | }); 74 | } 75 | 76 | // CALL GITHUB API 77 | // DOCS https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#list-notifications-for-the-authenticated-user 78 | const apiUrl = "https://api.github.com/notifications?all=" + showReadNotifs.toString(); 79 | const headers = [ 80 | "Accept: application/vnd.github.json", 81 | "X-GitHub-Api-Version: 2022-11-28", 82 | `Authorization: BEARER ${githubToken}`, 83 | ]; 84 | const response = httpRequestWithHeaders(apiUrl, headers); 85 | if (!response) { 86 | return JSON.stringify({ 87 | items: [{ title: "No response from GitHub.", subtitle: "Try again later.", valid: false }], 88 | }); 89 | } 90 | const responseObj = JSON.parse(response); 91 | 92 | // GUARD error, for example invalid API token 93 | if (responseObj.message) { 94 | return JSON.stringify({ 95 | items: [{ title: responseObj.message, subtitle: "Error", valid: false }], 96 | }); 97 | } 98 | 99 | // GUARD: no notifications 100 | if (responseObj.length === 0) { 101 | const deactivatedMods = { 102 | cmd: { valid: false, subtitle: "" }, 103 | alt: { valid: false, subtitle: "" }, 104 | }; 105 | return JSON.stringify({ 106 | items: [ 107 | { 108 | title: "Show read notifications", 109 | variables: { mode: "show-read-notifications" }, 110 | mods: deactivatedMods, 111 | }, 112 | ], 113 | }); 114 | } 115 | 116 | //─────────────────────────────────────────────────────────────────────────── 117 | 118 | /** @type {Record} */ 119 | const typeMaps = { 120 | // biome-ignore lint/style/useNamingConvention: not by me 121 | PullRequest: "🟧", 122 | // biome-ignore lint/style/useNamingConvention: not by me 123 | Issue: "🔵", 124 | // biome-ignore lint/style/useNamingConvention: not by me 125 | Discussion: "🗣️", 126 | // biome-ignore lint/style/useNamingConvention: not by me 127 | CheckSuite: "🤖", 128 | // biome-ignore lint/style/useNamingConvention: not by me 129 | Release: "🎉", 130 | }; 131 | /** @type {Record} */ 132 | const reasonMaps = { 133 | author: "👤", 134 | mention: "⭕", 135 | // biome-ignore lint/style/useNamingConvention: not by me 136 | team_mention: "⭕", 137 | subscribed: "🔔", 138 | comment: "💬", 139 | assign: "➡", 140 | // biome-ignore lint/style/useNamingConvention: not by me 141 | ci_activity: "⚙️", 142 | invitation: "👥", 143 | manual: "🫱", 144 | // biome-ignore lint/style/useNamingConvention: not by me 145 | review_requested: "➡", 146 | // biome-ignore lint/style/useNamingConvention: not by me 147 | security_alert: "❗", 148 | // biome-ignore lint/style/useNamingConvention: not by me 149 | state_change: "❇️", 150 | }; 151 | 152 | /** @type AlfredItem[] */ 153 | const notifications = responseObj.map((/** @type {GithubNotif} */ notif) => { 154 | const apiUrl = notif.subject.latest_comment_url || notif.subject.url || ""; 155 | const typeIcon = typeMaps[notif.subject.type] || notif.subject.type; 156 | const reasonIcon = reasonMaps[notif.reason] || notif.reason; 157 | const updatedAt = humanRelativeDate(notif.updated_at); 158 | const subtitle = `${typeIcon} ${reasonIcon} ${notif.repository.name} · ${updatedAt}`; 159 | 160 | /** @type {AlfredItem} */ 161 | const alfredItem = { 162 | title: notif.subject.title, 163 | subtitle: subtitle, 164 | arg: apiUrl, 165 | variables: { mode: "open" }, 166 | mods: { 167 | cmd: { 168 | arg: notif.id, 169 | // CAVEAT mark-as-unread not support in GitHub Notification API 170 | valid: !showReadNotifs, 171 | subtitle: showReadNotifs ? "🚫 Is already marked as read." : "⌘: Mark as Read", 172 | variables: { mode: "mark-as-read", notificationsLeft: responseObj.length - 1 }, 173 | }, 174 | alt: { 175 | subtitle: apiUrl ? "⌥: Copy URL" : "(🚫 No URL)", 176 | valid: Boolean(apiUrl), 177 | variables: { mode: "copy" }, 178 | }, 179 | }, 180 | }; 181 | return alfredItem; 182 | }); 183 | 184 | return JSON.stringify({ items: notifications }); 185 | } 186 | -------------------------------------------------------------------------------- /scripts/my-github-issues.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | /** @param {string} str */ 8 | function alfredMatcher(str) { 9 | const clean = str.replace(/[-()_.:#/\\;,[\]]/g, " "); 10 | const camelCaseSeparated = str.replace(/([A-Z])/g, " $1"); 11 | return [clean, camelCaseSeparated, str].join(" "); 12 | } 13 | 14 | /** @param {string} url @return {string} */ 15 | function httpRequest(url) { 16 | const queryURL = $.NSURL.URLWithString(url); 17 | const data = $.NSData.dataWithContentsOfURL(queryURL); 18 | return $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js; 19 | } 20 | 21 | //────────────────────────────────────────────────────────────────────────────── 22 | 23 | // biome-ignore lint/correctness/noUnusedVariables: alfred_run 24 | function run() { 25 | const username = $.getenv("github_username"); 26 | 27 | // DOCS https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-issues-assigned-to-the-authenticated-user--parameters 28 | const apiURL = `https://api.github.com/search/issues?q=involves:${username}&sort=updated&per_page=100`; 29 | const response = httpRequest(apiURL); 30 | if (!response) { 31 | return JSON.stringify({ 32 | items: [{ title: "No response from GitHub.", subtitle: "Try again later.", valid: false }], 33 | }); 34 | } 35 | 36 | // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: okay here 37 | const issues = JSON.parse(response).items.map((/** @type {GithubIssue} */ item) => { 38 | const issueAuthor = item.user.login; 39 | const repo = (item.repository_url.match(/[^/]+$/) || "")[0]; 40 | const comments = item.comments > 0 ? "💬 " + item.comments.toString() : ""; 41 | const labels = item.labels.map((label) => `[${label.name}]`).join(" "); 42 | 43 | const subtitle = [`#${item.number}`, repo, comments.toString(), labels] 44 | .filter(Boolean) 45 | .join(" "); 46 | 47 | // ICON 48 | let icon = issueAuthor === username ? "✏️ " : ""; 49 | if (item.pull_request) { 50 | if (item.draft) icon += "⬜ "; 51 | else if (item.state === "open") icon += "🟩 "; 52 | else if (item.pull_request.merged_at) icon += "🟪 "; 53 | else icon += "🟥 "; 54 | } else { 55 | // biome-ignore lint/style/useCollapsedElseIf: here it's more readable this way 56 | if (item.state === "open") icon += "🟢 "; 57 | else if (item.state_reason === "not_planned") icon += "⚪ "; 58 | else if (item.state_reason === "completed") icon += "🟣 "; 59 | } 60 | 61 | let matcher = alfredMatcher(item.title) + " " + alfredMatcher(repo) + " " + item.state; 62 | if (item.pull_request) matcher += " pr"; 63 | else matcher += " issue"; 64 | if (item.draft) matcher += " draft"; 65 | 66 | return { 67 | title: icon + item.title, 68 | subtitle: subtitle, 69 | match: matcher, 70 | arg: item.html_url, 71 | quicklookurl: item.html_url, 72 | }; 73 | }); 74 | return JSON.stringify({ 75 | items: issues, 76 | cache: { 77 | seconds: 150, // fast to pick up recently created issues 78 | loosereload: true, 79 | }, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /scripts/my-github-prs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | /** @param {string} str */ 8 | function alfredMatcher(str) { 9 | const clean = str.replace(/[-()_.:#/\\;,[\]]/g, " "); 10 | const camelCaseSeparated = str.replace(/([A-Z])/g, " $1"); 11 | return [clean, camelCaseSeparated, str].join(" ") + " "; 12 | } 13 | 14 | /** 15 | * @param {string} url 16 | * @param {string[]} header 17 | * @return {string} response 18 | */ 19 | function httpRequestWithHeaders(url, header) { 20 | let allHeaders = ""; 21 | for (const line of header) { 22 | allHeaders += ` -H "${line}"`; 23 | } 24 | const curlRequest = `curl --silent --location ${allHeaders} "${url}" || true`; 25 | console.log("curl command:", curlRequest); 26 | return app.doShellScript(curlRequest); 27 | } 28 | 29 | /** @param {string} isoDateStr */ 30 | function humanRelativeDate(isoDateStr) { 31 | const deltaMins = (Date.now() - +new Date(isoDateStr)) / 1000 / 60; 32 | /** @type {"year"|"month"|"week"|"day"|"hour"|"minute"} */ 33 | let unit; 34 | let delta; 35 | if (deltaMins < 60) { 36 | unit = "minute"; 37 | delta = Math.floor(deltaMins); 38 | } else if (deltaMins < 60 * 24) { 39 | unit = "hour"; 40 | delta = Math.floor(deltaMins / 60); 41 | } else if (deltaMins < 60 * 24 * 7) { 42 | unit = "day"; 43 | delta = Math.floor(deltaMins / 60 / 24); 44 | } else if (deltaMins < 60 * 24 * 7 * 4) { 45 | unit = "week"; 46 | delta = Math.floor(deltaMins / 60 / 24 / 7); 47 | } else if (deltaMins < 60 * 24 * 7 * 4 * 12) { 48 | unit = "month"; 49 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4); 50 | } else { 51 | unit = "year"; 52 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4 / 12); 53 | } 54 | const formatter = new Intl.RelativeTimeFormat("en", { style: "narrow", numeric: "auto" }); 55 | const str = formatter.format(-delta, unit); 56 | return str.replace(/m(?= ago$)/, "min"); // "m" -> "min" (more distinguishable from "month") 57 | } 58 | 59 | //────────────────────────────────────────────────────────────────────────────── 60 | 61 | // biome-ignore lint/correctness/noUnusedVariables: alfred_run 62 | function run() { 63 | const username = $.getenv("github_username"); 64 | const tokenShellCmd = "test -e $HOME/.zshenv && source $HOME/.zshenv ; echo $GITHUB_TOKEN"; 65 | const githubToken = 66 | $.getenv("github_token_from_alfred_prefs").trim() || app.doShellScript(tokenShellCmd).trim(); 67 | 68 | const apiUrl = `https://api.github.com/search/issues?q=author:${username}+is:pr+is:open&per_page=100`; 69 | const headers = ["Accept: application/vnd.github.json", "X-GitHub-Api-Version: 2022-11-28"]; 70 | if (githubToken) headers.push(`Authorization: BEARER ${githubToken}`); 71 | 72 | const response = httpRequestWithHeaders(apiUrl, headers); 73 | if (!response) { 74 | return JSON.stringify({ 75 | items: [{ title: "No response from GitHub.", subtitle: "Try again later.", valid: false }], 76 | }); 77 | } 78 | 79 | const openPrs = JSON.parse(response).items.map((/** @type {GithubIssue} */ item) => { 80 | const repo = (item.repository_url.match(/[^/]+$/) || "")[0]; 81 | const comments = item.comments > 0 ? "💬 " + item.comments.toString() : ""; 82 | const icon = item.draft ? "⬜" : "🟩 "; 83 | 84 | const components = [ 85 | `#${item.number}`, 86 | repo, 87 | comments.toString(), 88 | `(${humanRelativeDate(item.created_at)})`, 89 | ]; 90 | const subtitle = components.filter(Boolean).join(" "); 91 | 92 | return { 93 | title: icon + item.title, 94 | subtitle: subtitle, 95 | match: alfredMatcher(item.title) + alfredMatcher(repo), 96 | arg: item.html_url, 97 | quicklookurl: item.html_url, 98 | }; 99 | }); 100 | return JSON.stringify({ 101 | items: openPrs, 102 | cache: { seconds: 150, loosereload: true }, // fast to pick up recently created prs 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /scripts/my-github-repos.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | const app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | //────────────────────────────────────────────────────────────────────────────── 6 | 7 | /** @param {string} str */ 8 | function alfredMatcher(str) { 9 | const clean = str.replace(/[-_.]/g, " "); 10 | const camelCaseSeparated = str.replace(/([A-Z])/g, " $1"); 11 | return [clean, camelCaseSeparated, str].join(" ") + " "; 12 | } 13 | 14 | /** 15 | * @param {string} url 16 | * @param {string[]} header 17 | * @return {string} response 18 | */ 19 | function httpRequestWithHeaders(url, header) { 20 | let allHeaders = ""; 21 | for (const line of header) { 22 | allHeaders += ` -H "${line}"`; 23 | } 24 | const curlRequest = `curl --silent --location ${allHeaders} "${url}" || true`; 25 | console.log("curl command:", curlRequest); 26 | return app.doShellScript(curlRequest); 27 | } 28 | 29 | /** @param {number} starcount */ 30 | function shortNumber(starcount) { 31 | const starStr = starcount.toString(); 32 | if (starcount < 2000) return starStr; 33 | return starStr.slice(0, -3) + "k"; 34 | } 35 | 36 | //────────────────────────────────────────────────────────────────────────────── 37 | 38 | /** @type {AlfredRun} */ 39 | // biome-ignore lint/correctness/noUnusedVariables: Alfred run 40 | function run() { 41 | // CONFIG 42 | const username = $.getenv("github_username"); 43 | const localRepoFolder = $.getenv("local_repo_folder"); 44 | const cloneDepth = Number.parseInt($.getenv("clone_depth")); 45 | const shallowClone = cloneDepth > 0; 46 | const tokenShellCmd = "test -e $HOME/.zshenv && source $HOME/.zshenv ; echo $GITHUB_TOKEN"; 47 | const githubToken = 48 | $.getenv("github_token_from_alfred_prefs").trim() || app.doShellScript(tokenShellCmd).trim(); 49 | const useAlfredFrecency = $.getenv("use_alfred_frecency") === "1"; 50 | 51 | // determine local repos 52 | /** @type {Record} */ 53 | const localRepos = {}; 54 | app.doShellScript(`mkdir -p "${localRepoFolder}"`); 55 | const localRepoPaths = app 56 | .doShellScript(`find ${localRepoFolder} -type d -maxdepth 2 -name ".git"`) 57 | .split("\r"); 58 | 59 | for (const gitFolderPath of localRepoPaths) { 60 | /** @type {{path: string; dirty: boolean|undefined}} */ 61 | const repo = {}; 62 | repo.path = gitFolderPath.replace(/\.git\/?$/, ""); 63 | const name = repo.path.replace(/.*\/(.*)\/$/, "$1"); 64 | try { 65 | repo.dirty = app.doShellScript(`cd "${repo.path}" && git status --porcelain`) !== ""; 66 | } catch (_error) { 67 | // error can occur with cloud sync issues 68 | repo.dirty = undefined; 69 | } 70 | localRepos[name] = repo; 71 | } 72 | 73 | //─────────────────────────────────────────────────────────────────────────── 74 | // FETCH REMOTE REPOS 75 | 76 | // DOCS https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-a-user 77 | let apiURL = `https://api.github.com/users/${username}/repos?type=all&per_page=100&sort=updated`; 78 | const headers = ["Accept: application/vnd.github.json", "X-GitHub-Api-Version: 2022-11-28"]; 79 | if (githubToken) { 80 | // DOCS https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user--parameters 81 | apiURL = "https://api.github.com/user/repos?per_page=100&sort=updated"; 82 | headers.push(`Authorization: BEARER ${githubToken}`); 83 | } 84 | 85 | const response = httpRequestWithHeaders(apiURL, headers); 86 | if (!response) { 87 | return JSON.stringify({ 88 | items: [{ title: "No response from GitHub.", subtitle: "Try again later.", valid: false }], 89 | }); 90 | } 91 | const parsedRepos = JSON.parse(response); 92 | console.log("Repo count:", parsedRepos.length); 93 | 94 | const repos = parsedRepos 95 | .filter((/** @type {GithubRepo} */ repo) => !repo.archived) // github API does now allow filtering when requesting 96 | .sort( 97 | ( 98 | /** @type {GithubRepo&{isLocal: boolean}} */ a, 99 | /** @type {GithubRepo&{isLocal: boolean}} */ b, 100 | ) => { 101 | a.isLocal = Boolean(localRepos[a.name]); 102 | b.isLocal = Boolean(localRepos[b.name]); 103 | if (a.isLocal && !b.isLocal) return -1; 104 | if (!a.isLocal && b.isLocal) return 1; 105 | return 0; // use sorting from GitHub (updated status) 106 | }, 107 | ) 108 | // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: fine here 109 | .map((/** @type {GithubRepo&{local: {path: string}|undefined}} */ repo) => { 110 | let matcher = alfredMatcher(repo.name); 111 | let type = ""; 112 | let subtitle = ""; 113 | repo.local = localRepos[repo.name]; 114 | const memberRepo = repo.owner.login !== username; 115 | const mainArg = repo.local?.path || repo.html_url; 116 | 117 | // open in terminal when local, clone when not 118 | let termAct = "Open in Terminal"; 119 | if (!repo.local) termAct = shallowClone ? `Shallow Clone (depth ${cloneDepth})` : "Clone"; 120 | const terminalArg = repo.local?.path || repo.html_url; 121 | if (repo.local) { 122 | if (localRepos[repo.name]?.dirty) type += "✴️ "; 123 | type += "📂 "; 124 | matcher += "local "; 125 | } 126 | 127 | // extra info 128 | if (repo.fork) type += "🍴 "; 129 | if (repo.fork) matcher += "fork "; 130 | if (repo.is_template) type += "📄 "; 131 | if (repo.is_template) matcher += "template "; 132 | if (repo.private) type += "🔒 "; 133 | if (repo.private) matcher += "private "; 134 | if (repo.stargazers_count > 0) subtitle += `⭐ ${shortNumber(repo.stargazers_count)} `; 135 | if (repo.open_issues > 0) subtitle += `🟢 ${repo.open_issues} `; 136 | if (repo.forks_count > 0) subtitle += `🍴 ${repo.forks_count} `; 137 | if (memberRepo) subtitle += `👤 ${repo.owner.login} `; 138 | if (memberRepo) matcher += "member "; 139 | 140 | /** @type {AlfredItem} */ 141 | const alfredItem = { 142 | title: `${type}${repo.name}`, 143 | subtitle: subtitle, 144 | match: matcher, 145 | arg: mainArg, 146 | quicklookurl: repo.private ? undefined : mainArg, 147 | uid: useAlfredFrecency ? repo.full_name : undefined, 148 | mods: { 149 | ctrl: { subtitle: "⌃: " + termAct, arg: terminalArg }, 150 | alt: { subtitle: "⌥: Copy GitHub URL", arg: repo.html_url }, 151 | cmd: { subtitle: "⌘: Open at GitHub", arg: repo.html_url }, 152 | }, 153 | }; 154 | return alfredItem; 155 | }); 156 | 157 | return JSON.stringify({ 158 | items: repos, 159 | variables: { ownerOfRepo: "true" }, 160 | cache: { seconds: 15, loosereload: true }, // short, since cloned repos should be available immediately 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /scripts/public-github-repo-search.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | ObjC.import("stdlib"); 3 | //────────────────────────────────────────────────────────────────────────────── 4 | 5 | /** @param {string} str */ 6 | function alfredMatcher(str) { 7 | const clean = str.replace(/[-_.]/g, " "); 8 | const camelCaseSeparated = str.replace(/([A-Z])/g, " $1"); 9 | return [clean, camelCaseSeparated, str].join(" ") + " "; 10 | } 11 | 12 | /** @param {string} url */ 13 | function httpRequest(url) { 14 | const queryURL = $.NSURL.URLWithString(url); 15 | const requestData = $.NSData.dataWithContentsOfURL(queryURL); 16 | return $.NSString.alloc.initWithDataEncoding(requestData, $.NSUTF8StringEncoding).js; 17 | } 18 | 19 | /** @param {string} isoDateStr */ 20 | function humanRelativeDate(isoDateStr) { 21 | const deltaMins = (Date.now() - +new Date(isoDateStr)) / 1000 / 60; 22 | /** @type {"year"|"month"|"week"|"day"|"hour"|"minute"} */ 23 | let unit; 24 | let delta; 25 | if (deltaMins < 60) { 26 | unit = "minute"; 27 | delta = Math.floor(deltaMins); 28 | } else if (deltaMins < 60 * 24) { 29 | unit = "hour"; 30 | delta = Math.floor(deltaMins / 60); 31 | } else if (deltaMins < 60 * 24 * 7) { 32 | unit = "day"; 33 | delta = Math.floor(deltaMins / 60 / 24); 34 | } else if (deltaMins < 60 * 24 * 7 * 4) { 35 | unit = "week"; 36 | delta = Math.floor(deltaMins / 60 / 24 / 7); 37 | } else if (deltaMins < 60 * 24 * 7 * 4 * 12) { 38 | unit = "month"; 39 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4); 40 | } else { 41 | unit = "year"; 42 | delta = Math.floor(deltaMins / 60 / 24 / 7 / 4 / 12); 43 | } 44 | const formatter = new Intl.RelativeTimeFormat("en", { style: "narrow", numeric: "auto" }); 45 | const str = formatter.format(-delta, unit); 46 | return str.replace(/m(?= ago$)/, "min"); // "m" -> "min" (more distinguishable from "month") 47 | } 48 | 49 | /** @param {number} starcount */ 50 | function shortNumber(starcount) { 51 | const starStr = starcount.toString(); 52 | if (starcount < 2000) return starStr; 53 | return starStr.slice(0, -3) + "k"; 54 | } 55 | 56 | //────────────────────────────────────────────────────────────────────────────── 57 | 58 | /** @type {AlfredRun} */ 59 | // biome-ignore lint/correctness/noUnusedVariables: Alfred run 60 | function run(argv) { 61 | const query = argv[0]; 62 | 63 | // GUARD 64 | if (!query) { 65 | return JSON.stringify({ items: [{ title: "Waiting for query…", valid: false }] }); 66 | } 67 | 68 | // DOCS https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories 69 | const apiURL = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}`; 70 | const response = httpRequest(apiURL); 71 | if (!response) { 72 | return JSON.stringify({ 73 | items: [{ title: "No response from GitHub.", subtitle: "Try again later.", valid: false }], 74 | }); 75 | } 76 | 77 | //─────────────────────────────────────────────────────────────────────────── 78 | 79 | const forkOnClone = $.getenv("fork_on_clone") === "1"; 80 | const cloneDepth = Number.parseInt($.getenv("clone_depth")); 81 | const shallowClone = cloneDepth > 0; 82 | 83 | /** @type {AlfredItem[]} */ 84 | const repos = JSON.parse(response) 85 | .items.filter((/** @type {GithubRepo} */ repo) => !(repo.fork || repo.archived)) 86 | .map((/** @type {GithubRepo} */ repo) => { 87 | // calculate relative date 88 | // INFO pushed_at refers to commits only https://github.com/orgs/community/discussions/24442 89 | // CAVEAT pushed_at apparently also includes pushes via PR :( 90 | const lastUpdated = repo.pushed_at ? humanRelativeDate(repo.pushed_at) : ""; 91 | 92 | const subtitle = [ 93 | repo.owner.login, 94 | "★ " + shortNumber(repo.stargazers_count), 95 | lastUpdated, 96 | repo.description, 97 | ] 98 | .filter(Boolean) 99 | .join(" · "); 100 | 101 | let cloneSubtitle = shallowClone ? `⌃: Shallow Clone (depth ${cloneDepth})` : "⌃: Clone"; 102 | if (forkOnClone) cloneSubtitle += " & Fork"; 103 | 104 | const secondUrl = repo.homepage || repo.html_url + "/releases"; 105 | 106 | return { 107 | title: repo.name, 108 | subtitle: subtitle, 109 | match: alfredMatcher(repo.name), 110 | arg: repo.html_url, 111 | quicklookurl: repo.html_url, 112 | mods: { 113 | cmd: { 114 | arg: secondUrl, 115 | subtitle: `⌘: Open "${secondUrl}"`, 116 | }, 117 | ctrl: { 118 | subtitle: cloneSubtitle, 119 | }, 120 | }, 121 | }; 122 | }); 123 | 124 | // GUARD no results 125 | if (repos.length === 0) { 126 | repos.push({ 127 | title: "🚫 No results", 128 | subtitle: `No results found for '${query}'`, 129 | valid: false, 130 | mods: { 131 | shift: { valid: false }, 132 | cmd: { valid: false }, 133 | alt: { valid: false }, 134 | ctrl: { valid: false }, 135 | }, 136 | }); 137 | } 138 | return JSON.stringify({ items: repos }); 139 | } 140 | -------------------------------------------------------------------------------- /scripts/resolve-notification.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | # shellcheck disable=2154 3 | 4 | # MARK AS READ 5 | if [[ "$mode" == "mark-as-read" ]]; then 6 | # DOCS https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#mark-a-thread-as-read 7 | thread_id="$1" 8 | curl -sL \ 9 | -X PATCH \ 10 | -H "Accept: application/vnd.github+json" \ 11 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 12 | -H "X-GitHub-Api-Version: 2022-11-28" \ 13 | "https://api.github.com/notifications/threads/$thread_id" 14 | return 0 15 | fi 16 | 17 | #─────────────────────────────────────────────────────────────────────────────── 18 | 19 | api_url="$1" 20 | # DOCS https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#get-a-thread 21 | if [[ -z "$api_url" && "$mode" == "open" ]]; then 22 | # some notification types like ci-activity do not provide a thread 23 | github_url="https://github.com/notifications" 24 | else 25 | response=$(curl -sL -H "Accept: application/vnd.github+json" \ 26 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 27 | -H "X-GitHub-Api-Version: 2022-11-28" \ 28 | "$api_url") 29 | # using `grep` here avoids `jq` dependency 30 | github_url=$(echo "$response" | grep --max-count=1 '"html_url"' | cut -d '"' -f4) 31 | 32 | # VALIDATE 33 | if [[ -z "$github_url" && "$mode" == "open" ]]; then 34 | # some notification types like ci-activity do not provide a thread 35 | github_url="https://github.com/notifications" 36 | fi 37 | fi 38 | 39 | if [[ "$mode" == "open" ]]; then 40 | open "$github_url" 41 | if [[ "$github_url" == "https://github.com/notifications" ]]; then 42 | echo "Opening notification inbox at GitHub instead." 43 | return 1 44 | fi 45 | elif [[ "$mode" == "copy" ]]; then 46 | if [[ -z "$github_url" ]]; then 47 | echo "Error: No URL found for notification." 48 | return 1 49 | fi 50 | echo -n "$github_url" | pbcopy 51 | echo -n "$github_url" # pass for Alfred notification 52 | fi 53 | --------------------------------------------------------------------------------