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

refactor: 'less' notification viewing logic #86

Merged
merged 8 commits into from
Jul 17, 2024
133 changes: 87 additions & 46 deletions gh-notify
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ set -o errexit -o nounset -o pipefail
# NotificationReason:
# assign, author, comment, invitation, manual, mention, review_requested, security_alert, state_change, subscribed, team_mention, ci_activity
# NotificationSubjectTypes:
# CheckSuite, Commit, Discussion, Issue, PullRequest, Release,
# RepositoryVulnerabilityAlert, ...
# CheckSuite, Commit, Discussion, Issue, PullRequest, Release, RepositoryVulnerabilityAlert, ...

# ====================== set variables =======================

Expand Down Expand Up @@ -43,7 +42,7 @@ export GH_NOTIFY_PER_PAGE_LIMIT=50
# Assign 'GH_NOTIFY_DEBUG_MODE' with 'true' to see more information
export GH_NOTIFY_DEBUG_MODE=${GH_NOTIFY_DEBUG_MODE:-false}
if $GH_NOTIFY_DEBUG_MODE; then
export gh_notify_debug_log="${BASH_SOURCE%/*}/gh_notify_debug.log"
export gh_notify_debug_log="${BASH_SOURCE[0]%/*}/gh_notify_debug.log"

# Tell the user where we saved the debug information
trap 'echo [DEBUG] $gh_notify_debug_log' EXIT
Expand All @@ -54,7 +53,8 @@ if $GH_NOTIFY_DEBUG_MODE; then
# Unset GH_FORCE_TTY to avoid unnecessary color codes in the debug file
unset GH_FORCE_TTY

# Redirect stdout and stderr to the terminal and a file
# Redirect stdout and stderr to the terminal and a file, in fzf 0.52.0+ the UI is no longer
# written to stderr: https://github.com/junegunn/fzf/discussions/3792
exec &> >(tee -a "$gh_notify_debug_log")

# [DISABLED] 'GH_DEBUG' sends the output to file descriptor 2, but these error messages can be
Expand Down Expand Up @@ -189,9 +189,13 @@ done

# ===================== helper functions ==========================

gh_rest_api() {
command gh api --header "$GH_REST_API_VERSION" --method GET --cache=0s "$@"
}

