Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Adjust abbreviation lengths and version checking logic #82

Merged
merged 7 commits into from
Mar 19, 2024
210 changes: 117 additions & 93 deletions gh-notify
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ export WHITE_BOLD='\033[1m'
export exclusion_string='XXX_BOGUS_STRING_THAT_SHOULD_NOT_EXIST_XXX'
export filter_string=''
export num_notifications='0'
export only_participating_flag='false'
export include_all_flag='false'
export only_participating_flag=false
export include_all_flag=false
export preview_window_visibility='hidden'
export python_executable=''
# not necessarily to be exported, since they are not used in any child process
print_static_flag='false'
mark_read_flag='false'
print_static_flag=false
mark_read_flag=false
update_subscription_url=''

# ===================== basic functions =====================
Expand Down Expand Up @@ -112,12 +112,12 @@ while getopts 'e:f:n:u:pawhsr' flag; do
e) exclusion_string="${OPTARG}" ;;
f) filter_string="${OPTARG}" ;;
n) num_notifications="${OPTARG}" ;;
p) only_participating_flag='true' ;;
p) only_participating_flag=true ;;
u) update_subscription_url="${OPTARG}" ;;
a) include_all_flag='true' ;;
a) include_all_flag=true ;;
w) preview_window_visibility='nohidden' ;;
s) print_static_flag='true' ;;
r) mark_read_flag='true' ;;
s) print_static_flag=true ;;
r) mark_read_flag=true ;;
h)
print_help_text
exit 0
Expand All @@ -137,10 +137,8 @@ get_notifs() {
if [ "$num_notifications" != "0" ]; then
local_page_size=$num_notifications
fi
printf >&2 "." # "marching ants" because sometimes this takes a bit.
# Use '-F/--field' to pass a variable that is a number, Boolean, or null. Use '-f/--raw-field'
# for other variables.
# Playground to test jq: https://jqplay.org/
# "marching ants" because sometimes this takes a bit.
printf >&2 "."
gh api --header "$GH_REST_API_VERSION" --method GET notifications --cache=0s \
--field per_page="$local_page_size" --field page="$page_num" \
--field participating="$only_participating_flag" --field all="$include_all_flag" \
Expand Down Expand Up @@ -178,14 +176,14 @@ get_notifs() {
$time_sec | strflocaltime("%d/%b %H:%M")
end; "gray"),
owner_abbreviated: colored(
if (.repository.owner.login | length) > 11 then
.repository.owner.login | .[0:10] | tostring + "…"
if (.repository.owner.login | length) > 10 then
.repository.owner.login | .[0:9] | tostring + "…"
else
.repository.owner.login
end; "cyan"),
name_abbreviated: colored(
if (.repository.name | length) > 16 then
.repository.name | .[0:15] | tostring + "…"
if (.repository.name | length) > 13 then
.repository.name | .[0:12] | tostring + "…"
else
.repository.name
end; "cyan_bold"),
Expand Down Expand Up @@ -239,7 +237,7 @@ print_notifs() {
number=${url/*\//#}
fi
fi
printf "\n%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%b%s%b\t%s \t%s\n" \
printf "\n%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%b%s%b\t%s\t%s\n" \
"$iso8601" "$thread_id" "$thread_state" "$comment_url" "$repo_full_name" \
"$unread_symbol" "$timefmt" "$repo_abbreviated" "$type" "$GREEN" "$number" \
"$NC" "$reason" "$title"
Expand Down Expand Up @@ -406,7 +404,7 @@ select_notif() {
--no-multi \
--pointer="▶" \
--preview "view_notification {}" \
--preview-window "wrap:${preview_window_visibility}:50%:right:border-left:<65(wrap:${preview_window_visibility}:75%:down:border-top)" \
--preview-window "default:wrap:${preview_window_visibility}:60%:right:border-left" \
--print-query \
--prompt "GitHub Notifications > " \
--reverse \
Expand All @@ -422,17 +420,17 @@ select_notif() {
[[ -z $type ]] && exit 0
case "$expected_key" in
esc)
# quit with exit code 0; 'fzf' returns 130
# TODO: when updating to '0.38.0' use '--bind "esc:become:"'
# quit with exit code 0; 'fzf' returns 130 by default
exit 0
;;
ctrl-x)
if grep -qE "Issue|PullRequest" <<<"$type"; then
gh issue comment "$num" --repo "$repo_full_name"
mark_individual_read "$selected_line" || die "Failed to mark the notification as read."
else
printf "Writing comments is only supported for %bIssues%b and %bPullRequests%b.\n" "$WHITE_BOLD" "$NC" "$WHITE_BOLD" "$NC"
exit 1
fi
mark_individual_read "$selected_line" || die "Failed to mark the notification as read."
;;
*)
die "Unexpected key '$expected_key'"
Expand All @@ -445,102 +443,125 @@ check_version() {
local tool=$1 threshold=$2 on_error=${3:-die}
local user_version
declare -a ver_parts threshold_parts
user_version=$($tool --version 2>&1 | grep -Eo '[0-9]+(\.[0-9]+)*' | sed q)
user_version=$(command $tool --version 2>&1 |
command grep --color=never --extended-regexp --only-matching --regexp='[0-9]+(\.[0-9]+)*' |
command sed q)

IFS='.' read -ra ver_parts <<<"$user_version"
IFS='.' read -ra threshold_parts <<<"$threshold"

for i in "${!ver_parts[@]}"; do
if (("${ver_parts[i]}" < "${threshold_parts[i]}")); then
for i in "${!threshold_parts[@]}"; do
if ((i >= ${#ver_parts[@]})) || ((ver_parts[i] < threshold_parts[i])); then
$on_error "Your '$tool' version '$user_version' is insufficient. The minimum required version is '$threshold'."
elif ((ver_parts[i] > threshold_parts[i])); then
break
fi
done
}

gh_notify() {
local graphql_query_resource updated_state update_text
local graphql_mutation_update_subscription possibleTypes notifs

# Bail early if we aren't static and pre-reqs aren't found
if [ $print_static_flag = "false" ]; then
for python in python python3; do
if type -p $python >/dev/null; then
python_executable=$python
break
fi
done
if [ -z "$python_executable" ]; then
die "install 'python' or use the -s flag"
update_subscription() {
local graphql_query_resource=$'query ($url_input: URI!) {resource(url: $url_input) { ... on Subscribable { __typename id viewerCanSubscribe viewerSubscription }}}'
local graphql_mutation_update_subscription=$'mutation ($updated_state: SubscriptionState!, $node_id: ID!) { updateSubscription(input: {state: $updated_state, subscribableId: $node_id}) { subscribable { viewerSubscription }}}'
local graphql_query_subscribable=$'{ __type(name: "Subscribable") { possibleTypes { name }}}'
local updated_state update_text possibleTypes
if IFS=$'\t' read -r object_type node_id viewer_can_subscribe viewer_subscription < <(gh api graphql \
--raw-field url_input="$update_subscription_url" \
--raw-field query="$graphql_query_resource" \
--jq '.data.resource | map(.) | @tsv' 2>/dev/null); then
if [[ -z $object_type ]]; then
die "Your input appears to be an invalid URL: '$update_subscription_url'."
elif [[ $viewer_subscription != "SUBSCRIBED" && ! $viewer_can_subscribe ]]; then
die "You are not allowed to subscribe to this '$object_type'."
fi
# The enumValues for the 'SubscriptionState' are:
#"UNSUBSCRIBED" - "The User is only notified when participating or @mentioned."
#"SUBSCRIBED" - "The User is notified of all conversations."
#"IGNORED" - "The User is never notified."
case "$viewer_subscription" in
SUBSCRIBED)
updated_state="UNSUBSCRIBED"
update_text="Notifications disabled for this '$object_type'."
;;
IGNORED | UNSUBSCRIBED)
updated_state="SUBSCRIBED"
update_text="Notifications enabled for this '$object_type'."
;;
*)
# TODO: When a user makes a 'Custom' selection on what to watch in a repo via the
# Web UI. For instance, if a user chooses to only watch 'Releases', then
# 'viewer_subscription' becomes null for any URL associated with the entire repo and
# this error message is displayed. Currently, there is no workaround for this. Last
# checked: March '24.
die "The queried value for the current status is unknown: '$viewer_subscription'."
;;
esac

# NOTE: For example, if you "UNSUBSCRIBE" from an Issue but you are still
# "SUBSCRIBED" to the Repository where the Issue resides, the Issue's
# subscription status is automatically set to "IGNORED" and can never be set
# to "UNSUBSCRIBED" as long as you are "SUBSCRIBED" to the Repository. This is
# a design decision by GitHub.
updated_state=$(gh api graphql --raw-field updated_state="$updated_state" \
--raw-field node_id="$node_id" \
--raw-field query="$graphql_mutation_update_subscription" \
--jq '.data.updateSubscription.subscribable.viewerSubscription') ||
die "Failed GraphQL mutation to update the subscription status."
echo "The list of all your subscriptions is only available via the Web UI."
printf "%bhttps://github.com/notifications/subscriptions%b\n\n" "$DARK_GRAY" "$NC"
printf "%b%s%b: %b%s%b\n" "$GREEN" "$updated_state" "$NC" "$WHITE_BOLD" "$update_text" "$NC"
printf "%b%s%b\n" "$DARK_GRAY" "$update_subscription_url" "$NC"
exit 0
else
possibleTypes=$(gh api graphql --raw-field query="$graphql_query_subscribable" \
--jq '.data.__type.possibleTypes | map(.name) | join(", ")' ||
die "Failed GraphQL query for possibleTypes.")
die "$(
printf "The URL is not valid for subscription.\n"
printf "Valid subscription types: %b%s%b\n" "$WHITE_BOLD" "$possibleTypes" "$NC"
)"
fi
}

for tool in less fzf; do
if ! type -p $tool >/dev/null; then
die "install '$tool' or use the -s flag"
fi
done
check_version fzf "$MIN_FZF_VERSION"
gh_notify() {
local python_version notifs

if ! type -p gh >/dev/null; then
die "install 'gh'"
fi

if [[ -n $update_subscription_url ]]; then
graphql_query_resource=$'query ($url_input: URI!) {resource(url: $url_input) { ... on Subscribable { __typename id viewerCanSubscribe viewerSubscription }}}'
if IFS=$'\t' read -r object_type node_id viewer_can_subscribe viewer_subscription < <(gh api graphql \
--raw-field url_input="$update_subscription_url" --raw-field query="$graphql_query_resource" \
--jq '.data.resource | map(.) | @tsv' 2>/dev/null); then
if [[ $viewer_subscription != "SUBSCRIBED" && ! $viewer_can_subscribe ]]; then
die "You are not allowed to subscribe to this '$object_type'."
fi
case "$viewer_subscription" in
SUBSCRIBED)
updated_state="UNSUBSCRIBED"
update_text="Notifications disabled for this '$object_type'."
;;
IGNORED | UNSUBSCRIBED)
updated_state="SUBSCRIBED"
update_text="Notifications enabled for this '$object_type'."
;;
*)
die "ERROR: the queried value for the current status is unknown: $viewer_subscription"
;;
esac
graphql_mutation_update_subscription=$'mutation ($updated_state: SubscriptionState!, $node_id: ID!) { updateSubscription(input: {state: $updated_state, subscribableId: $node_id}) { subscribable { viewerSubscription }}}'
# NOTE: For example, if you "UNSUBSCRIBE" from an Issue but you are still
# "SUBSCRIBED" to the Repository where the Issue resides, the Issue's
# subscription status is automatically set to "IGNORED" and can never be set
# to "UNSUBSCRIBED" as long as you are "SUBSCRIBED" to the Repository. This is
# a design decision by GitHub. The enumValues for the 'SubscriptionState' are:
# "UNSUBSCRIBED" - "The User is only notified when participating or @mentioned."
# "SUBSCRIBED" - "The User is notified of all conversations."
# "IGNORED" - "The User is never notified."
updated_state=$(gh api graphql --raw-field updated_state="$updated_state" \
--raw-field node_id="$node_id" \
--raw-field query="$graphql_mutation_update_subscription" \
--jq '.data.updateSubscription.subscribable.viewerSubscription') ||
die "Failed GraphQL mutation to update the subscription status."
echo "The list with all your subscriptions is only available via the Web UI."
printf "%bhttps://github.com/notifications/subscriptions%b\n\n" "$DARK_GRAY" "$NC"
printf "%b%s%b: %b%s%b\n" "$GREEN" "$updated_state" "$NC" "$WHITE_BOLD" "$update_text" "$NC"
printf "%b%s%b\n" "$DARK_GRAY" "$update_subscription_url" "$NC"
exit 0
else
possibleTypes=$(gh api graphql --raw-field query='{ __type(name: "Subscribable") { possibleTypes { name }}}' \
--jq '.data.__type.possibleTypes | map(.name) | join(", ")' ||
die "Failed GraphQL query for possibleTypes.")
printf "The URL is invalid to subscribe to.\nValid subscription types: %b%s%b\n" "$WHITE_BOLD" "$possibleTypes" "$NC"
exit 1
fi
update_subscription
fi

if [ "$mark_read_flag" = "true" ]; then
if $mark_read_flag; then
mark_all_read "" || die "Failed to mark notifications as read."
echo "All notifications have been marked as read."
exit 0
fi

if ! $print_static_flag; then
for python_version in python python3; do
if type -p $python_version >/dev/null; then
python_executable=$python_version
break
fi
done
if [ -z "$python_executable" ]; then
die "install 'python' or use the -s flag"
fi

if ! type -p fzf >/dev/null; then
die "install 'fzf' or use the -s flag"
fi

check_version fzf "$MIN_FZF_VERSION"
fi

notifs="$(print_notifs)"
if [ -z "$notifs" ]; then
echo "$FINAL_MSG"
exit 0
elif [ $print_static_flag = "false" ]; then
elif ! $print_static_flag; then
select_notif "$notifs"
else
# remove unimportant elements from the static display
Expand All @@ -549,4 +570,7 @@ gh_notify() {
fi
}

gh_notify
# This will call the function only when the script is run, not when it's sourced
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
gh_notify
fi
34 changes: 19 additions & 15 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
# GitHub CLI Notification Extension
A [gh](https://github.com/cli/cli) extension to view your GitHub notifications from the command line.

https://github.com/meiji163/gh-notify/assets/92653266/ecb7f246-ea5e-452d-b114-587f05b931e4
https://github.com/meiji163/gh-notify/assets/92653266/b7d7fcdb-8a25-43fc-8f63-d11f30960084

</div>

## Install

Make sure you have [GitHub CLI (gh)](https://github.com/cli/cli#installation) installed.

```sh
# install
gh ext install meiji163/gh-notify
Expand All @@ -18,13 +20,11 @@ gh ext upgrade meiji163/gh-notify
gh ext remove meiji163/gh-notify
```

- to use `gh notify` interactively also install these tools
- [Fuzzy Finder (fzf)](https://github.com/junegunn/fzf#installation)
- [Less pager](http://greenwoodsoftware.com/less/download.html), is usually installed by
default on Linux and macOS
- [Python](https://www.python.org/) in cases where `gh` can't open the `URL` in your
browser, this oneliner is used as a cross-platform solution: `python -m webbrowser
<URL>`
To use `gh notify` interactively, install these tools as well:
- [Fuzzy Finder (fzf)](https://github.com/junegunn/fzf#installation) - This allows for
interaction with listed data.
- [Python](https://www.python.org/) - In cases where `gh` can't open the `URL` in your browser, this
one-liner is used as a cross-platform solution: `python -m webbrowser <URL>`

## Usage

Expand Down Expand Up @@ -81,13 +81,16 @@ gh notify [Flags]
## Customizations

### Fuzzy Finder (fzf)
Customize fzf key bindings by exporting `ENVIRONMENT VARIABLES` to your `.bashrc`/`.zshrc`. See the man page (`man fzf`) for `AVAILABLE KEYS/ EVENTS` or [junegunn/fzf#environment-variables](https://github.com/junegunn/fzf#environment-variables) on GitHub for more details.
You can customize the `fzf` key bindings by exporting `ENVIRONMENT VARIABLES` to your `.bashrc` or
`.zshrc`. For `AVAILABLE KEYS/ EVENTS`, refer to the `fzf` man page or visit
[junegunn/fzf#environment-variables](https://github.com/junegunn/fzf#environment-variables) on
GitHub.

- NOTE: [How to use ALT commands in a terminal on macOS?](https://superuser.com/questions/496090/how-to-use-alt-commands-in-a-terminal-on-os-x)
- **NOTE**: [How to use ALT commands in a terminal on macOS?](https://superuser.com/questions/496090/how-to-use-alt-commands-in-a-terminal-on-os-x)

```sh
# ~/.bashrc or ~/.zshrc
# The following examples allow you to clear the input query with alt+c,
# The examples below enable you to clear the input query with alt+c,
# jump to the first/last result with alt+u/d, refresh the preview window with alt+r
# and scroll the preview in larger steps with ctrl+w/s.
export FZF_DEFAULT_OPTS="
Expand All @@ -97,13 +100,14 @@ export FZF_DEFAULT_OPTS="
--bind 'ctrl-w:preview-half-page-up,ctrl-s:preview-half-page-down'"
```

### GitHub command line tool (gh)
In the config file of the `gh` tool you can set your preferred editor. This is handy when you use the <kbd>ctrl</kbd><kbd>x</kbd> hotkey to write a comment on a notification.
### GitHub Command Line Tool (gh)
In the `gh` tool's config file, you can specify your preferred editor. This is particularly useful
when you use the <kbd>ctrl</kbd><kbd>x</kbd> hotkey to comment on a notification.

```sh
# See more details
# To see more details
gh config
# For example, set the editor to Visual Studio Code or Vim.
# For example, you can set the editor to Visual Studio Code or Vim.
gh config set editor "code --wait"
gh config set editor vim
```
Loading