├── .github └── workflows │ └── quality_check.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── gh-look └── readme.md /.github/workflows/quality_check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Quality check 3 | on: 4 | push: 5 | pull_request_target: 6 | workflow_dispatch: 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v4 13 | - name: Spell Check 14 | # https://github.com/crate-ci/typos 15 | uses: crate-ci/typos@master 16 | - name: Shell and shfmt Check 17 | # https://github.com/luizm/action-sh-checker 18 | uses: luizm/action-sh-checker@master 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | SHELLCHECK_OPTS: --external-sources --norc 22 | SHFMT_OPTS: --simplify --indent 0 --case-indent 23 | with: 24 | sh_checker_comment: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .trashme 2 | 3 | # Vale style folders 4 | .github/vale_styles/* 5 | !.github/vale_styles/Vocab 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default_stages: [pre-commit] 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.6.0 6 | hooks: 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: https://github.com/adhtruong/mirrors-typos 10 | rev: v1.24.6 11 | hooks: 12 | - id: typos 13 | args: [--format, brief, --write-changes, --force-exclude] 14 | - repo: https://github.com/tcort/markdown-link-check 15 | rev: v3.12.2 16 | hooks: 17 | - id: markdown-link-check 18 | name: markdown link check 19 | args: [--quiet] 20 | - repo: https://github.com/shellcheck-py/shellcheck-py 21 | rev: v0.10.0.1 22 | hooks: 23 | - id: shellcheck 24 | args: [--external-source, --norc] 25 | - repo: https://github.com/scop/pre-commit-shfmt 26 | rev: v3.9.0-1 27 | hooks: 28 | - id: shfmt 29 | args: [--simplify, --indent, '0', --case-indent, --write] 30 | - repo: https://github.com/lyz-code/yamlfix 31 | rev: 1.17.0 32 | hooks: 33 | - id: yamlfix 34 | - repo: https://github.com/adhtruong/mirrors-typos 35 | rev: v1.24.6 36 | hooks: 37 | - id: typos 38 | name: detect typos in commit message 39 | args: [--format, brief] 40 | stages: [commit-msg] 41 | - repo: https://github.com/jorisroovers/gitlint 42 | rev: v0.19.1 43 | hooks: 44 | - id: gitlint 45 | name: validate commit message format 46 | args: [--ignore, body-is-missing, --msg-filename] 47 | stages: [commit-msg] 48 | - repo: https://github.com/commitizen-tools/commitizen 49 | rev: v3.29.0 50 | hooks: 51 | - id: commitizen 52 | name: check commit message structure 53 | stages: [commit-msg] 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /gh-look: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit -o nounset -o pipefail 3 | # https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin 4 | 5 | # <------------------- TODOs -------------------- > 6 | 7 | # TODO: the statement [[...]] && ... || ... is a typical bash pitfall case, if the command after && fails the command after || gets executed as well (it should not); use a simple if ... else ... statement 8 | # https://mywiki.wooledge.org/BashPitfalls#cmd1_.26.26_cmd2_.7C.7C_cmd3 9 | # TODO: When making a emoji reaction let the user choose from a list of valid emojis, currently it only doees on start up 10 | # TODO: If there is a merge event on a PR, don't display the "close" event as well https://pkg.go.dev/text/template 11 | # TODO: sometimes when searching for a popular repo like "vscode, tsx,..." it does not give the result on the first try and you have to reload with "ctrl+r". INVESTIGATE, why this is! 12 | # TODO: disable comments that are marked as spam, use "isMinimized" or "minimizedReason" 13 | # TODO: find a better way to manage the help section for each command, currently too much duplication. 14 | # TODO: run tests on the script - https://github.com/dodie/testing-in-bash 15 | # TODO: allow the user to set their hotkeys in a config file 16 | # TODO: when the user sets the "-o" flag, let the user choose from a list of valid sorting options with fzf 17 | # TODO: add a hotkey to allow switching between page results, page 1, page 2, ... page n 18 | # TODO: toggle the reaction with a single hotkey, first check if there has been an reaction with the current selected emoji, if not make a reaction, if there has been undo the reaction 19 | # TODO: add option to subscribe to a specific issue/PR, must visulaize that I am subscribed to it, check GraphQL API 'updateSubscription' (mutation)/ 'viewerSubscription'(query). 20 | # TODO: adopt shell completion, for example: https://github.com/molovo/revolver/blob/master/revolver.zsh-completion 21 | # TODO: use 'export -f' to make functions available in child processes, or use "set -a" https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin 22 | 23 | # <------------------- DOCUMENTATION -------------------- > 24 | 25 | # writing GitHub GraphQL queries 26 | # https://docs.github.com/en/graphql 27 | # modify GO templates 28 | # https://pkg.go.dev/text/template 29 | 30 | # <------------------- VARIABLES -------------------- > 31 | 32 | SORTING_ORDER="" 33 | OWNER_REPO_NAME="" 34 | USER_NAME="" 35 | PREVIEW_WINDOW_VISIBILITY="hidden" 36 | INITIAL_QUERY="" 37 | INITIAL_COMMENT_QUERY="" 38 | OUTPUT_SELECTION="" 39 | # resetting the '-F' option to its default setting with '-+F' 40 | LANG_BAT_SETUP="bat --pager 'less --clear-screen -+F -+X -R' --paging always --color always --style plain --language" 41 | 42 | # for better speed 43 | CACHE_TIME="20s" 44 | # minimum required fzf version, using "become(...)" 45 | # https://github.com/junegunn/fzf/releases 46 | MIN_FZF_VERSION="0.38.0" 47 | # workflow runs 48 | UPDATE_TIME=10 49 | EXCLUDE_PULL_REQUESTS=true 50 | NUMBER_WORKFLOW_RUN_LIST=10 51 | # colors 52 | COLOR_RESET=$(tput sgr0) 53 | RED_NORMAL=$(tput setaf 1) 54 | GREEN_NORMAL=$(tput setaf 2) 55 | YELLOW_NORMAL=$(tput setaf 3) 56 | BLUE_NORMAL=$(tput setaf 4) 57 | WHITE_BOLD=$( 58 | tput bold 59 | tput setaf 7 60 | ) 61 | 62 | # emojis 63 | REACTION_EMOJI="THUMBS_UP" 64 | valid_emojis=("👍:THUMBS_UP" "👎:THUMBS_DOWN" "😄:LAUGH" "🎉:HOORAY" "😕:CONFUSED" "💖:HEART" "🚀:ROCKET" "👀:EYES") 65 | # GH_TOGGLE_EMOJI_REACTION=$'gh api graphql --raw-field node_id={1} --raw-field query=\'query ($node_id: ID!) { node(id: $node_id) { ... on Reactable { reactionGroups { viewerHasReacted content }}}}\' --jq \'.data.node.reactionGroups[] | select(.viewerHasReacted) | .content\''" | while IFS= read -r line; do if grep -q \"$line\" <<<\"$REACTION_EMOJI\"; then echo true && break; else echo false; fi; done | tail -1" 66 | GH_ADD_REACTION=$'gh api graphql --silent --raw-field query=\'mutation($id: ID! $emoji: ReactionContent! ) { addReaction(input: {subjectId: $id content: $emoji }) { clientMutationId }}\'' 67 | GH_REMOVE_REACTION=$'gh api graphql --silent --raw-field query=\'mutation($id: ID! $emoji: ReactionContent! ) { removeReaction(input: {subjectId: $id content: $emoji }) { clientMutationId }}\'' 68 | 69 | # <--------------- HELPER FUNCTIONS ---------------- > 70 | 71 | _die_with_octocat() { 72 | GH_PAGER="cat" gh api octocat 73 | printf "%s[%s] < ERROR > %s%s" "$RED_NORMAL" "$(date +"%H:%M:%S")" "${1:?"No error message has been defined."}" "$COLOR_RESET" 74 | printf "\n%s\n" "${2-}" 75 | exit 1 76 | } 77 | 78 | # for comparing multi-digit version numbers https://apple.stackexchange.com/a/123408/11374 79 | _version_number() { 80 | echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' 81 | } 82 | 83 | # Here options are defined that are common to all commands. 84 | # https://github.com/junegunn/fzf-git.sh/blob/main/fzf-git.sh#L104 85 | _fzf_basic_options() { 86 | # Hint: tablerow truncates the table columns to fit the entire table in the current terminal window. 87 | # The workaround to avoid the truncation is to force a really wide terminal with GH_FORCE_TTY=10000. 88 | # IMPORTANT: anything after "$@" will overwrite options in the actual command 89 | SHELL="bash" GH_FORCE_TTY=10000 fzf -- \ 90 | --header '' \ 91 | --delimiter '\s+' \ 92 | --color 'header:regular' \ 93 | "$@" \ 94 | --ansi --layout reverse --info inline --no-multi --height 100% \ 95 | --ellipsis '' --border horizontal \ 96 | --no-separator --print-query \ 97 | --preview-window "$PREVIEW_WINDOW_VISIBILITY:wrap:right:65%:border-left" \ 98 | --bind 'btab:change-preview-window(50%:nohidden|65%:down:nohidden:border-top|nohidden)' 99 | } 100 | 101 | # More ideas: https://raw.githubusercontent.com/sindresorhus/cli-spinners/master/spinners.json 102 | SPIN_FORM=(▰▱▱▱▱▱▱ ▰▰▱▱▱▱▱ ▰▰▰▱▱▱▱ ▰▰▰▰▱▱▱ ▰▰▰▰▰▱▱ ▰▰▰▰▰▰▱ ▰▰▰▰▰▰▰) 103 | # Restarting fzf immediately may not show the newly created changes. Wait at least a second or two. 104 | _load_indicator() { 105 | # Save the current terminal state 106 | tput smcup 107 | # Save cursor position 108 | tput sc 109 | for i in "${SPIN_FORM[@]}"; do 110 | # Restore cursor position to the saved position 111 | tput rc 112 | printf "%s %s" "${1:-"Restarting"}" "$i" 113 | sleep "${2-"0.2"}" 114 | done 115 | # Restore the terminal state 116 | tput rmcup 117 | } 118 | 119 | # confirm user decisions and return an exit status based on the user's response 120 | _user_confirm() { 121 | local confirm 122 | tput smcup 123 | printf "%s\n\n${WHITE_BOLD}CONFIRM${COLOR_RESET} %s (${GREEN_NORMAL}y${COLOR_RESET}/${RED_NORMAL}n${COLOR_RESET})?\n> " "${1:-""}" "${2-""}" 124 | stty raw -echo 125 | confirm=$(dd bs=1 count=1 2>/dev/null) 126 | stty sane 127 | tput rmcup 128 | [[ $confirm == "y" ]] && return 0 || return 1 129 | } 130 | 131 | # return the emoji from its corresponding string name 132 | _emoji_from_name() { 133 | for emoji in "${valid_emojis[@]}"; do 134 | if [[ ${emoji#*:} == "$1" ]]; then 135 | echo "${emoji%%:*}" 136 | break 137 | fi 138 | done 139 | } 140 | 141 | # choose a valid emoji from a list 142 | _emoji_picker() { 143 | for emoji in "${valid_emojis[@]}"; do 144 | echo "${emoji%%:*} ${emoji#*:}" 145 | done | 146 | _fzf_basic_options \ 147 | --header "Pick an emoji and hit enter" \ 148 | --bind 'enter:become:echo {2}' \ 149 | --delimiter ' ' 150 | } 151 | 152 | # <--------------- FUNCTIONS ---------------- > 153 | 154 | help_general_function() { 155 | cat <${COLOR_RESET} List Issues from current directory 177 | ${GREEN_NORMAL}-c ${COLOR_RESET} Cache the response, for example "30s", "15m", "1h" (default: 20s) 178 | ${GREEN_NORMAL}-e ${COLOR_RESET} Emoji to make a reaction (default: 👍) 179 | ${GREEN_NORMAL}-h ${COLOR_RESET} Help 180 | ${GREEN_NORMAL}-o ${COLOR_RESET} sorting order of Issues (default: created-desc) 181 | ${GREEN_NORMAL}-r ${COLOR_RESET} Specify a repository (form: OWNER/REPO) 182 | ${GREEN_NORMAL}-w ${COLOR_RESET} Display the preview window upon start (default: hidden) 183 | 184 | ${WHITE_BOLD}Symbols${COLOR_RESET} 185 | 💬 Total number of comments (gray commentable; red locked) 186 | 📣 Total number of emojis (green reactable; yellow reacted; red locked) 187 | 188 | ${WHITE_BOLD}Hotkeys${COLOR_RESET} 189 | ${GREEN_NORMAL}? ${COLOR_RESET} Toggle help 190 | ${GREEN_NORMAL}enter ${COLOR_RESET} See comments 191 | ${GREEN_NORMAL}tab ${COLOR_RESET} Toggle preview 192 | ${GREEN_NORMAL}shift+tab ${COLOR_RESET} Change preview window size 193 | ${GREEN_NORMAL}ctrl+a ${COLOR_RESET} ALL Issues 194 | ${GREEN_NORMAL}ctrl+b ${COLOR_RESET} Browser 195 | ${GREEN_NORMAL}ctrl+e ${COLOR_RESET} Edit an Issue 196 | ${GREEN_NORMAL}ctrl+f ${COLOR_RESET} Fuzzy search 197 | ${GREEN_NORMAL}ctrl+g ${COLOR_RESET} Close/Reopen Issue after confirmation 198 | ${GREEN_NORMAL}ctrl+n ${COLOR_RESET} New Issue 199 | ${GREEN_NORMAL}ctrl+o ${COLOR_RESET} OPEN Issues only 200 | ${GREEN_NORMAL}ctrl+p ${COLOR_RESET} Put "involves:@me" into the search 201 | ${GREEN_NORMAL}ctrl+r ${COLOR_RESET} Reload 202 | ${GREEN_NORMAL}ctrl+t ${COLOR_RESET} React with $(_emoji_from_name "$REACTION_EMOJI") emoji 203 | ${GREEN_NORMAL}ctrl+u ${COLOR_RESET} Undo the $(_emoji_from_name "$REACTION_EMOJI") emoji reaction 204 | ${GREEN_NORMAL}ctrl+x ${COLOR_RESET} Write a comment 205 | ${GREEN_NORMAL}shift+left ${COLOR_RESET} Switch to Search 206 | ${GREEN_NORMAL}shift+right${COLOR_RESET} Switch to Pull Requests 207 | ${GREEN_NORMAL}esc ${COLOR_RESET} Quit 208 | EOF 209 | } 210 | 211 | fzf_issue_function() { 212 | GH_ISSUE_PREVIEW=$'gh api graphql --paginate --raw-field id={1} --raw-field query=\'query($id: ID!, $endCursor: String){node(id: $id) { ... on Issue { author { login } body createdAt comments(first: 100) { totalCount nodes { author { login } body createdAt viewerDidAuthor reactionGroups { content reactors(last: 4) { totalCount nodes { ... on Actor { login }}}}}} timelineItems(itemTypes: [CLOSED_EVENT, ISSUE_COMMENT, REOPENED_EVENT], first: 100, after: $endCursor) { nodes { ... on ClosedEvent { actor { login } closer { ... on PullRequest { baseRepository { nameWithOwner } number }} createdAt stateReason } ... on IssueComment { author { login } body createdAt viewerDidAuthor reactionGroups { content reactors(last: 4) { totalCount nodes { ... on Actor { login }}}} reactionGroups { content viewerHasReacted reactors(last: 4) { nodes { ... on Actor { login }} totalCount }}} ... on ReopenedEvent { actor { login } createdAt stateReason }} pageInfo { hasNextPage endCursor }} labels(first:10) { nodes { name } } number reactionGroups { content viewerHasReacted reactors(last: 4) { nodes { ... on Actor { login }} totalCount }} state title viewerDidAuthor }}}\' --template \'{{- $prefix := .data.node -}}{{- $stateIssueColor := "green+b" -}}{{- if eq $prefix.state "CLOSED" -}}{{- $stateIssueColor = "93+b" -}}{{- end -}}{{- $issueAuthor := "[DELETED_USER]" -}}{{- if $prefix.author -}}{{- $issueAuthor = $prefix.author.login -}}{{- end -}}{{- $authorColor := "cyan+hb" -}}{{- if $prefix.viewerDidAuthor -}}{{- $authorColor = "yellow+bh" -}}{{- end -}}{{- tablerow ($prefix.title | color "white+bh") -}}{{- tablerender -}}{{- tablerow (printf "%s ◉ #%.0f" $prefix.state $prefix.number | color $stateIssueColor) ($issueAuthor | color $authorColor) (printf "│ %s ∙ %.0f Comments │" (timeago $prefix.createdAt) $prefix.comments.totalCount | color "gray+h") -}}{{- tablerender -}}{{- if $prefix.labels.nodes -}}{{- tablerow ("Labels:" | color "white+b") (pluck "name" $prefix.labels.nodes | join ", " | printf "[ %s ]" | color "white") -}}{{- tablerender -}}{{- end -}}{{- range $prefix.reactionGroups -}}{{ $emoji := .content }}{{ if eq .content "CONFUSED" }}{{ $emoji = "😕" }}{{ else if eq .content "EYES" }}{{ $emoji = "👀" }}{{ else if eq .content "HEART" }}{{ $emoji = "💖" }}{{ else if eq .content "HOORAY" }}{{ $emoji = "🎉" }}{{ else if eq .content "LAUGH" }}{{ $emoji = "😄" }}{{ else if eq .content "THUMBS_DOWN" }}{{ $emoji = "👎" }}{{ else if eq .content "THUMBS_UP" }}{{ $emoji = "👍" }}{{ else if eq .content "ROCKET" }}{{ $emoji = "🚀" }}{{ end }}{{ if gt .reactors.totalCount 0.0 }}{{ $emojiGiver := pluck "login" .reactors.nodes | join ", " | color "green+d" }}{{ if gt (pluck "login" .reactors.nodes | len ) 3 }}{{- /* NOTE: number of names limited */ -}}{{ $emojiGiver = slice (pluck "login" .reactors.nodes) 1 | join ", " | printf "%s, ..." | color "green+d" }}{{end}}{{- $emojiTotal := .reactors.totalCount | color "green+bd" -}}{{- if .viewerHasReacted -}}{{- $emojiTotal = .reactors.totalCount | color "yellow+bh" -}}{{- end -}}{{- tablerow (printf "%s %s" $emojiTotal $emoji) $emojiGiver -}}{{- tablerender -}}{{- end -}}{{- end }}{{- tablerow ("——————————————————————————————————————————————————————————"| color "gray+d") -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- $bodyText := $prefix.body }}{{ if eq ($bodyText | len) 0 -}}{{- $bodyText = " No description provided. " | color "white+i" -}}{{- end -}}{{- $bodyText -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- range $prefix.timelineItems.nodes -}}{{- $actorTimeline := "[DELETED_USER]" -}}{{- if .actor -}}{{- $actorTimeline = .actor.login -}}{{- end -}}{{- $authorTimelineColor := "cyan+hb" -}}{{- if .viewerDidAuthor -}}{{- $authorTimelineColor = "yellow+bh" -}}{{- end -}}{{- if eq .stateReason "COMPLETED" "NOT_PLANNED" -}}{{- $stateClosedColor := "93+b" -}}{{- if eq .stateReason "NOT_PLANNED" -}}{{- $stateClosedColor = "gray+hb" -}}{{- end -}}{{- $linkedPR := "" -}}{{- if .closer -}}{{- $linkedPR = (printf "%s #%v" .closer.baseRepository.nameWithOwner .closer.number | color "blue+hb") -}}{{- end -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color $stateClosedColor) -}}{{- tablerender -}}{{- tablerow (printf "%s closed this as %s %s %s" ($actorTimeline | color $authorTimelineColor) (.stateReason | color $stateClosedColor) (timeago .createdAt | color "white+bh") $linkedPR) -}}{{- tablerender -}}{{- tablerow ("◢■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◣"| color $stateClosedColor) -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- else if eq .stateReason "REOPENED" -}}{{- $stateReopenedColor := "green+b" -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color $stateReopenedColor) -}}{{- tablerender -}}{{- tablerow (printf "%s %s this Issue %s" ($actorTimeline | color $authorTimelineColor) (.stateReason | color $stateReopenedColor) (timeago .createdAt | color "white+bh")) -}}{{- tablerender -}}{{- tablerow ("◢■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◣"| color $stateReopenedColor) -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- else -}}{{- $authorTimeline := "[DELETED_USER]" -}}{{- if .author -}}{{- $authorTimeline = .author.login -}}{{- end -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color "gray+d") -}}{{- tablerender -}}{{- tablerow ($authorTimeline | color $authorTimelineColor) ( timeago .createdAt | color "white+bh") -}}{{- tablerender -}}{{- range .reactionGroups -}}{{ $emojiInComment := .content }}{{ if eq .content "CONFUSED" }}{{ $emojiInComment = "😕" }}{{ else if eq .content "EYES" }}{{ $emojiInComment = "👀" }}{{ else if eq .content "HEART" }}{{ $emojiInComment = "💖" }}{{ else if eq .content "HOORAY" }}{{ $emojiInComment = "🎉" }}{{ else if eq .content "LAUGH" }}{{ $emojiInComment = "😄" }}{{ else if eq .content "THUMBS_DOWN" }}{{ $emojiInComment = "👎" }}{{ else if eq .content "THUMBS_UP" }}{{ $emojiInComment = "👍" }}{{ else if eq .content "ROCKET" }}{{ $emojiInComment = "🚀" }}{{ end }}{{ if gt .reactors.totalCount 0.0 }}{{ $emojiInCommentGiver := pluck "login" .reactors.nodes | join ", " | color "green+d" }}{{ if gt (pluck "login" .reactors.nodes | len ) 3 }}{{- /* NOTE: number of names limited */ -}}{{ $emojiInCommentGiver = slice (pluck "login" .reactors.nodes) 1 | join ", " | printf "%s, ..." | color "green+d" }}{{end}}{{- $emojiInCommentTotal := .reactors.totalCount | color "green+bd" -}}{{- if .viewerHasReacted -}}{{- $emojiInCommentTotal = .reactors.totalCount | color "yellow+bh" -}}{{- end -}}{{- tablerow (printf "%s %s" $emojiInCommentTotal $emojiInComment) $emojiInCommentGiver -}}{{- tablerender -}}{{- end -}}{{- end -}}{{- tablerow ("‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥"| color "gray+d") -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- .body -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- end -}}{{- end -}}\'' 213 | 214 | # fzf allows hiding columns "--with-nth 2...", headers are also affected, so NODE_ID_HIDDEN is included there 215 | GH_ISSUE_COMMAND=$'gh api graphql --raw-field query=\'query($filter: String!) {search(query: $filter, type: ISSUE, first: 35) {issueCount nodes { ... on Issue { author { login } comments {totalCount} id number updatedAt reactions { totalCount viewerHasReacted } state title viewerCanReact viewerDidAuthor }}}}\' --template \' 216 | {{- $limitCount := .data.search.issueCount -}} 217 | {{- if gt .data.search.issueCount 35.0 -}}{{- $limitCount = 35.0 -}}{{- end -}} 218 | {{- tablerow "NODE_ID_HIDDEN" (printf "%.0f of ∑ %.0f" $limitCount .data.search.issueCount | color "blue+b") ("|" | color "white+d") ("? Help · esc Quit" | color "blue") -}}{{- tablerender -}} 219 | {{- tablerow "" -}}{{- tablerender -}} 220 | {{- $headerColor := "blue+b" -}} 221 | {{- tablerow "NODE_ID_HIDDEN" ("ISSUE" | color $headerColor) ("AUTHOR" | color $headerColor) ("LAST UPDATE" | color $headerColor) "💬" "📣" ("TITLE" | color $headerColor) -}} 222 | {{- range .data.search.nodes -}} 223 | {{- $stateIssueColor := "green" -}} 224 | {{- if eq .state "CLOSED" -}}{{- $stateIssueColor = "93" -}}{{- end -}} 225 | {{- $author := "[DELETED_USER]" -}} 226 | {{- if .author -}}{{- $author = .author.login -}}{{- end -}} 227 | {{- $authorColor := "cyan+h" -}} 228 | {{- if .viewerDidAuthor -}}{{- $authorColor = "yellow" -}}{{- end -}} 229 | {{- $commentCount := "\u00a0" -}}{{- /* NOTE: U+00a0 (non-breaking space) for entries without comments, which the viewer can comment on. Needed to make a if decision with fzf later in the script. */ -}} 230 | {{- if not .viewerCanReact -}}{{- $commentCount = (or (printf "%.0f" .comments.totalCount) "0") -}} 231 | {{- else if .comments.totalCount -}}{{- $commentCount = printf "%.0f" .comments.totalCount -}} 232 | {{- end -}} 233 | {{- $commentColor := "white+bd" -}} 234 | {{- if not .viewerCanReact -}}{{- $commentColor = "red+bd" -}}{{- end -}} 235 | {{- $emojiCount := "" -}} 236 | {{- if not .viewerCanReact -}}{{- $emojiCount = (or (printf "%.0f" .reactions.totalCount) "0") -}} 237 | {{- else if .reactions.totalCount -}}{{- $emojiCount = printf "%.0f" .reactions.totalCount -}} 238 | {{- end -}} 239 | {{- $emojiColor := "green+bd" -}} 240 | {{- if not .viewerCanReact -}}{{- $emojiColor = "red+bd" -}} 241 | {{- else if .reactions.viewerHasReacted -}}{{- $emojiColor = "yellow+b" -}} 242 | {{- end -}} 243 | {{- tablerow .id (printf "#%.0f" .number | color $stateIssueColor) ($author | color $authorColor) (timeago .updatedAt) ($commentCount | color $commentColor) ($emojiCount | color $emojiColor) .title -}} 244 | {{- end -}}\'' 245 | 246 | INITIAL_ISSUE_PROMPT=$(grep -q A <"$SCRATCH_FILE" && echo "Issues ◉ [$OWNER_REPO_NAME] > " || echo "${GREEN_NORMAL}OPEN Issues ◉ [$OWNER_REPO_NAME] > ") 247 | 248 | OUTPUT_SELECTION=$(FZF_DEFAULT_COMMAND="(grep -q A <$SCRATCH_FILE && $GH_ISSUE_COMMAND --cache ${1:-$CACHE_TIME} --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME $INITIAL_QUERY \" || $GH_ISSUE_COMMAND --cache ${1:-$CACHE_TIME} --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open $INITIAL_QUERY \") || true" \ 249 | _fzf_basic_options --disabled --header-lines 3 --with-nth 2.. \ 250 | --prompt "$INITIAL_ISSUE_PROMPT" \ 251 | --query "$INITIAL_QUERY" --preview "sleep 0.35; $GH_ISSUE_PREVIEW | $LANG_BAT_SETUP md" \ 252 | --bind "change:first+reload-sync:sleep 0.25; (grep -q A <$SCRATCH_FILE && $GH_ISSUE_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME \"{q} || $GH_ISSUE_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 253 | --bind "?:toggle-preview+change-preview:echo '$(help_issue_function)'" \ 254 | --bind "tab:toggle-preview+change-preview:sleep 0.35; $GH_ISSUE_PREVIEW | $LANG_BAT_SETUP md" \ 255 | --bind "ctrl-a:rebind(change)+change-prompt(Issues ◉ [$OWNER_REPO_NAME] > )+disable-search+execute-silent(echo A >$SCRATCH_FILE)+reload-sync:$GH_ISSUE_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME \"{q} || true" \ 256 | --bind "ctrl-b:execute-silent(gh issue view {2} --web --repo $OWNER_REPO_NAME)" \ 257 | --bind "ctrl-f:unbind(change)+enable-search+clear-query+change-prompt(${RED_NORMAL}FUZZY SEARCH [$OWNER_REPO_NAME] > )" \ 258 | --bind "ctrl-o:rebind(change)+change-prompt(${GREEN_NORMAL}OPEN Issues ◉ [$OWNER_REPO_NAME] > )+disable-search+execute-silent(echo B >$SCRATCH_FILE)+reload-sync:$GH_ISSUE_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open \"{q} || true" \ 259 | --bind "ctrl-p:put(involves:@me)" \ 260 | --bind "ctrl-r:reload-sync:(grep -q A <$SCRATCH_FILE && $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME \"{q} || $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 261 | --bind "ctrl-t:execute-silent($GH_ADD_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:(grep -q A <$SCRATCH_FILE && $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME \"{q} || $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 262 | --bind "ctrl-u:execute-silent($GH_REMOVE_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:(grep -q A <$SCRATCH_FILE && $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME \"{q} || $GH_ISSUE_COMMAND --raw-field filter=\"$SORTING_ORDER type:issue repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 263 | --expect "ctrl-e,ctrl-g,ctrl-n,ctrl-x,enter,shift-left,shift-right") || true 264 | 265 | printed_query="$(sed 1q <<<"$OUTPUT_SELECTION")" 266 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 267 | TICKET_NUMBER="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $2}' | tr -d "#")" 268 | output_textline="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{$1=""; sub("^ *",""); print $0}')" 269 | output_id="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $1}')" 270 | number_comments="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $6,$7}' | grep -Eo '[0-9]+' || true)" 271 | # reset the variable 272 | OUTPUT_SELECTION="" 273 | case "$expected_key" in 274 | ctrl-e) 275 | gh issue edit "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 276 | _load_indicator 277 | INITIAL_QUERY="$printed_query" 278 | fzf_issue_function 0s 279 | ;; 280 | ctrl-g) 281 | check_info_api=() 282 | # "[0]" is the ticket closed ? 283 | # "[1]" can the viewer close it ? 284 | while IFS='' read -r line; do check_info_api+=("$line"); done < <( 285 | gh api graphql --raw-field id="$output_id" \ 286 | --raw-field query=$'query ($id: ID!) { node(id: $id) { ... on Issue { closed viewerCanClose }}}' \ 287 | --jq '.data.node | .closed,.viewerCanClose' 288 | ) 289 | if [ "${check_info_api[0]}" = "true" ]; then 290 | if [ "${check_info_api[1]}" = "true" ]; then 291 | if _user_confirm "$output_textline" "reopen this issue"; then 292 | if _user_confirm "$output_textline" "do you want to add a comment"; then 293 | tput sc 294 | echo -en "${WHITE_BOLD}Enter a comment!${COLOR_RESET}\n> " 295 | read -r message 296 | tput rc && tput el 297 | gh issue reopen "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" --comment "$message" &>/dev/null 298 | else 299 | gh issue reopen "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" &>/dev/null 300 | fi 301 | _load_indicator $'🟢 Success.... Issue reopened!\nRestarting' 302 | else 303 | _load_indicator $'Discarding... Issue was not reopened!\nRestarting' 304 | fi 305 | else 306 | _load_indicator $'🛑 Missing permissions to reopen this Issue!\nRestarting' 307 | fi 308 | else 309 | if [ "${check_info_api[1]}" = "true" ]; then 310 | if _user_confirm "$output_textline" "close this issue"; then 311 | if _user_confirm "$output_textline" "do you want to add a comment"; then 312 | tput sc 313 | echo -en "${WHITE_BOLD}Enter a comment!${COLOR_RESET}\n> " 314 | read -r message 315 | tput rc && tput el 316 | gh issue close "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" --comment "$message" &>/dev/null 317 | else 318 | gh issue close "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" &>/dev/null 319 | fi 320 | _load_indicator $'🟢 Success.... Issue closed!\nRestarting' 321 | else 322 | _load_indicator $'Discarding... Issue was not closed!\nRestarting' 323 | fi 324 | else 325 | _load_indicator $'🛑 Missing permissions to close this Issue!\nRestarting' 326 | fi 327 | fi 328 | INITIAL_QUERY="$printed_query" 329 | fzf_issue_function 0s 330 | ;; 331 | ctrl-n) 332 | gh issue create --repo "$OWNER_REPO_NAME" || true 333 | _load_indicator 334 | INITIAL_QUERY="$printed_query" 335 | fzf_issue_function 0s 336 | ;; 337 | ctrl-x) 338 | gh issue comment "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 339 | _load_indicator 340 | INITIAL_QUERY="$printed_query" 341 | fzf_issue_function 0s 342 | ;; 343 | enter) 344 | INITIAL_QUERY="$printed_query" 345 | if [ -z "$number_comments" ]; then 346 | # adding U+00a0 (non-breaking space) for issues with zero comments and commentable 347 | # without it an issue with 0 comments and >1 reaction would trick the if check 348 | # sometimes the "age" column can only be 2 words instead of 3, e.g. "just now", that is why $6 and $7 349 | _load_indicator $'No comments on this issue\nRestarting' 350 | fzf_issue_function 0s 351 | else 352 | fzf_issue_comment_function 353 | fi 354 | ;; 355 | shift-left) 356 | INITIAL_QUERY="" 357 | fzf_search_function 358 | ;; 359 | shift-right) 360 | INITIAL_QUERY="" 361 | fzf_pr_function 362 | ;; 363 | esac 364 | } 365 | 366 | help_issue_comment_function() { 367 | cat < " \ 424 | --preview-window "+5:~5" --preview "$GH_COMMENTS_PREVIEW | $LANG_BAT_SETUP md" \ 425 | --bind "?:toggle-preview+change-preview:echo '$(help_issue_comment_function)'" \ 426 | --bind "tab:toggle-preview+change-preview:$GH_COMMENTS_PREVIEW | $LANG_BAT_SETUP md" \ 427 | --bind "ctrl-b:execute-silent(python -m webbrowser https://github.com/$OWNER_REPO_NAME/issues/$TICKET_NUMBER#issuecomment-{2})" \ 428 | --bind "ctrl-t:execute-silent($GH_ADD_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:$GH_ISSUE_COMMENTS_COMMAND --raw-field owner=${OWNER_REPO_NAME%/*} --raw-field name=${OWNER_REPO_NAME#*/} --field number=$TICKET_NUMBER || true" \ 429 | --bind "ctrl-u:execute-silent($GH_REMOVE_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:$GH_ISSUE_COMMENTS_COMMAND --raw-field owner=${OWNER_REPO_NAME%/*} --raw-field name=${OWNER_REPO_NAME#*/} --field number=$TICKET_NUMBER || true" \ 430 | --expect "ctrl-g,ctrl-n,ctrl-x,esc") || true 431 | 432 | printed_query="$(sed 1q <<<"$OUTPUT_SELECTION")" 433 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 434 | output_textline="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{$1=""; $2=""; sub("^ *",""); print $0}')" 435 | output_id="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $1}')" 436 | # reset the variable 437 | OUTPUT_SELECTION="" 438 | case "$expected_key" in 439 | ctrl-g) 440 | gh issue comment "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 441 | _load_indicator 442 | INITIAL_COMMENT_QUERY="$printed_query" 443 | fzf_issue_comment_function 0s 444 | ;; 445 | 446 | ctrl-n) 447 | gh issue create --repo "$OWNER_REPO_NAME" || true 448 | _load_indicator 449 | INITIAL_QUERY="$printed_query" 450 | fzf_issue_function 0s 451 | ;; 452 | ctrl-x) 453 | check_viewer_permission=$( 454 | gh api graphql --raw-field id="$output_id" \ 455 | --raw-field query=$'query ($id: ID!) { node(id: $id) { ... on IssueComment { viewerCanDelete }}}' \ 456 | --jq '.data.node.viewerCanDelete' 457 | ) 458 | if [ "$check_viewer_permission" = "true" ]; then 459 | if _user_confirm "$output_textline" "deletion of this comment"; then 460 | gh api graphql --silent --raw-field id="$output_id" \ 461 | --raw-field query=$'mutation($id: ID!) { deleteIssueComment(input: {id: $id}) { clientMutationId }}' 462 | _load_indicator $'🟢 Success.... comment deleted\nRestarting' 463 | else 464 | _load_indicator $'Discarding... comment was not deleted!\nRestarting' 465 | fi 466 | else 467 | _load_indicator $'🛑 Missing permissions to delete this comment!\nRestarting' 468 | fi 469 | INITIAL_COMMENT_QUERY="$printed_query" 470 | fzf_issue_comment_function 0s 471 | ;; 472 | esc) 473 | INITIAL_COMMENT_QUERY="" 474 | fzf_issue_function 475 | ;; 476 | esac 477 | } 478 | 479 | ###################################### PULL REQUEST (PR) 480 | 481 | help_pr_function() { 482 | cat <${COLOR_RESET} List Pull Requests from current directory 488 | ${GREEN_NORMAL}-c ${COLOR_RESET} Cache the response, for example "30s", "15m", "1h" (default: 20s) 489 | ${GREEN_NORMAL}-e ${COLOR_RESET} Emoji to make a reaction (default: 👍 ) 490 | ${GREEN_NORMAL}-h ${COLOR_RESET} Help 491 | ${GREEN_NORMAL}-o ${COLOR_RESET} sorting order of Pull Requests (default: created-desc) 492 | ${GREEN_NORMAL}-r ${COLOR_RESET} Specify a repo (form: OWNER/REPO) 493 | ${GREEN_NORMAL}-w ${COLOR_RESET} Display the preview window upon start (default: hidden) 494 | 495 | ${WHITE_BOLD}Symbols${COLOR_RESET} 496 | 🔖 Checks indicator 497 | ${YELLOW_NORMAL}◌${COLOR_RESET} in progress 498 | ${GREEN_NORMAL}✓${COLOR_RESET} success 499 | ${RED_NORMAL}x${COLOR_RESET} failure 500 | ⊘ cancelled 501 | 💬 Total number of comments (gray commentable; red locked) 502 | 📣 Total number of emojis (green reactable; yellow reacted; red locked) 503 | ${GREEN_NORMAL}+${COLOR_RESET} Additions 504 | ${RED_NORMAL}-${COLOR_RESET} Deletions 505 | 506 | ${WHITE_BOLD}Hotkeys${COLOR_RESET} 507 | ${GREEN_NORMAL}? ${COLOR_RESET} Toggle help 508 | ${GREEN_NORMAL}enter ${COLOR_RESET} See comments 509 | ${GREEN_NORMAL}tab ${COLOR_RESET} Toggle preview 510 | ${GREEN_NORMAL}shift+tab ${COLOR_RESET} Change preview window size 511 | ${GREEN_NORMAL}ctrl+a ${COLOR_RESET} ALL Pull Requests 512 | ${GREEN_NORMAL}ctrl+b ${COLOR_RESET} Browser 513 | ${GREEN_NORMAL}ctrl+d ${COLOR_RESET} Toggle diff 514 | ${GREEN_NORMAL}ctrl+e ${COLOR_RESET} Edit a Pull Request 515 | ${GREEN_NORMAL}ctrl+f ${COLOR_RESET} Fuzzy search 516 | ${GREEN_NORMAL}ctrl+g ${COLOR_RESET} Merge a Pull Request 517 | ${GREEN_NORMAL}ctrl+o ${COLOR_RESET} OPEN Pull Requests only 518 | ${GREEN_NORMAL}ctrl+p ${COLOR_RESET} Put "involves:@me" into the search 519 | ${GREEN_NORMAL}ctrl+r ${COLOR_RESET} Reload 520 | ${GREEN_NORMAL}ctrl+t ${COLOR_RESET} React with $(_emoji_from_name "$REACTION_EMOJI") emoji 521 | ${GREEN_NORMAL}ctrl+u ${COLOR_RESET} Undo the $(_emoji_from_name "$REACTION_EMOJI") emoji reaction 522 | ${GREEN_NORMAL}ctrl+x ${COLOR_RESET} Write a comment 523 | ${GREEN_NORMAL}ctrl+y ${COLOR_RESET} Checkout 524 | ${GREEN_NORMAL}shift+left ${COLOR_RESET} Switch to Issues 525 | ${GREEN_NORMAL}shift+right${COLOR_RESET} Switch to Workflow Runs 526 | ${GREEN_NORMAL}esc ${COLOR_RESET} Quit 527 | EOF 528 | } 529 | 530 | fzf_pr_function() { 531 | GH_PR_PREVIEW=$'gh api graphql --paginate --raw-field id={1} --raw-field query=\'query($id: ID!, $endCursor: String){node(id: $id) {... on PullRequest { additions author { login } body createdAt comments(first: 100) { totalCount nodes { author { login } body createdAt viewerDidAuthor reactionGroups { content reactors(last: 4) { totalCount nodes { ... on Actor { login }}}}}} commits(last: 1) { nodes { commit { statusCheckRollup { state contexts { checkRunCount checkRunCountsByState { state count }}}}}} deletions timelineItems(itemTypes: [CLOSED_EVENT, MERGED_EVENT, ISSUE_COMMENT], first: 100, after: $endCursor) { nodes { ... on ClosedEvent { actor { login } createdAt stateReason } ... on IssueComment { author { login } body createdAt viewerDidAuthor reactionGroups { content reactors(last: 4) { totalCount nodes { ... on Actor { login }}}} reactionGroups { content viewerHasReacted reactors(last: 4) { nodes { ... on Actor { login }} totalCount }}} ... on MergedEvent { actor { login } createdAt mergeRef { repository { name } name } commit { abbreviatedOid }}} pageInfo { hasNextPage endCursor }} labels(first:10) { nodes { name } } number reactionGroups { content viewerHasReacted reactors(last: 4) { nodes { ... on Actor { login }} totalCount }} state title viewerDidAuthor }}}\' --template \'{{- $prefix := .data.node -}}{{- $statePRColor := "green+b" -}}{{- if eq $prefix.state "CLOSED" -}}{{- $statePRColor = "red+b" -}}{{- else if eq $prefix.state "MERGED" -}}{{- $statePRColor = "magenta+b" -}}{{- end -}}{{- $issueAuthor := "[DELETED_USER]" -}}{{- if $prefix.author -}}{{- $issueAuthor = $prefix.author.login -}}{{- end -}}{{- $authorColor := "cyan+hb" -}}{{- if $prefix.viewerDidAuthor -}}{{- $authorColor = "yellow+bh" -}}{{- end -}}{{- tablerow ($prefix.title | color "white+bh") -}}{{- tablerender -}}{{- tablerow (printf "%s  #%.0f" $prefix.state $prefix.number | color $statePRColor) ($issueAuthor | color $authorColor) (printf "│ %s ∙ %.0f Comments │" (timeago $prefix.createdAt) $prefix.comments.totalCount | color "gray+h") -}}{{- tablerender -}}{{- $checksDisplay := "No checks" | color "gray+h" -}}{{- $statusCheckRollup := (index $prefix.commits.nodes 0).commit.statusCheckRollup -}}{{- if $statusCheckRollup -}}{{- $totalChecks := $statusCheckRollup.contexts.checkRunCount -}}{{- $failedChecks := 0 -}}{{- range $statusCheckRollup.contexts.checkRunCountsByState -}}{{- if eq .state "FAILURE" -}}{{ $failedChecks = .count -}}{{- end -}}{{- end -}}{{- if eq $statusCheckRollup.state "SUCCESS" -}}{{- $checksDisplay = printf "✓ %.0f Checks passed" $totalChecks | color "green" -}}{{- else if eq $statusCheckRollup.state "PENDING" -}}{{- $checksDisplay = "◌ Checks pending" | color "yellow" -}}{{- else if eq $statusCheckRollup.state "FAILURE" -}}{{- $checksDisplay = printf "x %.0f/%.0f Checks failed" $failedChecks $totalChecks | color "red" -}}{{- else if eq $statusCheckRollup.state "CANCELLED" -}}{{- $checksDisplay = "⊘ Checks cancelled" | color "gray+h" -}}{{- else -}}{{- $checksDisplay = $statusCheckRollup.state | color "white" -}}{{- end -}}{{- end -}}{{- tablerow (printf "%s %s ∙ %s" (printf "+%.0f" $prefix.additions | color "green") (printf "-%.0f" $prefix.deletions | color "red") $checksDisplay) -}}{{- tablerender -}}{{- if $prefix.labels.nodes -}}{{- tablerow ("Labels:" | color "white+b") (pluck "name" $prefix.labels.nodes | join ", " | printf "[ %s ]" | color "white") -}}{{- tablerender -}}{{- end -}}{{- range $prefix.reactionGroups -}}{{ $emoji := .content }}{{ if eq .content "CONFUSED" }}{{ $emoji = "😕" }}{{ else if eq .content "EYES" }}{{ $emoji = "👀" }}{{ else if eq .content "HEART" }}{{ $emoji = "💖" }}{{ else if eq .content "HOORAY" }}{{ $emoji = "🎉" }}{{ else if eq .content "LAUGH" }}{{ $emoji = "😄" }}{{ else if eq .content "THUMBS_DOWN" }}{{ $emoji = "👎" }}{{ else if eq .content "THUMBS_UP" }}{{ $emoji = "👍" }}{{ else if eq .content "ROCKET" }}{{ $emoji = "🚀" }}{{ end }}{{ if gt .reactors.totalCount 0.0 }}{{ $emojiGiver := pluck "login" .reactors.nodes | join ", " | color "green+d" }}{{ if gt (pluck "login" .reactors.nodes | len ) 3 }}{{- /* NOTE: number of names limited */ -}}{{ $emojiGiver = slice (pluck "login" .reactors.nodes) 1 | join ", " | printf "%s, ..." | color "green+d" }}{{end}}{{- $emojiTotal := .reactors.totalCount | color "green+bd" -}}{{- if .viewerHasReacted -}}{{- $emojiTotal = .reactors.totalCount | color "yellow+bh" -}}{{- end -}}{{- tablerow (printf "%s %s" $emojiTotal $emoji) $emojiGiver -}}{{- tablerender -}}{{- end -}}{{- end }}{{- tablerow ("——————————————————————————————————————————————————————————"| color "gray+d") -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- $bodyText := $prefix.body }}{{ if eq ($bodyText | len) 0 -}}{{- $bodyText = " No description provided. " | color "white+i" -}}{{- end -}}{{- $bodyText -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- range $prefix.timelineItems.nodes -}}{{- $actorTimeline := "[DELETED_USER]" -}}{{- if .actor -}}{{- $actorTimeline = .actor.login -}}{{- end -}}{{- $authorTimelineColor := "cyan+hb" -}}{{- if .viewerDidAuthor -}}{{- $authorTimelineColor = "yellow+bh" -}}{{- end -}}{{- if .mergeRef -}}{{- $stateMergedColor := "93+b" -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color $stateMergedColor) -}}{{- tablerender -}}{{- tablerow (printf "%s merged %s into %s %s" ($actorTimeline | color $authorTimelineColor) (.commit.abbreviatedOid | color $stateMergedColor) (printf "%s:%s" .mergeRef.repository.name .mergeRef.name) (timeago .createdAt | color "white+bh")) -}}{{- tablerender -}}{{- tablerow ("◢■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◣"| color $stateMergedColor) -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- else if eq .stateReason "COMPLETED" -}}{{- $stateClosedColor := "red+b" -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color $stateClosedColor) -}}{{- tablerender -}}{{- tablerow (printf "%s closed this %s" ($actorTimeline | color $authorTimelineColor) (timeago .createdAt | color "white+bh")) -}}{{- tablerender -}}{{- tablerow ("◢■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◣"| color $stateClosedColor) -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- else -}}{{- $authorTimeline := "[DELETED_USER]" -}}{{- if .author -}}{{- $authorTimeline = .author.login -}}{{- end -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow ("◥■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■◤"| color "gray+d") -}}{{- tablerender -}}{{- tablerow ($authorTimeline | color $authorTimelineColor) ( timeago .createdAt | color "white+bh") -}}{{- tablerender -}}{{- range .reactionGroups -}}{{ $emojiInComment := .content }}{{ if eq .content "CONFUSED" }}{{ $emojiInComment = "😕" }}{{ else if eq .content "EYES" }}{{ $emojiInComment = "👀" }}{{ else if eq .content "HEART" }}{{ $emojiInComment = "💖" }}{{ else if eq .content "HOORAY" }}{{ $emojiInComment = "🎉" }}{{ else if eq .content "LAUGH" }}{{ $emojiInComment = "😄" }}{{ else if eq .content "THUMBS_DOWN" }}{{ $emojiInComment = "👎" }}{{ else if eq .content "THUMBS_UP" }}{{ $emojiInComment = "👍" }}{{ else if eq .content "ROCKET" }}{{ $emojiInComment = "🚀" }}{{ end }}{{ if gt .reactors.totalCount 0.0 }}{{ $emojiInCommentGiver := pluck "login" .reactors.nodes | join ", " | color "green+d" }}{{ if gt (pluck "login" .reactors.nodes | len ) 3 }}{{- /* NOTE: number of names limited */ -}}{{ $emojiInCommentGiver = slice (pluck "login" .reactors.nodes) 1 | join ", " | printf "%s, ..." | color "green+d" }}{{end}}{{- $emojiInCommentTotal := .reactors.totalCount | color "green+bd" -}}{{- if .viewerHasReacted -}}{{- $emojiInCommentTotal = .reactors.totalCount | color "yellow+bh" -}}{{- end -}}{{- tablerow (printf "%s %s" $emojiInCommentTotal $emojiInComment) $emojiInCommentGiver -}}{{- tablerender -}}{{- end -}}{{- end -}}{{- tablerow ("‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥"| color "gray+d") -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- .body -}}{{- tablerow "" -}}{{- tablerender -}}{{- tablerow "" -}}{{- tablerender -}}{{- end -}}{{- end -}}\'' 532 | 533 | # fzf allows hiding columns "--with-nth 2...", headers are also affected, so NODE_ID_HIDDEN is included there 534 | GH_PR_COMMAND=$'gh api graphql --raw-field query=\'query($filter: String!) {search(query: $filter, type: ISSUE, first: 35) {issueCount nodes { ... on PullRequest { additions author { login } comments {totalCount} commits(last: 1) { nodes { commit { statusCheckRollup { state }}}} deletions id number updatedAt reactions { totalCount viewerHasReacted } state title viewerCanReact viewerDidAuthor }}}}\' --template \' 535 | {{- $limitCount := .data.search.issueCount -}} 536 | {{- if gt .data.search.issueCount 35.0 -}}{{- $limitCount = 35.0 -}}{{- end -}} 537 | {{- tablerow "NODE_ID_HIDDEN" (printf "%.0f of ∑ %.0f" $limitCount .data.search.issueCount | color "blue+b") ("|" | color "white+d") ("? Help · esc Quit" | color "blue") -}}{{- tablerender -}} 538 | {{- tablerow "" -}}{{- tablerender -}} 539 | {{- $headerColor := "blue+b" -}} 540 | {{- tablerow "NODE_ID_HIDDEN" ("PR" | color $headerColor) ("AUTHOR" | color $headerColor) ("LAST UPDATE" | color $headerColor) "🔖" "💬" "📣" ("+" | color "green+bd") ("-" | color "red+bd") ("TITLE" | color $headerColor) -}} 541 | {{- range .data.search.nodes -}} 542 | {{- $statePullRequestColor := "green" -}} 543 | {{- if eq .state "CLOSED" }}{{ $statePullRequestColor = "red" }}{{- else if eq .state "MERGED" }}{{ $statePullRequestColor = "magenta" }}{{ end -}} 544 | {{- $author := "[DELETED_USER]" -}} 545 | {{- if .author -}}{{- $author = .author.login -}}{{- end -}} 546 | {{- $authorColor := "cyan+h" -}} 547 | {{- if .viewerDidAuthor -}}{{- $authorColor = "yellow" -}}{{- end -}} 548 | {{- $commentCount := "\u00a0" -}}{{- /* NOTE: U+00a0 (non-breaking space) for entries without comments, which the viewer can comment on. Needed to make a if decision with fzf later in the script. */ -}} 549 | {{- if not .viewerCanReact -}}{{- $commentCount = (or (printf "%.0f" .comments.totalCount) "0") -}} 550 | {{- else if .comments.totalCount -}}{{- $commentCount = printf "%.0f" .comments.totalCount -}} 551 | {{- end -}} 552 | {{- $commentColor := "white+bd" -}} 553 | {{- if not .viewerCanReact -}}{{- $commentColor = "red+bd" -}}{{- end -}} 554 | {{- $emojiCount := "" -}} 555 | {{- if not .viewerCanReact -}}{{- $emojiCount = (or (printf "%.0f" .reactions.totalCount) "0") -}} 556 | {{- else if .reactions.totalCount -}}{{- $emojiCount = printf "%.0f" .reactions.totalCount -}} 557 | {{- end -}} 558 | {{- $emojiColor := "green+bd" -}} 559 | {{- if not .viewerCanReact -}}{{- $emojiColor = "red+bd" -}} 560 | {{- else if .reactions.viewerHasReacted -}}{{- $emojiColor = "yellow+b" -}} 561 | {{- end -}} 562 | {{- /* NOTE: Go templates use zero based indexing, like JavaScript */ -}} 563 | {{- $statusCheckRollup := "" -}} 564 | {{- if .commits.nodes -}} 565 | {{- $statusCheckRollup = (index .commits.nodes 0).commit.statusCheckRollup -}} 566 | {{- end -}} 567 | {{- $checksDisplay := "" -}} 568 | {{- if $statusCheckRollup -}} 569 | {{- if eq $statusCheckRollup.state "SUCCESS" -}} 570 | {{- $checksDisplay = "✓" | color "green" -}} 571 | {{- else if eq $statusCheckRollup.state "PENDING" -}} 572 | {{- $checksDisplay = "◌" | color "yellow" -}} 573 | {{- else if eq $statusCheckRollup.state "FAILURE" -}} 574 | {{- $checksDisplay = "x" | color "red" -}} 575 | {{- else if eq $statusCheckRollup.state "CANCELLED" -}} 576 | {{- $checksDisplay = "⊘" | color "gray+h" -}} 577 | {{- else -}} 578 | {{- $checksDisplay = "?" | color "white" -}} 579 | {{- end -}} 580 | {{- end -}} 581 | {{- tablerow .id (printf "#%.0f" .number | color $statePullRequestColor) ($author | color $authorColor) (timeago .updatedAt) $checksDisplay ($commentCount | color $commentColor) ($emojiCount | color $emojiColor) (printf "+%.0f" .additions | color "green") (printf "-%.0f" .deletions | color "red") .title -}} 582 | {{- end -}}\'' 583 | 584 | INITIAL_PR_PROMPT=$(grep -q A <"$SCRATCH_FILE" && echo "Pull Requests  [$OWNER_REPO_NAME] > " || echo "${GREEN_NORMAL}OPEN Pull Requests  [$OWNER_REPO_NAME] > ") 585 | 586 | OUTPUT_SELECTION=$(FZF_DEFAULT_COMMAND="(grep -q A <$SCRATCH_FILE && $GH_PR_COMMAND --cache ${1:-$CACHE_TIME} --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME $INITIAL_QUERY \" || $GH_PR_COMMAND --cache ${1:-$CACHE_TIME} --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open $INITIAL_QUERY \") || true" \ 587 | _fzf_basic_options --disabled --header-lines 3 --with-nth 2.. \ 588 | --prompt "$INITIAL_PR_PROMPT" \ 589 | --query "$INITIAL_QUERY" --preview "sleep 0.35; $GH_PR_PREVIEW | $LANG_BAT_SETUP md" \ 590 | --bind "change:first+reload-sync:sleep 0.25; (grep -q A <$SCRATCH_FILE && $GH_PR_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME \"{q} || $GH_PR_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 591 | --bind "?:toggle-preview+change-preview:echo '$(help_pr_function)'" \ 592 | --bind "tab:toggle-preview+change-preview:sleep 0.35; $GH_PR_PREVIEW | $LANG_BAT_SETUP md" \ 593 | --bind "ctrl-a:rebind(change)+change-prompt(Pull Requests  [$OWNER_REPO_NAME] > )+disable-search+execute-silent(echo A >$SCRATCH_FILE)+reload-sync:$GH_PR_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME \"{q} || true" \ 594 | --bind "ctrl-b:execute-silent(gh pr view {2} --web --repo $OWNER_REPO_NAME)" \ 595 | --bind "ctrl-f:unbind(change)+enable-search+clear-query+change-prompt(${RED_NORMAL}FUZZY SEARCH [$OWNER_REPO_NAME] > )" \ 596 | --bind "ctrl-d:toggle-preview+change-preview(gh pr diff {2} --repo $OWNER_REPO_NAME"$' | if type -p delta >/dev/null; then delta --width ${FZF_PREVIEW_COLUMNS:-$COLUMNS}; else cat; fi)' \ 597 | --bind "ctrl-o:rebind(change)+change-prompt(${GREEN_NORMAL}OPEN Pull Requests  [$OWNER_REPO_NAME] > )+disable-search+execute-silent(echo B >$SCRATCH_FILE)+reload-sync:$GH_PR_COMMAND --cache $CACHE_TIME --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open \"{q} || true" \ 598 | --bind "ctrl-p:put(involves:@me)" \ 599 | --bind "ctrl-r:reload-sync:(grep -q A <$SCRATCH_FILE && $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME \"{q} || $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 600 | --bind "ctrl-t:execute-silent($GH_ADD_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:(grep -q A <$SCRATCH_FILE && $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME \"{q} || $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 601 | --bind "ctrl-u:execute-silent($GH_REMOVE_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:(grep -q A <$SCRATCH_FILE && $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME \"{q} || $GH_PR_COMMAND --raw-field filter=\"$SORTING_ORDER type:pr repo:$OWNER_REPO_NAME state:open \"{q}) || true" \ 602 | --expect "ctrl-e,ctrl-g,ctrl-x,ctrl-y,enter,shift-left,shift-right") || true 603 | 604 | printed_query="$(sed 1q <<<"$OUTPUT_SELECTION")" 605 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 606 | TICKET_NUMBER="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $2}' | tr -d "#")" 607 | number_comments="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $7,$8}' | grep -Eo '[0-9]+' || true)" 608 | # reset the variable 609 | OUTPUT_SELECTION="" 610 | case "$expected_key" in 611 | ctrl-e) 612 | gh pr edit "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 613 | _load_indicator 614 | INITIAL_QUERY="$printed_query" 615 | fzf_pr_function 0s 616 | ;; 617 | ctrl-g) 618 | gh pr merge "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" 619 | exit 0 620 | ;; 621 | ctrl-x) 622 | gh pr comment "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 623 | _load_indicator 624 | INITIAL_QUERY="$printed_query" 625 | fzf_pr_function 0s 626 | ;; 627 | ctrl-y) 628 | gh pr checkout "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" 629 | exit 0 630 | ;; 631 | enter) 632 | INITIAL_QUERY="$printed_query" 633 | if [ -z "$number_comments" ]; then 634 | _load_indicator $'No comments on this Pull Request\nRestarting' 635 | fzf_pr_function 636 | else 637 | fzf_pr_comment_function 638 | fi 639 | ;; 640 | shift-left) 641 | INITIAL_QUERY="" 642 | fzf_issue_function 643 | ;; 644 | shift-right) 645 | INITIAL_QUERY="" 646 | fzf_workflow_run_function 647 | ;; 648 | esac 649 | } 650 | 651 | help_pr_comment_function() { 652 | cat < " \ 727 | --preview-window "+5:~5" --preview "$GH_COMMENTS_PREVIEW | $LANG_BAT_SETUP md" \ 728 | --bind "?:toggle-preview+change-preview:echo '$(help_pr_comment_function)'" \ 729 | --bind "tab:toggle-preview+change-preview:$GH_COMMENTS_PREVIEW | $LANG_BAT_SETUP md" \ 730 | --bind "ctrl-b:execute-silent(python -m webbrowser https://github.com/$OWNER_REPO_NAME/pull/$TICKET_NUMBER#issuecomment-{2})" \ 731 | --bind "ctrl-d:toggle-preview+change-preview(gh pr diff $TICKET_NUMBER --repo $OWNER_REPO_NAME"$' | if type -p delta >/dev/null; then delta --width ${FZF_PREVIEW_COLUMNS:-$COLUMNS}; else cat; fi)' \ 732 | --bind "ctrl-t:execute-silent($GH_ADD_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:$GH_PR_COMMENTS_COMMAND --raw-field owner=${OWNER_REPO_NAME%/*} --raw-field name=${OWNER_REPO_NAME#*/} --field number=$TICKET_NUMBER || true" \ 733 | --bind "ctrl-u:execute-silent($GH_REMOVE_REACTION --raw-field id={1} --raw-field emoji=$REACTION_EMOJI)+reload-sync:$GH_PR_COMMENTS_COMMAND --raw-field owner=${OWNER_REPO_NAME%/*} --raw-field name=${OWNER_REPO_NAME#*/} --field number=$TICKET_NUMBER || true" \ 734 | --expect "ctrl-x,esc") || true 735 | 736 | printed_query="$(sed 1q <<<"$OUTPUT_SELECTION")" 737 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 738 | # reset the variable 739 | OUTPUT_SELECTION="" 740 | case "$expected_key" in 741 | ctrl-x) 742 | gh pr comment "$TICKET_NUMBER" --repo "$OWNER_REPO_NAME" || true 743 | _load_indicator 744 | INITIAL_COMMENT_QUERY="$printed_query" 745 | fzf_pr_comment_function 0s 746 | ;; 747 | esc) 748 | INITIAL_COMMENT_QUERY="" 749 | fzf_pr_function 750 | ;; 751 | esac 752 | } 753 | 754 | ###################################### WORKFLOW RUNS 755 | help_workflow_run_function() { 756 | cat <${COLOR_RESET} List Workflow Runs from current directory 762 | ${GREEN_NORMAL}-h ${COLOR_RESET} Help 763 | ${GREEN_NORMAL}-i ${COLOR_RESET} Interval for updating the list (default: 10) 764 | ${GREEN_NORMAL}-n ${COLOR_RESET} Number of listed Runs (default: 10, max: 100) 765 | ${GREEN_NORMAL}-p ${COLOR_RESET} Pull Requests are included in the list (default: not included) 766 | ${GREEN_NORMAL}-r ${COLOR_RESET} Specify a repository (form: OWNER/REPO) 767 | ${GREEN_NORMAL}-w ${COLOR_RESET} Display the preview window upon start (default: hidden) 768 | 769 | ${WHITE_BOLD}Symbols${COLOR_RESET} 770 | 👀 List is updated every ${UPDATE_TIME}s, stop it with a defined hotkey 771 | 🔖 Status 772 | ${YELLOW_NORMAL}◌${COLOR_RESET} in progress 773 | ${GREEN_NORMAL}✓${COLOR_RESET} success 774 | ${RED_NORMAL}x${COLOR_RESET} failure 775 | ⊘ cancelled 776 | ⌛️ Elapsed Time (min) 777 | 778 | ${WHITE_BOLD}Hotkeys${COLOR_RESET} 779 | ${GREEN_NORMAL}? ${COLOR_RESET} Toggle help 780 | ${GREEN_NORMAL}enter ${COLOR_RESET} See logs, press "q" to return 781 | ${GREEN_NORMAL}tab ${COLOR_RESET} Toggle preview 782 | ${GREEN_NORMAL}shift+tab ${COLOR_RESET} Change preview window size 783 | ${GREEN_NORMAL}ctrl+a ${COLOR_RESET} Abort watch mode 784 | ${GREEN_NORMAL}ctrl+b ${COLOR_RESET} Browser 785 | ${GREEN_NORMAL}ctrl+d ${COLOR_RESET} Download attached artifacts 786 | ${GREEN_NORMAL}ctrl+g ${COLOR_RESET} Cancel a Run 787 | ${GREEN_NORMAL}ctrl+f ${COLOR_RESET} See failed logs 788 | ${GREEN_NORMAL}ctrl+o ${COLOR_RESET} Operate in watch mode 👀 update the list every ${UPDATE_TIME}s 789 | ${GREEN_NORMAL}ctrl+x ${COLOR_RESET} Delete a Run 790 | ${GREEN_NORMAL}ctrl+y ${COLOR_RESET} Copy the ID of the Run (macOS only) 791 | ${GREEN_NORMAL}shift+left${COLOR_RESET} Switch to Pull Requests 792 | ${GREEN_NORMAL}esc ${COLOR_RESET} Quit 793 | EOF 794 | } 795 | 796 | fzf_workflow_run_function() { 797 | INITIAL_WORKFLOW_PROMPT="Workflow Runs  $(printf "[%s]" "$OWNER_REPO_NAME")" 798 | GH_WORKFLOWS_COMMAND="gh api --cache ${UPDATE_TIME}s --method GET --raw-field exclude_pull_requests=$EXCLUDE_PULL_REQUESTS --field per_page=$NUMBER_WORKFLOW_RUN_LIST repos/$OWNER_REPO_NAME/actions/runs "$' --jq \' 799 | def colors: 800 | { 801 | "gray": "\u001b[90m", 802 | "red": "\u001b[31m", 803 | "green": "\u001b[32m", 804 | "yellow": "\u001b[33m", 805 | "cyan": "\u001b[36m", 806 | "white_bold": "\u001b[1;37m", 807 | "COMMENT": "Header lines must be colored when a column element is colored so that they are aligned correctly. Also, only color codes that have the same length may be used, i.e. - \u001b[1;36m - would only work if the header was colored with - \u001b[0000m -. This is just a very silly workaround for the fact that the column command cannot handle ANSI colors to correctly calculate the actual cell width.", 808 | "none": "\u001b[00m", 809 | "none_bold": "\u001b[0000m", 810 | "reset": "\u001b[0m" 811 | }; 812 | def colored(text; color): colors[color] + text + colors.reset; 813 | ["ID", "URL", "STATUS", colored(" "; "none"), colored("🔖"; "none"), "⌛️", colored("Workflow"; "none"), colored("Branch"; "none_bold"), "Event", "Title"], 814 | (.workflow_runs[] | [ 815 | .id, 816 | .html_url, 817 | .status, 818 | colored((.created_at | fromdateiso8601) as $time_sec | if ((now - $time_sec) / 3600) < 1 then ((now - $time_sec) / 60 | floor) | tostring + "min ago" elif ((now - $time_sec) / 3600) < 24 then ((now - $time_sec) / 3600 | floor) | tostring + "h ago" else ($time_sec | strftime("%d/%b/%y")) end; "gray"), 819 | (if (.conclusion == null and .status == null) then colored(" "; "none") elif .conclusion == null then if .status == "in_progress" then colored("◌"; "yellow") else colored(.status; "none") end else if .conclusion == "success" then colored("✓"; "green") elif .conclusion == "failure" then colored("x"; "red") elif .conclusion == "cancelled" then colored("⊘"; "gray") else colored(.conclusion; "none") end end), 820 | ((if .status == "in_progress" then now else (.updated_at | fromdateiso8601) end) - (.created_at | fromdateiso8601) | (./60|floor)+(.%60/60*100|round/100) | .*100|round/100), 821 | colored(.name; "cyan"), 822 | colored(.head_branch; "white_bold"), 823 | .event, 824 | .display_title] 825 | ) | @tsv\' | column -ts "\t"' 826 | 827 | OUTPUT_SELECTION=$(FZF_DEFAULT_COMMAND="$GH_WORKFLOWS_COMMAND" \ 828 | _fzf_basic_options --header-lines 1 --with-nth 4.. \ 829 | --header $'\n? Help · esc Quit\n\n' \ 830 | --prompt "${GREEN_NORMAL}$INITIAL_WORKFLOW_PROMPT 👀 ${UPDATE_TIME}s > " \ 831 | --query "$INITIAL_QUERY" \ 832 | --preview "gh run view {1} --repo $OWNER_REPO_NAME" \ 833 | --bind "tab:toggle-preview+change-preview:gh run view {1} --repo $OWNER_REPO_NAME" \ 834 | --bind "?:toggle-preview+change-preview:echo '$(help_workflow_run_function)'" \ 835 | --bind "load:reload-sync:sleep $UPDATE_TIME;$GH_WORKFLOWS_COMMAND || true" \ 836 | --bind "ctrl-a:unbind(load)+change-prompt($INITIAL_WORKFLOW_PROMPT > )+reload-sync:$GH_WORKFLOWS_COMMAND || true" \ 837 | --bind "ctrl-b:execute-silent:python -m webbrowser {2}" \ 838 | --bind "ctrl-d:become(gh run download {1} --repo $OWNER_REPO_NAME)+reload-sync:$GH_WORKFLOWS_COMMAND || true" \ 839 | --bind "ctrl-g:execute-silent(gh run cancel {1} --repo $OWNER_REPO_NAME)+down+reload-sync:$GH_WORKFLOWS_COMMAND || true" \ 840 | --bind "ctrl-o:rebind(load)+change-prompt(${GREEN_NORMAL}$INITIAL_WORKFLOW_PROMPT 👀 ${UPDATE_TIME}s > )+reload-sync:$GH_WORKFLOWS_COMMAND || true" \ 841 | --bind "ctrl-x:execute-silent(gh run delete {1} --repo $OWNER_REPO_NAME)+down+reload-sync:$GH_WORKFLOWS_COMMAND || true" \ 842 | --bind "ctrl-y:execute:printf 'gh run view --log {1} --repo %s' '$OWNER_REPO_NAME' | pbcopy" \ 843 | --expect "ctrl-f,enter,shift-left") || true 844 | 845 | printed_query="$(sed 1q <<<"$OUTPUT_SELECTION")" 846 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 847 | output_id="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $1}')" 848 | output_status="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $3}')" 849 | # reset the variable 850 | OUTPUT_SELECTION="" 851 | case "$expected_key" in 852 | enter) 853 | if grep -Eq 'in_progress|queued' <<<"$output_status"; then 854 | gh run watch --repo "$OWNER_REPO_NAME" 855 | elif ! gh run view --log "$output_id" --repo "$OWNER_REPO_NAME" &>/dev/null; then 856 | _load_indicator $'No logs found\nRestarting' 857 | else 858 | GH_PAGER="$LANG_BAT_SETUP log" gh run view --log "$output_id" --repo "$OWNER_REPO_NAME" 859 | fi 860 | INITIAL_QUERY="$printed_query" 861 | fzf_workflow_run_function 862 | ;; 863 | ctrl-f) 864 | if ! gh run view --log-failed "$output_id" --repo "$OWNER_REPO_NAME" &>/dev/null; then 865 | _load_indicator $'No failed logs found\nRestarting' 866 | else 867 | GH_PAGER="$LANG_BAT_SETUP log" gh run view --log-failed "$output_id" --repo "$OWNER_REPO_NAME" 868 | fi 869 | INITIAL_QUERY="$printed_query" 870 | fzf_workflow_run_function 871 | ;; 872 | shift-left) 873 | INITIAL_QUERY="" 874 | fzf_pr_function 875 | ;; 876 | esac 877 | } 878 | 879 | ###################################### SEARCH 880 | help_search_function() { 881 | cat <${COLOR_RESET} Search for GitHub repos 887 | ${GREEN_NORMAL}-c ${COLOR_RESET} Cache the response, for example "30s", "15m", "1h" (default: 20s) 888 | ${GREEN_NORMAL}-h ${COLOR_RESET} Help 889 | ${GREEN_NORMAL}-w ${COLOR_RESET} Display the preview window upon start (default: hidden) 890 | 891 | ${WHITE_BOLD}Symbols${COLOR_RESET} 892 | ⭐ Number of stargazers 893 | 🏁 Primary language 894 | 895 | ${WHITE_BOLD}Hotkeys${COLOR_RESET} 896 | ${GREEN_NORMAL}? ${COLOR_RESET} Toggle help 897 | ${GREEN_NORMAL}tab ${COLOR_RESET} Preview README 898 | ${GREEN_NORMAL}shift+tab ${COLOR_RESET} Change preview window size 899 | ${GREEN_NORMAL}ctrl+b ${COLOR_RESET} Browser 900 | ${GREEN_NORMAL}ctrl+f ${COLOR_RESET} Preview release infos 901 | ${GREEN_NORMAL}ctrl+r ${COLOR_RESET} Reload 902 | ${GREEN_NORMAL}ctrl+t ${COLOR_RESET} Star repo 903 | ${GREEN_NORMAL}ctrl+u ${COLOR_RESET} Unstar repo 904 | ${GREEN_NORMAL}shift+left ${COLOR_RESET} Switch to your starred repos 905 | ${GREEN_NORMAL}shift+right${COLOR_RESET} Switch to Issues 906 | ${GREEN_NORMAL}esc ${COLOR_RESET} Quit 907 | EOF 908 | } 909 | 910 | fzf_search_function() { 911 | GH_SEARCH_COMMAND=$'gh api graphql --raw-field query=\'query($filter: String!) { search(query: $filter, type: REPOSITORY, first: 15) { repositoryCount nodes { ... on Repository {description isFork name owner {login} primaryLanguage {name} defaultBranchRef { target { ... on Commit { history(first: 1) { nodes { committedDate }}}}} stargazerCount viewerHasStarred}}}}\' --template=\' 912 | {{- $limitCount := .data.search.repositoryCount -}} 913 | {{- if gt .data.search.repositoryCount 15.0 -}}{{- $limitCount = 15.0 -}}{{- end -}} 914 | {{- tablerow (printf "%.0f of ∑ %.0f Repos" $limitCount .data.search.repositoryCount | color "yellow") ("|" | color "white") ("? Help · esc Quit" | color "blue") -}}{{- tablerender -}} 915 | {{- tablerow "" -}}{{- tablerender -}} 916 | {{- $headerColor := "blue+b" -}} 917 | {{- tablerow ("REPO" | color $headerColor) "⭐" "🏁" ("LAST COMMIT" | color $headerColor) ("DESCRIPTION" | color $headerColor) -}} 918 | {{- range .data.search.nodes -}} 919 | {{- $stargazerCount := printf "%.0f" .stargazerCount -}} 920 | {{- if gt .stargazerCount 1000000.0 -}}{{- $stargazerCount = ">1M" -}} 921 | {{- else if gt .stargazerCount 100000.0 -}}{{- $stargazerCount = printf "%.3sk" $stargazerCount -}} 922 | {{- else if gt .stargazerCount 10000.0 -}}{{- $stargazerCount = printf "%.2sk" $stargazerCount -}} 923 | {{- else if gt .stargazerCount 1000.0 -}}{{- $stargazerCount = printf "%.1sk" $stargazerCount -}} 924 | {{- else if eq .stargazerCount 0.0 -}}{{- $stargazerCount = "" -}} 925 | {{- end -}} 926 | {{- $stargazerColor := "yellow" -}} 927 | {{- if .viewerHasStarred }}{{ $stargazerColor = "011+h:094" }}{{ end -}} 928 | {{- $langName := "" -}} 929 | {{- if .primaryLanguage }}{{ $langName = .primaryLanguage.name }}{{ end -}} 930 | {{- if eq $langName "JavaScript" }}{{- $langName = "" -}} 931 | {{- else if eq $langName "Python" }}{{- $langName = "" -}} 932 | {{ end -}} 933 | {{- $time := "" -}}{{- /* NOTE: If a repo has no defaultBranch and/or commits, you need to conditionalize the .defaultBranchRef and the .committedDate to be safe against nil conditions, for example for repositories that are empty - "shellywu/wtclient". */ -}} 934 | {{- if .defaultBranchRef }} 935 | {{- range .defaultBranchRef.target.history.nodes -}} 936 | {{- if .committedDate -}}{{- $time = (timeago .committedDate) -}}{{- end -}} 937 | {{- end -}} 938 | {{ end -}} 939 | {{- tablerow (printf "%s%s%s" (.owner.login | color "cyan+h") ("/" | color "gray+h") (.name | color "cyan+hb")) ($stargazerCount | color $stargazerColor) ($langName | color "green") ($time | color "gray+h") .description -}} 940 | {{- end -}}\'' 941 | 942 | OUTPUT_SELECTION=$(FZF_DEFAULT_COMMAND="$GH_SEARCH_COMMAND --raw-field filter='$INITIAL_QUERY' --cache $CACHE_TIME" \ 943 | _fzf_basic_options \ 944 | --disabled \ 945 | --prompt "Search  repos > " \ 946 | --header-lines 3 \ 947 | --preview "gh repo view {1}" --preview-window wrap:"$PREVIEW_WINDOW_VISIBILITY" \ 948 | --query "$INITIAL_QUERY" --exact \ 949 | --bind "change:first+reload-sync:$GH_SEARCH_COMMAND --raw-field filter={q} --cache $CACHE_TIME || true" \ 950 | --bind "?:toggle-preview+change-preview:echo '$(help_search_function)'" \ 951 | --bind 'tab:toggle-preview+change-preview(gh repo view {1})' \ 952 | --bind "ctrl-b:execute-silent(gh repo view {1} --web)" \ 953 | --bind $'ctrl-f:toggle-preview+change-preview([[ $(gh release list -R {1}) ]] && gh release view -R {1})' \ 954 | --bind "ctrl-r:reload-sync:$GH_SEARCH_COMMAND --raw-field filter={q} || true" \ 955 | --bind "ctrl-t:execute-silent(gh api --method PUT /user/starred/{1})+reload-sync:$GH_SEARCH_COMMAND --raw-field filter={q} --cache 0s || true" \ 956 | --bind "ctrl-u:execute-silent(gh api --method DELETE /user/starred/{1})+reload-sync:$GH_SEARCH_COMMAND --raw-field filter={q} --cache 0s || true" \ 957 | --expect "shift-left,shift-right") || true 958 | 959 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 960 | output_textline="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $1}')" 961 | # reset the variable 962 | OUTPUT_SELECTION="" 963 | case "$expected_key" in 964 | shift-left) 965 | INITIAL_QUERY="" 966 | USER_NAME="$(gh api graphql --cache 1h --raw-field query='{viewer{login}}' --jq '.data.viewer.login')" 967 | fzf_star_function 968 | ;; 969 | shift-right) 970 | INITIAL_QUERY="" 971 | OWNER_REPO_NAME=${output_textline:-"$OWNER_REPO_NAME"} 972 | if [ -z "$OWNER_REPO_NAME" ]; then 973 | OWNER_REPO_NAME="$(gh api graphql --cache 1h --field owner=:owner --field name=:repo --raw-field query=$'query ($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { nameWithOwner }}' --jq '.[].repository.nameWithOwner' 2>/dev/null || echo "cli/cli")" 974 | fi 975 | fzf_issue_function 0s 976 | ;; 977 | esac 978 | } 979 | 980 | ###################################### STARS 981 | help_star_function() { 982 | cat <${COLOR_RESET} List your stars (sorted by the time the user set the star) 988 | ${GREEN_NORMAL}-c ${COLOR_RESET} Cache the response, for example "30s", "15m", "1h" (default: 20s) 989 | ${GREEN_NORMAL}-h ${COLOR_RESET} Help 990 | ${GREEN_NORMAL}-u ${COLOR_RESET} List stars of another user 991 | ${GREEN_NORMAL}-w ${COLOR_RESET} Display the preview window upon start (default: hidden) 992 | 993 | ${WHITE_BOLD}Symbols${COLOR_RESET} 994 | ⭐ Number of stargazers 995 | 🏁 Primary language 996 | 997 | ${WHITE_BOLD}Hotkeys${COLOR_RESET} 998 | ${GREEN_NORMAL}? ${COLOR_RESET} Toggle help 999 | ${GREEN_NORMAL}tab ${COLOR_RESET} Preview README 1000 | ${GREEN_NORMAL}shift+tab ${COLOR_RESET} Change preview window size 1001 | ${GREEN_NORMAL}ctrl+b ${COLOR_RESET} Browser 1002 | ${GREEN_NORMAL}ctrl+f ${COLOR_RESET} Preview release infos 1003 | ${GREEN_NORMAL}ctrl+r ${COLOR_RESET} Reload 1004 | ${GREEN_NORMAL}ctrl+t ${COLOR_RESET} Star repo (useful for stars of another user) 1005 | ${GREEN_NORMAL}ctrl+u ${COLOR_RESET} Unstar repo 1006 | ${GREEN_NORMAL}shift+right${COLOR_RESET} Switch to Search 1007 | ${GREEN_NORMAL}esc ${COLOR_RESET} Quit 1008 | EOF 1009 | } 1010 | 1011 | fzf_star_function() { 1012 | GH_STAR_COMMAND=$'gh api graphql --paginate --raw-field query=\'query($endCursor: String, $USER_NAME:String! ) { user(login:$USER_NAME) { starredRepositories(orderBy: { field: STARRED_AT direction: DESC }, first: 100, after: $endCursor) { totalCount pageInfo { hasNextPage, endCursor } nodes { name owner {login} primaryLanguage {name} stargazerCount viewerHasStarred defaultBranchRef { target { ... on Commit { history(first: 1) { nodes { committedDate }}}}} description }}}}\' --template \' 1013 | {{- tablerow (printf "∑ %.0f Starred Repos" .data.user.starredRepositories.totalCount | color "yellow+h") ( "|" | color "white") ("? Help · esc Quit" | color "blue") -}}{{- tablerender -}} 1014 | {{- tablerow "" -}}{{- tablerender -}} 1015 | {{- $headerColor := "yellow+b" -}} 1016 | {{- tablerow ("REPO" | color $headerColor) "⭐" "🏁" ("LAST COMMIT" | color $headerColor) ("DESCRIPTION" | color $headerColor) -}} 1017 | {{- range .data.user.starredRepositories.nodes -}} 1018 | {{- $stargazerCount := printf "%.0f" .stargazerCount -}} 1019 | {{- if gt .stargazerCount 1000000.0 -}}{{- $stargazerCount = ">1M" -}} 1020 | {{- else if gt .stargazerCount 100000.0 -}}{{- $stargazerCount = printf "%.3sk" $stargazerCount -}} 1021 | {{- else if gt .stargazerCount 10000.0 -}}{{- $stargazerCount = printf "%.2sk" $stargazerCount -}} 1022 | {{- else if gt .stargazerCount 1000.0 -}}{{- $stargazerCount = printf "%.1sk" $stargazerCount -}} 1023 | {{- else if eq .stargazerCount 0.0 -}}{{- $stargazerCount = "" -}} 1024 | {{- end -}} 1025 | {{- $stargazerColor := "yellow" -}} 1026 | {{- if .viewerHasStarred }}{{ $stargazerColor = "011+h:094" }}{{ end -}} 1027 | {{- $langName := "" -}} 1028 | {{- if .primaryLanguage }}{{ $langName = .primaryLanguage.name }}{{ end -}} 1029 | {{- if eq $langName "JavaScript" }}{{- $langName = "" -}} 1030 | {{- else if eq $langName "Python" }}{{- $langName = "" -}} 1031 | {{ end -}} 1032 | {{- $time := "" -}}{{- /* NOTE: If a repo has no defaultBranch and/or commits, you need to conditionalize the .defaultBranchRef and the .committedDate to be safe against nil conditions, for example for repositories that are empty - "shellywu/wtclient". */ -}} 1033 | {{- if .defaultBranchRef }} 1034 | {{- range .defaultBranchRef.target.history.nodes -}} 1035 | {{- if .committedDate -}}{{- $time = (timeago .committedDate) -}}{{- end -}} 1036 | {{- end -}} 1037 | {{ end -}} 1038 | {{- tablerow (printf "%s%s%s" (.owner.login | color "cyan+h") ("/" | color "gray+h") (.name | color "cyan+hb")) ($stargazerCount | color $stargazerColor) ($langName | color "green") ($time | color "gray+h") .description -}} 1039 | {{- end -}}\'' 1040 | 1041 | OUTPUT_SELECTION=$(FZF_DEFAULT_COMMAND="$GH_STAR_COMMAND --raw-field USER_NAME=$USER_NAME --cache $CACHE_TIME" \ 1042 | _fzf_basic_options --nth 1,3,7.. \ 1043 | --prompt "${YELLOW_NORMAL}Search  stars [$USER_NAME] > " --header-lines 3 \ 1044 | --preview "gh repo view {1}" --preview-window wrap:"$PREVIEW_WINDOW_VISIBILITY" \ 1045 | --query "$INITIAL_QUERY" --exact \ 1046 | --bind "?:toggle-preview+change-preview:echo '$(help_star_function)'" \ 1047 | --bind 'tab:toggle-preview+change-preview(gh repo view {1})' \ 1048 | --bind "ctrl-b:execute-silent(gh repo view {1} --web)" \ 1049 | --bind $'ctrl-f:toggle-preview+change-preview([[ $(gh release list -R {1}) ]] && gh release view -R {1})' \ 1050 | --bind "ctrl-r:reload-sync:$GH_STAR_COMMAND --raw-field USER_NAME=$USER_NAME --cache 0s" \ 1051 | --bind "ctrl-t:execute-silent(gh api --method PUT user/starred/{1})+reload-sync:$GH_STAR_COMMAND --raw-field USER_NAME=$USER_NAME --cache 0s" \ 1052 | --bind "ctrl-u:execute-silent(gh api --method DELETE user/starred/{1})+reload-sync:$GH_STAR_COMMAND --raw-field USER_NAME=$USER_NAME --cache 0s" \ 1053 | --expect "shift-right") || true 1054 | 1055 | expected_key="$(sed '1d;3d' <<<"$OUTPUT_SELECTION")" 1056 | output_textline="$(sed '1d;2d' <<<"$OUTPUT_SELECTION" | awk '{print $1}')" 1057 | # reset the variable 1058 | OUTPUT_SELECTION="" 1059 | case "$expected_key" in 1060 | shift-right) 1061 | INITIAL_QUERY="" 1062 | OWNER_REPO_NAME="$output_textline" 1063 | fzf_search_function 1064 | ;; 1065 | esac 1066 | } 1067 | 1068 | # <------------------- SCRIPT -------------------- > 1069 | 1070 | if [ $# = 0 ]; then 1071 | help_general_function 1072 | exit 0 1073 | else 1074 | ############# Check needed programs installed ############# 1075 | if ! command -v bat >/dev/null; then 1076 | _die_with_octocat "Bat was not found." "https://github.com/sharkdp/bat" 1077 | fi 1078 | if ! command -v fzf >/dev/null; then 1079 | _die_with_octocat "Fuzzy finder (fzf) was not found." "https://github.com/junegunn/fzf" 1080 | fi 1081 | USER_FZF_VERSION="$(fzf --version | awk '{print $1}')" 1082 | if [ "$(_version_number $MIN_FZF_VERSION)" -gt "$(_version_number "$USER_FZF_VERSION")" ]; then 1083 | _die_with_octocat "Minimum required \`fzf\` version is: $MIN_FZF_VERSION (found: $USER_FZF_VERSION)" 1084 | fi 1085 | if ! command -v python >/dev/null; then 1086 | _die_with_octocat "Python was not found." "https://www.python.org/" 1087 | fi 1088 | ############# Check needed scratch file exits and writeable ############# 1089 | # The purpose of this file is to signal to the fzf which search mode is active and remember the last one. 1090 | # This allows switching between "Open" and "All issues/ prs" and active search within the selected category. 1091 | SCRATCH_FILE="${BASH_SOURCE%/*}/.trashme" 1092 | if [ ! -e "$SCRATCH_FILE" ]; then 1093 | touch "$SCRATCH_FILE" 2>/dev/null || echo "Not allowed creating $SCRATCH_FILE" 1094 | fi 1095 | if [ ! -w "$SCRATCH_FILE" ]; then 1096 | _die_with_octocat "Not allowed writing to: $SCRATCH_FILE" 1097 | fi 1098 | fi 1099 | 1100 | while [ $# -gt 0 ]; do 1101 | COMMAND_ARG="$1" 1102 | case "$COMMAND_ARG" in 1103 | i | issue | p | pr) 1104 | shift 1105 | while getopts :c:eho:r:w flag; do 1106 | case "$flag" in 1107 | c) 1108 | if [[ $OPTARG =~ ^[0-9]+[smh]$ ]]; then 1109 | CACHE_TIME="$OPTARG" 1110 | else 1111 | _die_with_octocat "Invalid cache time: $OPTARG" 1112 | fi 1113 | ;; 1114 | e) REACTION_EMOJI="$(_emoji_picker)" ;; 1115 | h) 1116 | grep -Eq "i|issue" <<<"$COMMAND_ARG" && help_issue_function 1117 | grep -Eq "p|pr" <<<"$COMMAND_ARG" && help_pr_function 1118 | exit 0 1119 | ;; 1120 | o) 1121 | if [[ $OPTARG =~ ^[A-Za-z-]{7,}$ ]]; then 1122 | SORTING_ORDER="sort:$OPTARG" 1123 | else 1124 | _die_with_octocat "Invalid sort order: $OPTARG" "Check the readme or this link: https://docs.github.com/en/search-github/searching-on-github" 1125 | fi 1126 | ;; 1127 | r) 1128 | # local syntax test first to fail fast, and then gh api test 1129 | # curl is faster, but unauthenticated API requests to GitHub are capped at 60 per hour. Authenticated API requests 5000 per hour. 1130 | if [[ $OPTARG =~ ^[^:/]+/[^:/]+$ ]]; then 1131 | OWNER_REPO_NAME="$OPTARG" 1132 | else 1133 | _die_with_octocat "Respect the syntax: gh look {issue,pr} -R OWNER/REPO" 1134 | fi 1135 | if ! gh api --silent repos/"$OWNER_REPO_NAME" 2>/dev/null; then 1136 | _die_with_octocat "No GitHub repo called: $OWNER_REPO_NAME" 1137 | fi 1138 | ;; 1139 | w) PREVIEW_WINDOW_VISIBILITY="nohidden" ;; 1140 | *) 1141 | grep -Eq "i|issue" <<<"$COMMAND_ARG" && _die_with_octocat "Invalid command:" "$(help_issue_function)" 1142 | grep -Eq "p|pr" <<<"$COMMAND_ARG" && _die_with_octocat "Invalid command:" "$(help_pr_function)" 1143 | ;; 1144 | esac 1145 | done 1146 | # shift all processed options away, all that's left "$*" are non-options (aka mass-arguments/operands) 1147 | shift "$((OPTIND - 1))" 1148 | INITIAL_QUERY="$*" 1149 | if [ -z "$OWNER_REPO_NAME" ]; then 1150 | # if the OWNER_REPO_NAME is not set yet, check if the repo has a remote and assign the variable 1151 | git ls-remote --get-url >/dev/null 2>&1 || _die_with_octocat "No remote git repo found" 1152 | # local alternative, but requires Perl-compatible regular expressions (PCREs) 1153 | # git remote -v | grep -m 1 -oP '(?<=\:).+(?=\.)' 1154 | OWNER_REPO_NAME="$(gh api graphql --cache 1h --field owner=:owner --field name=:repo --raw-field query=$'query ($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { nameWithOwner }}' --jq '.[].repository.nameWithOwner')" 1155 | fi 1156 | 1157 | grep -Eq "i|issue" <<<"$COMMAND_ARG" && fzf_issue_function 1158 | grep -Eq "p|pr" <<<"$COMMAND_ARG" && fzf_pr_function 1159 | break 1160 | ;; 1161 | r | run) 1162 | shift 1163 | while getopts "hi:n:pr:w" option; do 1164 | case "$option" in 1165 | h) 1166 | help_workflow_run_function 1167 | exit 0 1168 | ;; 1169 | i) 1170 | if [[ $OPTARG =~ ^[0-9]+$ ]]; then 1171 | UPDATE_TIME=${OPTARG} 1172 | else 1173 | _die_with_octocat "Invalid update time: $OPTARG" 1174 | fi 1175 | ;; 1176 | n) NUMBER_WORKFLOW_RUN_LIST=${OPTARG} ;; 1177 | p) EXCLUDE_PULL_REQUESTS=false ;; 1178 | r) 1179 | if [[ $OPTARG =~ ^[^:/]+/[^:/]+$ ]]; then 1180 | OWNER_REPO_NAME="$OPTARG" 1181 | else 1182 | _die_with_octocat "Respect the syntax: gh look {issue,pr} -R OWNER/REPO" 1183 | fi 1184 | if ! gh api --silent repos/"$OWNER_REPO_NAME" 2>/dev/null; then 1185 | _die_with_octocat "No GitHub repo called: $OWNER_REPO_NAME" 1186 | fi 1187 | ;; 1188 | w) PREVIEW_WINDOW_VISIBILITY="nohidden" ;; 1189 | *) 1190 | _die_with_octocat "Invalid command:" "$(help_workflow_run_function)" 1191 | ;; 1192 | esac 1193 | done 1194 | shift "$((OPTIND - 1))" 1195 | test=$(gh repo set-default --view "$(git config --get remote.origin | cut -d: -f2 | cut -d. -f1)" 2>/dev/null || true) 1196 | # "set-default --view" always returns a zero exit value, the workaround is to test for any spaces in the string 1197 | [[ -z ${OWNER_REPO_NAME} ]] && [[ ! $test =~ \ ]] && OWNER_REPO_NAME="$test" 1198 | 1199 | if [[ -z ${OWNER_REPO_NAME} ]]; then 1200 | remote_choice="$(git remote -v | cut -d: -f2 | cut -d. -f1 | sort -u)" 1201 | if [[ -z $remote_choice ]]; then 1202 | _die_with_octocat "No Git remotes found." 1203 | elif [[ $(wc -l <<<"$remote_choice") -eq 1 ]]; then 1204 | OWNER_REPO_NAME="$remote_choice" 1205 | elif [[ $(wc -l <<<"$remote_choice") -gt 1 ]]; then 1206 | # an empty lines is added because of '--print-query' 1207 | OWNER_REPO_NAME="$(_fzf_basic_options <<<"$remote_choice" \ 1208 | --header "For which repository would you like to see Workflow Runs ?" | 1209 | tr -d '\n')" 1210 | fi 1211 | fi 1212 | fzf_workflow_run_function 1213 | break 1214 | ;; 1215 | s | search) 1216 | shift 1217 | while getopts :c:hw flag; do 1218 | case "$flag" in 1219 | c) 1220 | if [[ $OPTARG =~ ^[0-9]+[smh]$ ]]; then 1221 | CACHE_TIME="$OPTARG" 1222 | else 1223 | _die_with_octocat "Invalid cache time: $OPTARG" 1224 | fi 1225 | ;; 1226 | h) 1227 | help_search_function 1228 | exit 0 1229 | ;; 1230 | w) PREVIEW_WINDOW_VISIBILITY="nohidden" ;; 1231 | *) 1232 | _die_with_octocat "Invalid command:" "$(help_search_function)" 1233 | ;; 1234 | esac 1235 | done 1236 | shift "$((OPTIND - 1))" 1237 | INITIAL_QUERY="$*" 1238 | fzf_search_function 1239 | break 1240 | ;; 1241 | st | star) 1242 | shift 1243 | while getopts :c:hu:w flag; do 1244 | case "$flag" in 1245 | c) 1246 | if [[ $OPTARG =~ ^[0-9]+[smh]$ ]]; then 1247 | CACHE_TIME="$OPTARG" 1248 | else 1249 | _die_with_octocat "Invalid cache time: $OPTARG" 1250 | fi 1251 | ;; 1252 | h) 1253 | help_star_function 1254 | exit 0 1255 | ;; 1256 | u) 1257 | # local syntax test first to fail fast, and then gh api test 1258 | if [[ $OPTARG =~ ^[^:/]+$ ]]; then 1259 | USER_NAME="$OPTARG" 1260 | else 1261 | _die_with_octocat "Respect the syntax: gh look star -u USER" 1262 | fi 1263 | # gh api --silent users/... would be shorter, but would result in a false positive for organizations 1264 | if ! gh api graphql --silent --raw-field USER_NAME="$USER_NAME" --raw-field query=$'query ($USER_NAME: String!) { user(login: $USER_NAME) { login }}' 2>/dev/null; then 1265 | _die_with_octocat "No USER called: $USER_NAME" 1266 | fi 1267 | ;; 1268 | w) PREVIEW_WINDOW_VISIBILITY="nohidden" ;; 1269 | *) 1270 | _die_with_octocat "Invalid command:" "$(help_star_function)" 1271 | ;; 1272 | esac 1273 | done 1274 | shift "$((OPTIND - 1))" 1275 | INITIAL_QUERY="$*" 1276 | # if the USER_NAME is not set yet, use your own. 1277 | if [ -z "$USER_NAME" ]; then 1278 | USER_NAME="$(gh api graphql --cache 1h --raw-field query='{viewer{login}}' --jq '.data.viewer.login')" 1279 | fi 1280 | fzf_star_function 1281 | break 1282 | ;; 1283 | *) 1284 | _die_with_octocat "Invalid command:" "$(help_general_function)" 1285 | ;; 1286 | esac 1287 | done 1288 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # gh look 4 | 5 | Drop an emoji, write comments, star repositories, check workflow progress, browse issue trackers, search for repositories, ... all interactively by combining `gh` with `fzf`. 6 | 7 | ```mermaid 8 | %% GitHub seems to not display fontawesome icons 9 | %% https://fontawesome.com/search 10 | %% https://mermaid.js.org/syntax/flowchart.html#basic-support-for-fontawesome 11 | 12 | flowchart LR 13 | subgraph Helper_Keys[ ] 14 | Help([fa:fa-circle-question ? ᐧ Help]) 15 | ESC([fa:fa-arrow-right-from-bracket esc ᐧ Quit]) 16 | end 17 | subgraph Overview[ ] 18 | direction LR 19 | Search([fa:fa-magnifying-glass Search]) -->|shift+left| Stars([fa:fa-user Stars]) 20 | Stars-->|shift+right| Search 21 | Search -->|shift+right| Issues([fa:fa-circle-dot Issues]) 22 | Issues -->|shift+left| Search 23 | subgraph Issue_and_PR[ ] 24 | Issues --> |shift+right|PullRequests([fa:fa-code-pull-request Pull Requests]) 25 | PullRequests --> |shift+left| Issues 26 | end 27 | Issue_and_PR -->|enter| Comments([fa:fa-comments Comments]) -->|esc| Issue_and_PR 28 | Workflows([fa:fa-circle-play Workflow Runs]) -->|shift+left| PullRequests 29 | PullRequests -->|shift+right| Workflows 30 | end 31 | 32 | linkStyle default stroke-width:0.4px 33 | classDef Subgraph_Empty_Style fill:transparent,stroke-width:0px 34 | class Overview,Helper_Keys Subgraph_Empty_Style 35 | style Issue_and_PR fill:transparent,stroke-width:0.5px,stroke:#5b387c90 36 | ``` 37 | 38 |
39 | 40 | --- 41 | 42 | ## 💻 Requirements 43 | - [bat](https://github.com/sharkdp/bat#installation) - preview looks better 44 | - [Fuzzy Finder (fzf)](https://github.com/junegunn/fzf#installation) - allow for interaction with listed data 45 | - [GitHub command line tool (gh)](https://github.com/cli/cli#installation) - get the data from Github 46 | - [Python](https://www.python.org) - used to open URLs on different operating systems (`python -m webbrowser `) 47 | 48 | ```zsh 49 | brew install fzf gh bat 50 | 51 | # install this extension 52 | gh ext install LangLangBart/gh-look 53 | # upgrade 54 | gh ext upgrade LangLangBart/gh-look 55 | # uninstall 56 | gh ext remove LangLangBart/gh-look 57 | ``` 58 | 59 | --- 60 | 61 | ## 👨‍💻 Usage 62 | 63 | ```sh 64 | gh look [Command] [Flags] [Search term] 65 | ``` 66 | 67 | | Command | Description | Example | 68 | | :-------- | :----------------------------- | :------------------------------------ | 69 | | i, issue | List Issues | gh look issue -r cli/cli involves:@me | 70 | | p, pr | List Pull Requests | gh look pr -h | 71 | | r, run | List Workflow Runs | gh look run -r microsoft/vscode -n 20 | 72 | | s, search | Search for GitHub Repositories | gh look search -w keycastr | 73 | | st, star | List Starred Repositories | gh look star -u ashtom | 74 | 75 | - see available `Flags` for each command with `gh look [Command] --help` or interactively with ? 76 | 77 | --- 78 | 79 | ## 💪 Contributing 80 | 81 | > [!NOTE] 82 | > _Pre-commit is a multi-language package manager for pre-commit hooks. You specify a list_ 83 | > _of hooks you want and **pre-commit manages the installation and execution** of any hook_ 84 | > _written in any language before every commit._ **Source:** [pre-commit 85 | > introduction](https://pre-commit.com/#introduction) 86 | 87 | ```sh 88 | # install the git hook scripts 89 | pre-commit install --hook-type commit-msg --hook-type pre-commit 90 | # pre-commit installed at .git/hooks/commit-msg 91 | # pre-commit installed at .git/hooks/pre-commit 92 | ``` 93 | 94 | --- 95 | 96 | ## 💁 FAQ 97 | 98 | ### Strange icons 99 | - [NERD FONT](https://www.nerdfonts.com/cheat-sheet) icons are being used. If you see some `strange` icons, follow the steps in the link to install a better font: [powerlevel10k#fonts](https://github.com/romkatv/powerlevel10k#fonts) 100 | 101 | ### Ordering options 102 | - to change the order in which elements are listed see for details: [GitHub Docs - Searching on GitHub](https://docs.github.com/en/search-github/searching-on-github) 103 | - Valid Ordering options: {author-date,committer-date,created,interactions,reactions,updated}-{desc,asc} 104 | --------------------------------------------------------------------------------