get_notifs() {
local page_num="$1"
command gh api --header "$GH_REST_API_VERSION" --method GET notifications --cache=0s \
gh_rest_api notifications \
--field per_page="$GH_NOTIFY_PER_PAGE_LIMIT" --field page="$page_num" \
--field participating="$only_participating_flag" --field all="$include_all_flag" \
--jq \
Expand All @@ -207,7 +211,15 @@ get_notifs() {
def colored(text; color):
colors[color] + text + colors.reset;
.[] | {
updated_short: .updated_at | fromdateiso8601 | strftime("%Y-%m"),
updated_short:
# for some reason ".updated_at" can be null
if .updated_at then
.updated_at | fromdateiso8601 | strftime("%Y-%m")
else
# Github Discussion launched in 2020
# https://resources.github.com/devops/process/planning/discussions/
"2020"
end,
# UTC time ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
# https://docs.github.com/en/rest/overview/resources-in-the-rest-api#timezones
iso8601: now | strftime("%Y-%m-%dT%H:%M:%SZ"),
Expand All @@ -217,15 +229,21 @@ get_notifs() {
repo_full_name: .repository.full_name,
unread_symbol: colored((if .unread then "\u25cf" else "\u00a0" end); "magenta"),
# make sure each outcome has an equal number of fields separated by spaces
timefmt: colored(((if .unread then .last_read_at // .updated_at else .updated_at end) | fromdateiso8601) as $time_sec |
# difference is less than one hour
if ((now - $time_sec) / 3600) < 1 then
(now - $time_sec) / 60 | floor | tostring + "min ago"
# difference is less than 24 hours
elif ((now - $time_sec) / 3600) < 24 then
(now - $time_sec) / 3600 | floor | tostring + "h ago"
timefmt: colored(
# for some reason ".updated_at" can be null
if (.unread and .last_read_at) or .updated_at then
((if .unread then .last_read_at // .updated_at else .updated_at end) | fromdateiso8601) as $time_sec |
# difference is less than one hour
if ((now - $time_sec) / 3600) < 1 then
(now - $time_sec) / 60 | floor | tostring + "min ago"
# difference is less than 24 hours
elif ((now - $time_sec) / 3600) < 24 then
(now - $time_sec) / 3600 | floor | tostring + "h ago"
else
$time_sec | strflocaltime("%d/%b %H:%M")
end
else
$time_sec | strflocaltime("%d/%b %H:%M")
"Not available"
end; "gray"),
owner_abbreviated: colored(
if (.repository.owner.login | length) > 10 then
Expand Down Expand Up @@ -337,11 +355,8 @@ process_url() {
# https://blog.cuviper.com/2013/11/10/how-short-can-git-abbreviate/
command basename "$url" | command head -c 12
elif command grep -q "Release" <<<"$type"; then
if IFS=$'\t' read -r number prerelease < <(command gh api "$url" \
--cache=100h \
--header "$GH_REST_API_VERSION" \
--method GET \
--jq '[.tag_name, .prerelease] | @tsv'); then
if IFS=$'\t' read -r number prerelease < <(gh_rest_api "$url" \
--cache=100h --jq '[.tag_name, .prerelease] | @tsv'); then
if "$prerelease"; then
echo "$number Pre-release"
else
Expand Down Expand Up @@ -372,7 +387,7 @@ process_discussion() {
command gh api graphql \
--cache=100h \
--raw-field query="$graphql_query_discussion" \
--raw-field filter="$title in:title updated:>=$updated_short repo:$repo_full_name" \
--raw-field filter="\"$title\" in:title updated:>=$updated_short repo:$repo_full_name" \
--jq '.data.search.nodes | "#\(.[].number)"' || die "Failed GraphQL discussion query."
}

Expand Down Expand Up @@ -424,16 +439,16 @@ open_in_browser() {

view_notification() {
local all_comments date time repo_full_name type number
if [ "$1" = "--all_comments" ]; then
if [[ $1 == "--all_comments" ]]; then
shift
all_comments="1"
fi
IFS=' ' read -r _ _ _ _ repo_full_name _ date time _ type number _ <<<"$1"
printf "[%s %s - %s]\n" "$date" "$time" "$type"
case "$type" in
Commit)
command gh api --header "$GH_REST_API_VERSION" --cache=24h \
--method GET "repos/$repo_full_name/commits/$number" --jq '.files[].patch' | highlight_output
gh_rest_api --cache=24h "repos/$repo_full_name/commits/$number" \
--jq '.files[].patch' | highlight_output
;;
Issue)
# use the '--comments' flag only if 'all_comments' exists and is not null
Expand All @@ -451,41 +466,66 @@ view_notification() {
esac
}

view_in_pager() {
local repo_full_name type number unhashed_num total_comments
local issue_or_pr="issues"
IFS=' ' read -r _ _ _ _ repo_full_name _ _ _ _ type number _ <<<"$1"
declare -a less_args
# The long option (--+…) for resetting the option to its default setting is broken in less
# version 643, so use only the short version. Ref: https://github.com/gwsw/less/issues/452
less_args=(
"--clear-screen" # to be painted from the top line down
"--RAW-CONTROL-CHARS" # Raw color codes in output (don't remove color codes)
"-+F" # disable exiting if the entire file can be displayed on the screen
"-+X" # reset screen clearing prevention
)

# Move to the end of the file only for Issues or PRs that have comments.
case "$type" in
Issue | PullRequest)
unhashed_num=$(command tr -d "#" <<<"$number")
[[ $type == "PullRequest" ]] && issue_or_pr="pulls"

if total_comments=$(gh_rest_api \
"repos/${repo_full_name}/${issue_or_pr}/${unhashed_num}" \
--jq '.comments' 2>/dev/null); then
if ((total_comments > 0)); then
less_args+=(
"+G" # start at the end of the file
)
fi
fi
;;
esac

# Redirect 'less' output to '/dev/tty' to interact with the terminal when in command
# substitution '$()'. Ref: https://github.com/junegunn/fzf/issues/1360#issuecomment-966054123
view_notification --all_comments "$1" | command less "${less_args[@]}" >/dev/tty
}

mark_all_read() {
local iso_time
IFS=' ' read -r iso_time _ <<<"$1"
# https://docs.github.com/en/rest/activity/notifications#mark-notifications-as-read
command gh api --silent --header "$GH_REST_API_VERSION" --method PUT notifications \
gh_rest_api --silent --method PUT notifications \
--raw-field last_read_at="$iso_time" --field read=true
}

mark_individual_read() {
local thread_id thread_state
IFS=' ' read -r _ thread_id thread_state _ <<<"$1"
if [ "$thread_state" = "UNREAD" ]; then
command gh api --silent --header "$GH_REST_API_VERSION" --method PATCH "notifications/threads/${thread_id}"
if [[ $thread_state == "UNREAD" ]]; then
gh_rest_api --silent --method PATCH "notifications/threads/${thread_id}"
fi
}

select_notif() {
declare -a less_args
# The long option (--+…) for resetting the option to its default setting is broken in
# less version 643, so only use the short version.
# Ref: https://github.com/gwsw/less/issues/452
less_args=(
"--clear-screen" # to be painted from the top line down
"--RAW-CONTROL-CHARS" # Raw color codes in output (don't remove color codes)
"-+F" # reset exiting if the entire file can be displayed on the first screen
"-+X" # reset screen clearing prevention
)

local output expected_key selected_line repo_full_name type num
# make functions available in child processes
# 'SHELL="$(which bash)"' is needed to use exported functions when the default shell
# is not bash
# Export functions to child processes. 'fzf' executes commands with $SHELL -c; to ensure
# compatibility when the default shell is not bash, set 'SHELL="$(which bash)"'.
export -f print_help_text print_notifs get_notifs
export -f process_page process_discussion process_url
export -f highlight_output open_in_browser view_notification
export -f process_page process_discussion process_url gh_rest_api
export -f highlight_output open_in_browser view_notification view_in_pager
export -f mark_all_read mark_individual_read
# The 'die' function is not exported because 'fzf' warns you about the error in
# a failed 'print_notifs' call, but does not display the message.
Expand All @@ -505,7 +545,7 @@ select_notif() {
--bind "${GH_NOTIFY_VIEW_PATCH_KEY}:toggle-preview+change-preview:if command grep -q PullRequest <<<{10}; then command gh pr diff {11} --patch --repo {5} | highlight_output; else view_notification {}; fi" \
--bind "${GH_NOTIFY_RELOAD_KEY}:reload:print_notifs || true" \
--bind "${GH_NOTIFY_MARK_READ_KEY}:execute-silent(mark_individual_read {})+reload:print_notifs || true" \
--bind "${GH_NOTIFY_VIEW_KEY}:execute:view_notification --all_comments {} | less ${less_args[*]} >/dev/tty" \
--bind "${GH_NOTIFY_VIEW_KEY}:execute:view_in_pager {}" \
--bind "${GH_NOTIFY_TOGGLE_PREVIEW_KEY}:toggle-preview+change-preview:view_notification {}" \
--bind "${GH_NOTIFY_TOGGLE_HELP_KEY}:toggle-preview+change-preview:print_help_text" \
--border horizontal \
Expand Down Expand Up @@ -543,7 +583,8 @@ select_notif() {
command 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"
printf "Writing comments is only supported for %bIssues%b and %bPullRequests%b.\n" \
"$WHITE_BOLD" "$NC" "$WHITE_BOLD" "$NC"
exit 1
fi
;;
Expand Down Expand Up @@ -661,7 +702,7 @@ gh_notify() {
break
fi
done
if [ -z "$python_executable" ]; then
if [[ -z $python_executable ]]; then
die "install 'python' or use the -s flag"
fi

Expand All @@ -673,7 +714,7 @@ gh_notify() {
fi

notifs="$(print_notifs)"
if [ -z "$notifs" ]; then
if [[ -z $notifs ]]; then
echo "$FINAL_MSG"
exit 0
elif ! $print_static_flag; then
Expand Down