#!/usr/bin/bash
# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
set -Eeuo pipefail #-x
die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; }
ABSPATHSCRIPT=$(readlink -f "$0")	# Resolve symlinks to find installdir

# == Dependencis & Imports ==
source "${ABSPATHSCRIPT%/*}"/../libexec/jj-fzf/preflight.sh
# Import jj_show_diff and helpers
source "${ABSPATHSCRIPT%/*}"/../libexec/jj-fzf/common.sh

# == Early options, outside a jj repo ==
COLORALWAYS=
while test $# -ne 0 ; do
  case "$1" in \
    --version)		echo jj-fzf 0.33.0 2025-09-26T14:10:01Z; exit ;;
    -h|--help)		exec man jj-fzf ;;
    --color=always)	COLORALWAYS=t ;;
    *)         		break ;;
  esac
  shift
done

# == Subdirs may vanish during checkouts ==
JJPWD="$PWD"
JJROOT=$(jj --ignore-working-copy root) &&
  cd "$JJROOT" ||	# always ensure root relative paths
    die "$PWD: not a JJ repository"

# == PREVIEW fast path ==
# handle 'preview' early on to avoid slow previews due to parsing the entire script
export REVPAT='^[^a-z()0-9]*([k-xyz]{7,})([?]*)\ '		# line start, ignore --graph, parse revision letters, catch '??'-postfix
export OPRPAT='^[^a-z0-9]*([0-9a-f]{9,})[?]*\ '	# line start, ignore --graph, parse hex letters, space separator
export HEX7PAT='\ ([0-9a-f]{7,})\ ' # space enclosed hexadecimal pattern

# == General Options ==
MULTISELECT= HELPKEYBINDINGS= SHOWKEYBINDINGS= COLORALWAYS= ONESHOT=false POSTCMDEXIT=
while test $# -ne 0 ; do
  case "$1" in \
    --help-bindings)	HELPKEYBINDINGS=t ;;
    -m)			MULTISELECT=-m ;;
    +m)			MULTISELECT= ;;
    --postcmd-exit)	POSTCMDEXIT=1 ;;
    --key-bindings)	SHOWKEYBINDINGS=t ;;
    --oneshot)		ONESHOT=true ;; # auto-exit after first command
    --color=always)	COLORALWAYS=t ;;
    *)         		break ;;
  esac
  shift
done

# == Config ==
export FZF_DEFAULT_OPTS=	# prevent user defaults from messing up the layout
declare -A DOC
# JJ repository
JJFZFSHOW="jj --no-pager --ignore-working-copy show --tool true"
JJFZFONELINE="jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline"
JJFZFPAGER="less -Rc"
JJSUBSHELL='T=$(tty 2>/dev/null||tty <&1 2>/dev/null||tty <&2 2>/dev/null) && test -n "$T" && echo -e "\n#\n# Type \"exit\" to leave subshell\n#" &&
            unset FZF_DEFAULT_COMMAND && { test ! -d "'"$JJPWD"'" || cd "'"$JJPWD"'"; } && exec /usr/bin/env '"$SHELL"' <$T 1>$T 2>$T'
INFO_BINDING=" fzf </dev/null >/dev/tty 2>&1 --prompt '        '  --disabled --layout=reverse --height 1 --margin 4 --padding 4 --border=block --no-info --no-scrollbar --no-clear --bind=enter:print-query "
FUNCTIONS=()
FZFSETTINGS=(
  --ansi --no-mouse -x -e --track
  --info default
  --layout reverse-list
  --scroll-off 3
  --bind "alt-up:offset-up"
  --bind "alt-down:offset-down"
  --bind "ctrl-x:jump"
  --bind "ctrl-z:execute( $JJSUBSHELL )"
  --bind='f11:change-preview-window(bottom,75%,border-horizontal|)'
  --preview-window 'wrap,right,border-left'
  --bind=ctrl-alt-x:"execute-silent($INFO_BINDING)+clear-screen"
)
FZFPOPUP=(fzf "${FZFSETTINGS[@]}" --margin "0,3%,5%,3%" --border)
TEMPD=
# for function exports to work sub-shell must be bash too
export SHELL=bash

# == JJ CONFIG ==
# parsable version of builtin_log_oneline; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_ONELINE='
if(root,
  format_root_commit(self),
  label(if(current_working_copy, "working_copy"),
    concat(
      separate(" ",
        format_short_change_id_with_hidden_and_divergent_info(self),
        if(author.email(), author.email().local(), email_placeholder),
        author.timestamp().local().format("%Y-%m-%d"),
        format_short_commit_id(commit_id),
        bookmarks,
        tags,
        working_copies,
        if(git_head, label("git_head", "git_head()")),
        if(conflict, label("conflict", "conflict")),
        if(empty, label("empty", "(empty)")),
        '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', '🌟')), }"'
        if(description,
          description.first_line(),
          label(if(empty, "empty"), description_placeholder),
        ),
      ) ++ "\n",
    ),
  )
)'

# == Utils ==
# Create temporary dir, assigns $TEMPD
temp_dir()
{
  test -n "$TEMPD" || {
    TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/jj-fzf.pid
  }
}
# Match JJ revision as first ASCII word (e.g. as in builtin_log_oneline)
export OPPAT='^[^a-z()0-9]*([0-9a-f]{9,})\ '
# Try to extract non-divergent revision or parse expression
xrev_maybe()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RV='@' || RV=
  # or match abbreviated change_id pattern
  if test -z "$RV" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECHANGE='if(self.divergent(), "", change_id)'
    # only allow non-divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECHANGE" 2>/dev/null) || :
  fi
  # or match syntactically valid expressions
  test -z "$RV" && # divergent matches produce concatenated change_ids
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T change_id 2>/dev/null) || :
  # final validation that $RV is indeed a unique identifier for a non-divergent change_id
  test -n "$RV" &&
    jj --no-pager --ignore-working-copy log --no-graph -T change_id -r "$RV" 2>/dev/null # pass on exit status
)
# Extract non-divergent revision or show error
xrev()
(
  xrev_maybe "$@" ||
    ERROR "failed to parse revision: ${1:-}"
)
FUNCTIONS+=( 'xrev' )
# Extract commit_id or show error
xrev_as_commit()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RC='@' || RC=
  # or match abbreviated change_id pattern
  if test -z "$RC" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECOMMIT='if(self.divergent(), "", commit_id)'
    # check for divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECOMMIT" 2>/dev/null) || :
    test -n "$RC" ||	# non-divergent, else fallback to commit hash parsing
      RC=$(echo " $* " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || :
  fi
  # or match syntactically valid expressions
  test -z "$RC" && # divergent matches produce concatenated commit_ids
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T commit_id 2>/dev/null) || :
  # final validation that $RC is indeed a unique identifier for a single commit
  test -n "$RC" &&
    jj --no-pager --ignore-working-copy log --no-graph -T commit_id -r "$RC" 2>/dev/null ||
      ERROR "failed to parse commit id: ${1:-}"
)
FUNCTIONS+=( 'xrev_as_commit' )
# Yield the revision change_id or a commit_id if it is divergent
xrev_or_commit()
(
  xrev_maybe "$@" ||
    xrev_as_commit "$@"
)
FUNCTIONS+=( 'xrev_or_commit' )
# Fill <ARRAY> with <PREFIX> and xrev_or_commit from $@
parse_xrevs_or_commits()
{
  local -n _parsed_REVS=$1	# nameref to the target array
  local _prefix_REV=$2
  shift 2
  test $# -ne 0 && _parsed_REVS=() || _parsed_REVS=($_prefix_REV @)
  while test $# -ne 0; do
    local _rev
    _parsed_rev=$(xrev_or_commit "$1") || die "no such revision: $1"
    _parsed_REVS+=($_prefix_REV "$_parsed_rev")
    shift
  done
}

# Look up full commit hash via JJ commit_id
rev_commitid()	( xrev_as_commit "$@" )
# Print first bookmark or the revision itself
rev_bookmark1()	( $JJFZFSHOW -T 'concat(separate(" ",bookmarks), " ", change_id)' -r "$1" | awk '{print $1;}' )
# Get revision description
rev_description() ( $JJFZFSHOW -T 'concat(description)' -r "$1" )

# Condense commit empty/description/parent state into a key word
rev_edpstate()
(
  export EDPSTATE='separate("-", if(empty, "empty", "diff"), if(description, "description", "silent"), "p" ++ self.parents().len()) ++ "\n"'
  $JJFZFSHOW -r "$1" -T "$EDPSTATE" # empty-description-p2 diff-silent-p1 etc
)

# List parents of a revision
rev_parents()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "$1-" -T 'change_id++"\n"'
)

# List commits of a revset
list_revset()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "$* " -T 'commit_id++"\n"'
)

# join_args <joiner> [args...]
join_args()
{
  local j="${1:-}" first="${2:-}"
  if shift 2; then
    printf "%s" "$first" "${@/#/$j}"
  fi
}

# reverse_array ORIG REVERSED - copy the elements from ORIG in REVERSED in reverse order
reverse_array()
{
  local -n array_O_=$1
  local -n array_R_=$2
  # Loop in reverse order
  for ((i=${#array_O_[@]}-1; i>=0; i--)); do
    array_R_+=("${array_O_[i]}")
  done
}

# diff_arrays BEFORE AFTER RESULT - store the elements from AFTER without elements from BEFORE in RESULT
diff_arrays()
{
  local -n array_O_=$1
  local -n array_N_=$2
  local -n array_R_=$3
  declare -A counts_
  # Mark elements in A
  for elem in "${array_O_[@]}" ; do
    counts_["$elem"]=1
  done
  # Add all of B to C if not in A
  for elem in "${array_N_[@]}"; do
    test -z "${counts_[$elem]:-}" &&
      array_R_+=("$elem") # || echo "SKIP: $elem : ${counts_[$elem]:-}"
  done
  true
}

# backward_chronologic [REVISIONS] - produce revisions in backwards chronological order
backward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r "$ORREVS" -T 'change_id ++ "\n"'
)

# forward_chronologic [REVISIONS] - produce revisions in chronological order
forward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r "$ORREVS" -T 'change_id ++ "\n"' --reversed
)

# Require .git directory and set GIT_DIR
require_git_dir()
{
  test -e "$JJROOT/.git" &&
    export GIT_DIR="$JJROOT/.git" || {
      test -e "$JJROOT/.jj/repo/store/git" &&
	export GIT_DIR="$JJROOT/.jj/repo/store/git" ||
	  die "$PWD: failed to find .git store"
    }
}

# Write revision from `jj new -m $3 --no-edit -B $2` to $1
jj_new_before_no_edit()
{
  local -n result_=$1 # nameref
  local R="$(xrev "${2:-}")" # must use revision to find new parents
  local M="${3:-}"
  # record base commit parents before/after
  local A=( $(rev_parents "$R") )
  ( set -x
    jj new --no-edit --message="$M" --insert-before "$R" # --no-pager
  ) || die
  local B=( $(rev_parents "$R") )
  local C=() && diff_arrays A B C
  [ ${#C[@]} -eq 1 ] ||
    die "failed to find newly created revision"
  result_="${C[0]}"
}

# Exit the current shell with an error message and delay
ERROR()
{
  FUNC="${FUNC:-$0}"
  echo "ERROR: ${FUNC:+$FUNC:}" "$*" >&2
  # Wait a few seconds unless the user presses Enter
  read -t "${JJ_FZF_ERROR_DELAY:-2}"
  exit
}

# == Helpers ==
# Echo signoff
echo_signoff()
(
  JJFZF_SIGNOFF=true	# config get jjfzf.signoff
  if test "${JJFZF_SIGNOFF:-true}" == true ; then
    echo # separating newline before signoff section
    $JJFZFSHOW -T 'format_detailed_signature(author) ++ "\n"' -r @ |
      sed -e 's/>.*/>/ ; s/^/Signed-off-by: /'
  fi
)
# Echo current or default message
echo_commit_msg()
(
  R="$1"
  if test "$R" != --merge ; then
    S=$(rev_edpstate "$R")
    # keep any existing message
    [[ $S =~ -silent- ]] || {
      rev_description "$R"
      return
    }
    # list parents
    PARENTS=( $(jj --no-pager --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r "$R-" --reversed) )
  else # --merge
    shift
    PARENTS=( $(forward_chronologic "$@") )
  fi
  # Create merge message
  if test "$R" == --merge -o "${#PARENTS[@]}" -ge 2 ; then
    SEP="^^^^^^^^^"
    NEWCOMMITS=()
    for p in "${PARENTS[@]}"; do
      NEWCOMMITS+=( $(rev_commitid $p) )
    done
    MERGE_BASE=$(git merge-base --octopus "${NEWCOMMITS[@]}")
    echo -e "# $SEP DRAFT:  merge" "${PARENTS[@]}" "$SEP # DELETE THIS"
    test "${#PARENTS[@]}" -le 2 &&
      echo "Merge branch '`rev_bookmark1 ${PARENTS[1]}`' into '`rev_bookmark1 ${PARENTS[0]}`'" ||
	echo "Merge branches:" "${PARENTS[@]}"
    for c in "${NEWCOMMITS[@]}"; do
      test "$c" == "$MERGE_BASE" && continue
      test "${#PARENTS[@]}" -le 2 &&
	echo -e "\n* Branch commit log:" || # "$c ^$MERGE_BASE"
	  echo -e "\n* Branch '`rev_bookmark1 $c`' commit log:"
      git log --pretty=$'\f%s%+b' $c ^$MERGE_BASE |
	sed '/^\([A-Z][a-z0-9-]*-by\|Cc\):/d' | # strip Signed-off-by:
	sed '/^$/d ; s/^/\t/ ; s/^\t\f$/  (no description)/ ; s/^\t\f/  /'
    done
    echo_signoff
  else # Commit message based on files
    # start with file name prefixes
    FILES=()
    readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | sed 's/^\w //')
    test ${#FILES[@]} -gt 0 &&
      printf "%s: \n" "${FILES[@]}" ||
	echo ""
    { jj config --no-pager get 'ui.default-description' 2>/dev/null || : ; } | sed '1{/^$/d}'
    test ${#FILES[@]} -le 0 ||
      echo_signoff
  fi
)
# Run user editor: user_editor_on_var <FILE> <VARIABLE> [COMMIT]
user_editor_on_var()
{
  local FILE="$1" COMMIT="${3:-}" N=
  declare -n _ueovMSG="$2"			# <VARIABLE> alias
  # create msg file
  temp_dir
  local TEMPFILE="$TEMPD/$FILE"
  cat >"$TEMPFILE" <<<"$_ueovMSG"
  test -z "$COMMIT" || {
    jj_show_diff --color=never -T '"\n"' -r "$COMMIT" |
      head -n 4000 > "$TEMPFILE.diff"
    test -s "$TEMPFILE.diff" && {
      echo
      echo '# -------- >8 -------- >8 -------- 8< -------- 8< --------'
      echo '# Everything below the snippet mark will be ignored'
      echo '#'
      echo '# Content diff of this revision:'
      cat "$TEMPFILE.diff"
    }
    rm -f "$TEMPFILE.diff"
  } >> "$TEMPFILE"
  # edit commit msg
  test -n "${JJ_EDITOR:-}" || # https://jj-vcs.github.io/jj/latest/config/#editor
    JJ_EDITOR="$(jj config get ui.editor 2>/dev/null || echo "${VISUAL:-${EDITOR:-pico}}")"
  $JJ_EDITOR "$TEMPFILE" &&
    N="$(cat "$TEMPFILE")" && {
      test "$_ueovMSG" != "$N" &&
	_ueovMSG="$(sed -r '/^# -+ >8 -+ >8 -+ 8< -+ 8< -+/Q' < "$TEMPFILE")"
      rm -f "$TEMPFILE"
      return 0
    }
  rm -f "$TEMPFILE"
  return 1
}
# Read input with completion: RESULT="$(PROMPT=… INIT=… read_completing [words…])"
read_completing()
(
  WORDS=( "$@" )
  _read_completion() {
    local line="$READLINE_LINE" point="$READLINE_POINT"
    local cur="${line:0:$point}" # Cut word at point
    # Extract current completion word
    cur="${cur##* }"
    # Generate completions
    local compreply=( $(compgen -W "${WORDS[*]}" -- "${cur}" || :) )
    if test ${#compreply[@]} -ne 1 ; then
      printf "%s\n" "${compreply[@]}" | column >&2
    else # Use unique completion
      local oldlen=${#cur}
      # Replace current word with the completion
      READLINE_LINE="${line:0:$((point - oldlen))}${compreply[0]}${line:$point}"
      READLINE_POINT=$(( point + ${#READLINE_LINE} - ${#line} ))
    fi
    true # Return false aborts readline
  }
  set -o emacs # Use emacs readline mode
  bind -x '"\t": _read_completion'
  READOPTS=()
  test -z "${PROMPT:-}" || READOPTS+=(-p "$PROMPT")
  test -z "${INIT:-}" || READOPTS+=(-i "$INIT")
  read -e "${READOPTS[@]}" INPUT
  test -z "$INPUT" ||
    printf "%s\n" "$INPUT"
)

# == Functions ==
declare -A KEYBINDINGS
declare -A MULTIKEYBINDINGS
declare -A POSTCMDS
FIRSTS=""
NEXTS=""
SILENTREFRESH=""

# get_REVSETCONFIG - setup array with config CLI args for the fzflog revset
get_REVSETCONFIG()
{
  local -n __l_array=$1	# nameref to an array
  local FZFLOG_DEPTH="$(jj --ignore-working-copy config get jj-fzf.fzflog-depth 2>/dev/null || echo 0)"
  __l_array+=( --config "revset-aliases.fzflog=''' jjlog | ancestors(bookmarks() | remote_bookmarks(), $FZFLOG_DEPTH) '''")
  __l_array+=( --config "revset-aliases.jjlog=''' $(jj --ignore-working-copy config get revsets.log 2>/dev/null || echo ..) '''")
  __l_array+=( --config "template-aliases.'format_short_change_id(id)'='id.shortest(8)'")
  __l_array+=( --config "template-aliases.'format_short_commit_id(id)'='id.shortest(8)'")
}

# fzflog [--revsetname] [rev] - revision log for fzf
fzflog()
(
  # SEE ALSO: jj config get revsets.log
  [[ "${1:-}" == --revsetname ]] && { REVSETNAME=true; shift; } || REVSETNAME=false
  [[ $# -ge 1 ]] &&
    REVSETS_LOG="$1" ||
      REVSETS_LOG=$(jj --ignore-working-copy config get 'jj-fzf.revsets.log' 2>/dev/null || :)
  test -n "$REVSETS_LOG" || REVSETS_LOG="fzflog"
  if $REVSETNAME ; then
    echo "$REVSETS_LOG"
  else
    REVSETCONFIG=() && get_REVSETCONFIG REVSETCONFIG
    jj --no-pager --ignore-working-copy "${REVSETCONFIG[@]}" \
       log --color=always -T "$JJ_FZF_ONELINE" -r "$REVSETS_LOG"
  fi
)
FUNCTIONS+=( 'fzflog' )

# revset-filter <revset> - assign to jj-fzf.revsets.log
DOC['revset-filter']='Restart `jj-fzf` using the current query string as new revset for this repository.'
revset-filter()
(
  REVSET="$1"
  REVSETCONFIG=() && get_REVSETCONFIG REVSETCONFIG
  jj --no-pager --ignore-working-copy "${REVSETCONFIG[@]}" \
     log --no-graph -T '' -r "$REVSET" >/dev/null 2>&1 ||
    REVSET=fzflog
  ( set -x
    jj --ignore-working-copy config set --repo 'jj-fzf.revsets.log' "$REVSET"
  ) || ERROR
)
KEYBINDINGS["Ctrl-R"]="revset-filter"	# overridden below

# Abandon Revision
DOC['abandon']='Use `jj abandon` to remove the currently selected revisions (or divergent commit) from the history.'
abandon()
(
  local -a REVS && parse_xrevs_or_commits REVS '-r' "$@"
  ( set -x
    jj abandon "${REVS[@]}" ) ||
    sleep 1
)
MULTIKEYBINDINGS["Alt-A"]="abandon"

# Bookmark Creation
DOC['bookmark']='Use `jj bookmark {create|set -B}` to (re-)assign a bookmark name to the currently selected revision (or divergent commit).'
bookmark()
(
  R="$(xrev_or_commit "${1:-@}")"
  #echo "# Existing Bookmarks:" && jj --no-pager --ignore-working-copy bookmark list
  readarray -t BOOKMARKS < <(jj --no-pager --ignore-working-copy bookmark list -T 'self.name()++"\n"' | sort | uniq)
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "::$R|$R+" -T 'bookmarks++"\n"' | sed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && INIT="${NEAREST[0]}" || INIT=""
  PROMPT='Bookmark Name: '
  echo "# Assign Bookmark to:"
  jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T builtin_log_oneline
  # Read bookmark with completion
  B="$(read_completing "${BOOKMARKS[@]}")"
  B="${B%% *}" && B="${B##* }" && test -z "$B" && return
  # See https://git-scm.com/docs/git-check-ref-format
  INVALIDPAT='(//|\.\.|/\.|[ :^~?*]|\[|^/|/$|\.$|^@$|@\{|\\|'$'[\x01-\x1f])'
  [[ "$B" =~ $INVALIDPAT ]] &&
    ERROR "bookmark contains invalid characters: $B"
  ( set -x
    jj bookmark set -r "$R" --allow-backwards "$B"
  ) || ERROR
  # jj git export --quiet
)
KEYBINDINGS["Alt-B"]="bookmark"

# Commit (full)
DOC['commit']='Use `jj commit` to describe the currently selected revision and create a new child revision as working-copy.'
commit()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  if test "$R" == "$W" -a "$IMMU" != true ; then
    user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
      test "$O" != "$MSG" ||
	ERROR "Commit cancelled by user"
      ( set -x
	jj commit --message="$MSG"
      ) || sleep 1
  else # R is not @, may be immutable
    [[ $IMMU =~ ^true ]] || {
      user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
	test "$O" != "$MSG" ||
	  ERROR "Commit cancelled by user"
      test "$O" != "$MSG" &&
	( set -x
	  jj describe --no-edit -r "$R" --message="$MSG"
	) || sleep 1
    }
    # open new empty working copy commit
    jj new "$R"
  fi
)
KEYBINDINGS["Alt-C"]="commit"		FIRSTS="$FIRSTS commit"

# Delete Bookmarks and Tags
DOC['delete-refs']='Use `jj bookmark list+delete` to list, selected and delete bookmarks and tags.'
delete-refs()
(
  R="$(xrev_or_commit "${1:-@}")"
  # find first local bookmark in $R, use as query arg
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T 'local_bookmarks++"\n"' | sed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && B=(-q "${NEAREST[0]}") || B=()
  require_git_dir # exports GIT_DIR
  # select bookmark or tag
  DELETELINE=$(
    "${FZFPOPUP[@]}" \
      --border-label '-[ DELETE BOOKMARK/TAG ]-' --color=border:red,label:red \
      --prompt "Delete > " \
      --header $'\n'"Delete selected Bookmark or Tag" --header-first \
      --no-tac --no-sort +m \
      "${B[@]}" \
      < <(
      # list local bookmarks
      jj --ignore-working-copy bookmark list | # sed reorders conflicted
	sed -r ':0; /^\s/!s/ \(conflicted\):/: (conflicted)/; N; $!b0; s/\n\s+/ /g' |
	while read MARK rest ; do
	  printf "%-32s [bookmark] %s\n" "${MARK%:}" "$rest"
	done
      echo
      # list git tags
      git tag -n1 | while read MARK rest ; do
	printf "%-32s [tag] %s\n" "$MARK" "$rest"
      done
    ) )
  # abandon-unreachable=true can be dangerous: https://github.com/jj-vcs/jj/discussions/7248#discussioncomment-14135120
  ABANDON_UNREACHABLE_FALSE=--config=git.abandon-unreachable-commits=false
  # delete given bookmark/tag line
  read MARK WHAT rest <<<"$DELETELINE"
  case "$WHAT" in
    "[bookmark]")	( set -x
			  jj $ABANDON_UNREACHABLE_FALSE bookmark delete exact:"$MARK"
			) || ERROR ;;
    "[tag]")		( set -x
			  git tag -d "$MARK"
			  jj $ABANDON_UNREACHABLE_FALSE --no-pager status
			) || ERROR ;;
  esac
)
KEYBINDINGS["Alt-D"]="delete-refs"

# diffedit
DOC['diffedit']='Use `jj diffedit` to select parts of the content diff to be kept in the currently selected revision.'
diffedit()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj diffedit -r "$R"
  ) || sleep 1
)
KEYBINDINGS["Alt-E"]="diffedit"

# Reset commit author
DOC['author-reset']='Use `jj describe --reset-author` to reset the author and email of the currently selected revision.'
author-reset()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj describe --reset-author --no-edit -r "$R"
  ) ||
    sleep 1
)
KEYBINDINGS["Ctrl-A"]="author-reset"

# Describe Commit Message
DOC['describe']='Use `jj describe` to describe the currently selected revision (or divergent commit).'
describe()
(
  R="$(xrev_or_commit "${1:-@}")"
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  user_editor_on_var "CHANGE-$R.txt" MSG "$R" ||
    ERROR "Describe cancelled by user"
  test "$O" != "$MSG" ||
    return
  (set -x
   jj describe --no-edit -r "$R" --message="$MSG"
  ) || ERROR
)
KEYBINDINGS["Ctrl-D"]="describe"

# File Editor
DOC['file-editor']='Use `jj edit` to switch to the currently selected revision and opens the files touched by this revision in `$EDITOR`.'
file-editor()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  # read files edited by revision
  readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | sed 's/^\w //')
  # make sure to edit revision
  test "$W" == "$R" || (
    IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
    [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
    set -x
    jj $CMD -r "$R"
  )
  ( set -x
    ${EDITOR:-nano} "${FILES[@]}"
  )
)
KEYBINDINGS["Ctrl-F"]="file-editor"

# Help with JJ commands
DOC['help']='Show the *jj-fzf* help and key binding commands.'
help()
(
  $ABSPATHSCRIPT --help "$@"
)
KEYBINDINGS["Ctrl-H"]="help"

# Split change
DOC['split-interactive']='Use `jj split` to interactively select content diff hunks to be split into a new commit. No text editor is invoked and the new commit gets an empty description.'
split-interactive()
(
  R="$(xrev "${1:-@}")"
  # To avoid message editing, truncate all but the first (original) description
  temp_dir
  cat > $TEMPD/noeditor <<-\__EOF__
	#!/usr/bin/bash
	set -Eeuo pipefail #-x
	TRUNCATE=n
	test $TRUNCATE == y && echo -n > "$1" || :
	sed 's/TRUNCATE=./TRUNCATE=y/' -i "$0"
	__EOF__
  chmod +x $TEMPD/noeditor
  export JJ_EDITOR="$TEMPD/noeditor" # Override ui.editor to implement --split-with-no-description
  ( set -x
    jj split --interactive -r "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-I"]="split-interactive"

# Diff Browser
DOC['diff']='Use `jj diff` to view differences between the currently selected revision and the working copy.'
diff()
(
  R="$(xrev_or_commit "${1:-@-}" 2>/dev/null)" || exit # invalid revision
  W="$(xrev_or_commit "@")" || ERROR
  REVS=( $(forward_chronologic "$R" "$W") )
  test "${#REVS[@]}" -ge 2 || REVS+=( "${REVS[0]}" )
  (
    # set -x
    jj --color=always log -r "${REVS[0]} | ${REVS[1]}" -T builtin_log_oneline # | sed -r '/[k-xyz]/!d; s/ +/  /'
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}" --stat
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}"
  ) 2>&1 | $JJFZFPAGER
)
KEYBINDINGS["Ctrl-I"]="diff"

# inject-commit
DOC['inject-commit']='Inject the currently selected revision (or divergent commit) as historic commit before @.'
inject-commit()
(
  R="$(xrev_or_commit "${1:-@}")"
  echo "# $ABSPATHSCRIPT: insert commit ($R) before @"
  ( set -x
    jj new --no-edit --insert-before @
    jj restore --from "$R" --to @- --restore-descendants
  ) || ERROR
)
KEYBINDINGS["Alt-J"]="inject-commit"

# Backout Commit
DOC['backout']='Use `jj backout` to create new commits that undo the changes made by the currently selected revisions and apply the changes on top of the working-copy.'
backout()
(
  local -a REVS && parse_xrevs_or_commits REVS '-r' "$@"
  ( set -x
    jj backout -d @ "${REVS[@]}"
  ) || die
)
MULTIKEYBINDINGS["Alt-K"]="backout"		FIRSTS="$FIRSTS backout"

# Line Blame: jj-fzf +<line> <gitfile>
if [[ $# == 2 ]] && [[ "${1:0:1}" == + ]] ; then
  absroot="$(readlink -f "$JJROOT")"
  absfile="$(readlink -f "$2")"
  [[ $absfile == $absroot/* ]] && {
    echo absroot=$absroot
    echo absf=$absfile
    file="${absfile:((1+${#absroot}))}"
    echo file=${absfile:((1+${#absroot}))}
    jj --no-pager status
    COMMIT="$(rev_commitid @)"
    EMPTY=$'^[| \033\[0-9;m]*$' # anchored pattern for empty line with git log graph chars
    SIGBY=$'^[| \033\[0-9;m]*Signed-off-by:.*@.*$' # anchored pattern for Signed-off-by
    grep -s -n '' "$file" /dev/null |
    "${FZFPOPUP[@]}" \
      --border-label '-[ LINE HISTORY (EXPERIMENTAL) ]-' --color=border:yellow,label:yellow \
      --preview " git log --graph --no-patch -M -C --find-copies-harder --pretty='%C(blue)%h %C(yellow)%aL %C(reset)%B' -L{2}:{1} --color $COMMIT | sed -nre '/($EMPTY|$SIGBY)/!p; /$EMPTY/{ p; :NEXT n; /($EMPTY|$SIGBY)/b NEXT; p; }' " \
      --bind "enter:execute( git log -M -C --find-copies-harder -L{2},+7:{1} --color $COMMIT | $JJFZFPAGER)" \
      --header "File Line History" \
      --no-tac --no-sort +m -d: \
      --track --bind 'focus:clear-query+unbind(focus)' \
      -q "${absfile:((1+${#absroot}))}:${1:1}:"
  }
  exit 0
fi

# Start multi-select mode
DOC["multi-select"]='Select multiple revisions to operate on.'
multi-select()
(
  $ABSPATHSCRIPT -m --postcmd-exit || :
)
KEYBINDINGS["Alt-M"]="multi-select"

# New --insert-before
DOC['new-before']='Use `jj new --no-edit --insert-before` to create and insert a new revision before the currently selected revision (or divergent commit).'
new-before()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new --no-edit --insert-before "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-Alt-N"]="new-before"

# New --insert-after
DOC['new-after']='Use `jj new --no-edit --insert-after` to create and insert a new revision after the currently selected revision (or divergent commit).'
new-after()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new --no-edit --insert-after "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-N"]="new-after"

# New
DOC['new']='Use `jj new` to create a new revision on top of the currently selected revision (or divergent commit).'
new()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new "$R"
  ) || sleep 1
)
KEYBINDINGS["Ctrl-N"]="new"		FIRSTS="$FIRSTS new"

# JJ_FZF_OP_LOG_ONELINE16 - Oneline op log with 16 character ids, parsed later on; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_OP_LOG_ONELINE16='
label(if(current_operation, "current_operation"),
  coalesce(
    if(root, format_root_operation(self)),
    concat(
      separate(" ", self.id().short(16), self.user(), self.time().start().ago()), " ",
      self.description().first_line(), " ",
      if(self.tags(), self.tags().first_line()),
    )
  )
)'
OP_LOG_FIRSTLINE='self.id() ++ ": " ++ self.description().first_line() ++ "\n"'

# operation_id_resolve [id] - resolve operations by following undo/redo
operation_id_resolve()
(
  op_id="${1-@}"
  while :; do
    next=$(
      jj --no-pager --ignore-working-copy op show --no-graph --no-op-diff -T "self.id() ++ ' ' ++ self.description().first_line()" "$op_id" |
	# Detect "restore to operation" indirection
	sed -rn 's/.*\brestore to operation ([0-9a-f]{32,}).*/\1/p'
	)
    [ -z "$next" ] && break	# Followed all indirections
    op_id="$next"
  done
  echo "$op_id"
)

# Show `jj op log` but mark undone operations with '⋯'
oplog()
(
  temp_dir
  # Determine range of undo operations
  LAST_OPID=$(operation_id_resolve @)
  if test "$LAST_OPID" != @ ; then
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16" |
      sed -r "1,/${LAST_OPID:0:16}\b/{ /${LAST_OPID:0:16}\b/! s/([@○])/⋯/ }" # ⮌ ⋯ ⤺↶
  else
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16"
  fi
)
FUNCTIONS+=( 'oplog' )

# Oplog
DOC['oplog-browser']='Use `jj op log` to browse the recent operations log. Use hotkeys to change the preview between diff, history and oplog entry mode. Undo the selected operation or restore its working copy into a new commit.'
oplog-browser()
(
  temp_dir
  echo > $TEMPD/oplog.env
  H=$'\n'
  H="$H"$'Ctrl-D: Preview the differences of an operation via `jj op diff -f <op> -t @`\n'
  H="$H"$'Ctrl-L: Preview history at a specific operation via `jj log -r ..`\n'
  H="$H"$'Ctrl-P: Preview changes in an operation with patch via `jj op show -p <op>`\n'
  H="$H"$'Ctrl-S: Preview "@" at a specific operation via `jj show @`\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject working copy of the selected operation as historic commit before @\n'
  H="$H"$'Alt-R: Restore repository to the selected operation via `jj op restore`\n'
  H="$H"$'Alt-Y: Redo the last undo operation (marked `⋯`)\n'
  H="$H"$'Alt-Z: Undo the next operation (not already marked `⋯`)\n'
  echo 'VIEW=preview_oppatch'		>> $TEMPD/oplog.env
  export FZF_DEFAULT_COMMAND="$ABSPATHSCRIPT oplog" TEMPD ABSPATHSCRIPT
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label '-[ OPLOG-BROWSER ]-' --color=border:bright-yellow,label:bright-yellow \
    --prompt "Operation > " \
    --header "$H" --header-first \
    --bind "ctrl-d:execute-silent( sed 's/^VIEW=.*/VIEW=preview_opdiff/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-l:execute-silent( sed 's/^VIEW=.*/VIEW=preview_oplog/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-p:execute-silent( sed 's/^VIEW=.*/VIEW=preview_oppatch/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-s:execute-silent( sed 's/^VIEW=.*/VIEW=preview_opshow/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "alt-j:execute( $ABSPATHSCRIPT restore-commit {} )+abort" \
    --bind "alt-r:execute( $ABSPATHSCRIPT op-restore {} )+abort" \
    --bind "alt-w:execute( $ABSPATHSCRIPT restore-commit {} )+abort" \
    --bind "alt-y:execute( $ABSPATHSCRIPT redo )+$RELOAD" \
    --bind "alt-z:execute( $ABSPATHSCRIPT undo )+$RELOAD" \
    --bind 'enter:execute( [[ {} =~ $OPPAT ]] || exit && COMMIT=$(jj log --no-pager --no-graph -T commit_id -r @ --at-operation "${BASH_REMATCH[1]}") && $ABSPATHSCRIPT logrev "$COMMIT" )' \
    --preview-window 'nowrap,right,border-left' \
    --preview '[[ {} =~ $OPPAT ]] || exit; . $TEMPD/oplog.env && ${ABSPATHSCRIPT%/*}/../libexec/jj-fzf/preview.sh $VIEW {}' \
    --no-tac --no-sort +m
  # TODO: remove alt-w in jj-fzf-0.26
)
KEYBINDINGS["Ctrl-O"]="oplog-browser"

restore-commit()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  COMMIT="$(jj --no-pager --ignore-working-copy --at-op $OP show --tool true -T commit_id -r @)"
  echo "# $ABSPATHSCRIPT: insert working copy commit (${COMMIT:0:12}) from operation ${OP:0:12} before @"
  ( set -x
    jj new --no-edit --insert-before @
    jj restore --from "$COMMIT" --to @- --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'restore-commit' )

op-restore()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following jj op restore"
  ( set -x
    jj op restore "$OP"
  ) || ERROR
)
FUNCTIONS+=( 'op-restore' )

# Show `jj evolog`
evolog_oneline()
(
  R="$1"	# TODO: use jj default with operations in multi-line parsing
  jj evolog --no-pager --ignore-working-copy --color=always -r "$R" -T "$ONELINE_COMMIT_BIGHEX"
)
FUNCTIONS+=( 'evolog_oneline' )

# Inject historic commit of a revision
evolog-inject()
(
  R="$(xrev "${1:-}")"
  [[ " $2 " =~ $HEX7PAT ]] || die "missing commit"
  C="$(xrev_as_commit "${BASH_REMATCH[1]}")"
  MSG="$(rev_description "$C")"
  NEWREV=
  jj_new_before_no_edit NEWREV "$R" "$MSG"
  ( set -x
    jj restore --from "$C" --to "$NEWREV" --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'evolog-inject' )

# Show `jj evolog`
evolog_pager()
(
  [[ " $* " =~ ' '$BIGHEXPAT' ' ]] && {
    # builtin_log_detailed
    jj --no-pager --ignore-working-copy evolog --color=always -p -r "${BASH_REMATCH[1]}" -T "builtin_log_compact_full_description(commit)" 2>&1 |
      $JJFZFPAGER
  }
)
FUNCTIONS+=( 'evolog_pager' )

# Evolog
DOC['evolog']='Use `jj evolog` to browse the evolution of the selected revision. Inject historic commits into the ancestry without changing descendants.'
evolog()
{
  R="$(xrev_or_commit "${1:-@}")"
  temp_dir
  H=$'\n'
  H="$H"$'Enter: Browse evolog with diff\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject evolog entry as historic commit before the revision without changing it.\n'
  export FZF_DEFAULT_COMMAND="$ABSPATHSCRIPT evolog_oneline $R"
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label "-[ EVOLOG $R ]-" --color=border:yellow,label:bright-yellow \
    --prompt "Evolog > " \
    --header "$H" --header-first \
    --bind "enter:execute( $ABSPATHSCRIPT evolog_pager {} )" \
    --bind "alt-j:execute( $ABSPATHSCRIPT evolog-inject $R {} )+abort" \
    --preview-window 'nowrap,right,border-left' \
    --preview "exec ${ABSPATHSCRIPT%/*}/../libexec/jj-fzf/preview.sh preview_evolog {}" \
    --no-tac --no-sort +m
}
KEYBINDINGS["Ctrl-T"]="evolog"

# Split files
DOC['split-files']='Use `jj split` in a loop to split each file modified by the currently selected revision into its own commit.'
split-files()
(
  R="$(xrev "${1:-@}")"
  # read files affected by $R
  mapfile -t MAPFILE < <(jj diff --name-only -r "$(rev_commitid "$R")")
  [[ ${#MAPFILE[@]} -gt 1 ]] ||
    return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following split"
  # create n-1 new commits from n files
  while [[ ${#MAPFILE[@]} -gt 1 ]] ; do
    unset 'MAPFILE[-1]' # unset 'MAPFILE[${#MAPFILE[@]}-1]'
    export JJ_EDITOR='true' # Override ui.editor to implement --split-with-no-description
    ( set -x
      jj split -r "$R" -- "${MAPFILE[@]}"
    ) || ERROR
  done
)
KEYBINDINGS["Alt-F"]="split-files"

# Fetch and push to remote Git repositories
DOC['push-remote']='Use `jj git fetch` and `jj git push --tracked --deleted` to update the local and remote repositories. Pushing needs confirmation after a dry-run.'
push-remote()
(
  ABANDON_UNREACHABLE_FALSE=--config=git.abandon-unreachable-commits=false
  ( set -x
    jj $ABANDON_UNREACHABLE_FALSE git fetch
    jj git push --tracked --deleted --dry-run
  ) || ERROR
  read -p 'Try to push to remote? ' YN
  [[ "${YN:0:1}" =~ [yY] ]] ||
    exit
  ( set -x
    jj git push --tracked --deleted
  ) || ERROR
)
KEYBINDINGS["Ctrl-P"]="push-remote"

# Absorb a content diff into mutable ancestors
absorb()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj absorb --from "$R"
  ) || ERROR
)
DOC['absorb']='Use `jj absorb` to split the content diff of the current revision and squash pieces into related mutable ancestors.'
KEYBINDINGS["Alt-O"]="absorb"

# Squash Into Parent
DOC['squash-into-parent']='Use `jj squash` to move the changes from the currently selected revision(s) (or divergent commit) into its parent.'
squash-into-parent()
(
  local -a REVS && parse_xrevs_or_commits REVS '' "$@"
  W="$(xrev_or_commit "@")"
  PARENTS="roots( none() $(printf '|%s-::' "${REVS[@]}") )"
  PARENTS=( $(list_revset " $PARENTS ") )
  test ${#PARENTS[@]} -eq 1 ||
    ERROR "revisions lack common parent"
  if [[ " ${REVS[*]} " == *" $W "* ]]; then
    # The working copy @ is to be squashed.
    # Squashing without --keep-emptied would start a new branch at @- which is
    # undesired if @+ exists. But using --keep-emptied does not squash the
    # message. As a workaround, create a new @+, so we never squash directly
    # from @. This new working copy will receive any children from the original
    # squashed working copy.
    ( set -x
      jj new --insert-after @
    ) || ERROR
  fi
  ( set -x
    jj squash --from "$(IFS='|' && echo "${REVS[*]}")" --into "${PARENTS[0]}"
  ) || ERROR
)
MULTIKEYBINDINGS["Alt-Q"]="squash-into-parent"

# Squash @ Commit
DOC['squash-@-into']='Use `jj squash` to move the changes from the working copy into the currently selected revision.'
squash-@-into()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  test "$R" == "$W" && return
  # See squash-into-parent, for why we need `new --insert-before` when squashing @.
  ( set -x
    jj new --insert-before @
    jj squash --from "$W" --into "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-W"]="squash-@-into"

# Reparent a revision
DOC['reparenting']='Start a dialog to add/delete parents of the current revision. Also supports `jj simplify-parents` after reparenting.'
reparenting()
(
  SRC="$(xrev "${1:-@}")"
  IMMU=$($JJFZFSHOW -r "$SRC" -T 'if(immutable, "true")')
  test "$IMMU" != true || exit 0
  temp_dir
  jj --no-pager --ignore-working-copy log --no-graph -T 'change_id ++ "\n"' -r "$SRC-" > $TEMPD/reparenting.lst
  echo 'OP="|"'		>  $TEMPD/reparenting.env
  echo 'SIMPLIFY=false'	>> $TEMPD/reparenting.env
  export SRC TEMPD
  # Parse jj log lines into reparenting.revs
  reparenting_revs()
  {
    mapfile -t PARENTS < $TEMPD/reparenting.lst && PARENTS="( $(join_args '|' "${PARENTS[@]}") )"
    test "$OP" == '|' && FILTER="~ ($SRC|$PARENTS)" || FILTER="& $PARENTS"
    for ARG in "$@" ; do
      [[ "$ARG" =~ $REVPAT ]] || continue
      R=$(jj --no-pager --ignore-working-copy log --no-graph -r "${BASH_REMATCH[1]} $FILTER" -T change_id)
      test -z "$R" ||
	echo "$R"
    done > $TEMPD/reparenting.revs
    mapfile -t REVS < $TEMPD/reparenting.revs && EXPR="$SRC-"
    test "${#REVS[@]}" -ge 1 && EXPR="$SRC- $OP ( $(join_args '|' "${REVS[@]}") )"
    # sort, so we generally merge younger branches into older branches
    forward_chronologic "$EXPR" > $TEMPD/newparents.lst
  }
  # Preview reparenting command for reparenting.revs
  reparenting_cmd()
  (
    echo
    echo "CHANGE PARENTS:"
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst && NEWPARENTS="$(join_args ' | ' "${NEWPARENTS[@]}")"
    echo "jj rebase --source \"$SRC\" --destination \\"
    echo "  \"$NEWPARENTS\""
    $SIMPLIFY && echo "jj simplify-parents --revisions \"$SRC\\"
    echo
    echo "SOURCE REVISION:"
    jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r "$SRC | $SRC-"
    echo
    test "$OP" == '|' && deladd='ADD' || deladd='REMOVE'
    echo "$deladd PARENTS:"
    test "$OP" == '|' && deladd='+ ' || deladd='- '
    while read R ; do
      echo -n "$deladd"
      jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline -r "$R"
    done < $TEMPD/reparenting.revs
  )
  # Provide functions for FZF
  export -f reparenting_revs reparenting_cmd join_args forward_chronologic backward_chronologic reverse_array
  H=$'\n'
  H="$H""Alt-A: ADD    - Add currently selected revisions as new parents"$'\n'
  H="$H""Alt-D: DEL    - Delete selected revisions from current list of parents"$'\n'
  H="$H""Alt-P: SIMPLIFY-PARENTS - Use simplify-parents after reparenting"$'\n'
  export FZF_DEFAULT_COMMAND="$ABSPATHSCRIPT fzflog"
  # FZF select parents
  "${FZFPOPUP[@]}" \
      --border-label '-[ CHANGE PARENTS ]-' --color=border:cyan,label:cyan \
      --preview ". $TEMPD/reparenting.env && reparenting_revs {+} && reparenting_cmd" \
      --prompt "Parents > " \
      --header "$H" --header-first \
      --bind "alt-a:execute-silent( sed 's/^OP=.*/OP=\"|\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-d:execute-silent( sed 's/^OP=.*/OP=\"~\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-p:execute-silent( sed 's/^SIMPLIFY=false/SIMPLIFY_=/; s/^SIMPLIFY=true/SIMPLIFY=false/; s/^SIMPLIFY_=/SIMPLIFY=true/' -i $TEMPD/reparenting.env )+refresh-preview" \
      -m --color=pointer:grey \
      --no-tac --no-sort > $TEMPD/selections.txt &&
    mapfile -t selections < $TEMPD/selections.txt &&
    source $TEMPD/reparenting.env &&
    reparenting_revs "${selections[@]}" &&
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst &&
    test "${#NEWPARENTS[@]}" -gt 0 ||
      exit # Reparenting cancelled
  # Re-parent revisions
  ( set -x
    # Ordering is not preserved with '(.|.|.)', only with -d. -d. -d.
    jj rebase --source "$SRC" "${NEWPARENTS[@]/#/-d}"
  ) || ERROR
  # simplify-parents
  ! $SIMPLIFY || (
    set -x
    jj simplify-parents --revisions "$SRC"
  ) || ERROR
)
KEYBINDINGS["Alt-P"]="reparenting"	FIRSTS="$FIRSTS reparenting"

# Rebase Branch/Source/Revision After/Before/Destination
DOC['rebase']='Start a dialog to `jj rebase` or `jj duplicate` a set of revisions (or branch) possibly with descendants, onto, before or after another revision. Also supports `jj simplify-parents` afterwards.'
rebase()
(
  local -a RBREVS && parse_xrevs_or_commits RBREVS '' "$@"
  temp_dir
  CREVS="${RBREVS[*]}" && CREVS="${CREVS// /|}" # combined revisions
  echo > $TEMPD/rebase.env
  echo 'DP='			>> $TEMPD/rebase.env
  echo 'CH='			>> $TEMPD/rebase.env
  echo 'TO=--destination'	>> $TEMPD/rebase.env
  echo 'SP=false'		>> $TEMPD/rebase.env
  echo 'II='			>> $TEMPD/rebase.env
  if test ${#RBREVS[@]} -le 1 ; then
    echo 'FR=--source'		>> $TEMPD/rebase.env
  else
    echo 'FR=--revisions'	>> $TEMPD/rebase.env
    CREVS="($CREVS)"		# parenthesis around multiple revisions
  fi
  QREVS="'$CREVS'"		# quoted revisions
  export JJFZFONELINE
  PREVIEW=". $TEMPD/rebase.env"
  PREVIEW="$PREVIEW"' && echo'
  PREVIEW="$PREVIEW"' && { test -z "$DP" || echo jj duplicate -r '"$QREVS"'$CH $TO $TARGET ; }'
  PREVIEW="$PREVIEW"' && { test -n "$DP" || echo jj rebase $II $FR '"$QREVS"' $TO $TARGET ; }'
  PREVIEW="$PREVIEW"' && { test $SP == true && echo && echo jj simplify-parents -r '"$QREVS"' || : ; } && echo'
  PREVIEW="$PREVIEW"' && F=${FR#--} && echo ${F^^}: && $JJFZFONELINE -r '"$QREVS"' && echo'
  PREVIEW="$PREVIEW"' && T=${TO#--} && echo ${T^^}: && $JJFZFONELINE -r '"$QREVS"' && echo'
  PREVIEW="$PREVIEW"' && echo COMMON: && $JJFZFONELINE -r "heads( ::('"$CREVS"') & ::$TARGET)"'
  H=''
  H="$H""Alt-B: BRANCH    - Rebase the whole branch relative to destination's ancestors"$'\n'
  H="$H""Alt-C: DUP-DESCENDANTS - duplicate the specified revisions and descendants"$'\n'
  H="$H""Alt-D: DUPLICATE - duplicate the specified revisions"$'\n'
  H="$H"'Alt-I: IGNORE-IMMUTABLE - Use `jj rebase --ignore-immutable` command'$'\n'
  H="$H"'Alt-P: SIMPLIFY-PARENTS - Use `jj simplify-parents` after rebasing'$'\n'
  H="$H""Alt-R: REVISION  - Rebase only given revision, moves descendants onto parent"$'\n'
  H="$H""Alt-S: SOURCE    - Rebase specified revision together with descendants"$'\n'
  H="$H""Ctrl-A: AFTER       - The revision to insert after"$'\n'
  H="$H""Ctrl-B: BEFORE      - The revision to insert before"$'\n'
  H="$H""Ctrl-D: DESTINATION - The revision to rebase onto"$'\n'
  export FZF_DEFAULT_COMMAND="$ABSPATHSCRIPT fzflog"
  TARGET=$("${FZFPOPUP[@]}" \
	     --border-label '-[ REBASE ]-' --color=border:green,label:green \
	     --preview "[[ {} =~ $REVPAT ]] || exit; export TARGET=\"\${BASH_REMATCH[1]}\"; $PREVIEW " \
	     --prompt "Rebase > " \
	     --header "$H" --header-first \
	     --bind "alt-d:execute-silent( sed 's/^CH=.*/CH=/;   s/^DP=.*/DP=1/; s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-c:execute-silent( sed 's/^CH=.*/CH=::/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--source/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-b:execute-silent( sed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--branch/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-s:execute-silent( sed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--source/   ' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-r:execute-silent( sed 's/^CH=.*/CH=/;   s/^DP=.*/DP=/;  s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-p:execute-silent( sed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "alt-i:execute-silent( sed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-d:execute-silent( sed 's/^TO=.*/TO=--destination/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-a:execute-silent( sed 's/^TO=.*/TO=--insert-after/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --bind "ctrl-b:execute-silent( sed 's/^TO=.*/TO=--insert-before/' -i $TEMPD/rebase.env )+refresh-preview" \
	     --no-tac --no-sort +m )
  [[ "$TARGET" =~ $REVPAT ]] &&
    TARGET="${BASH_REMATCH[1]}" ||
      exit 0
  TARGET="$(xrev_or_commit "$TARGET")"
  source $TEMPD/rebase.env
  rm -f TEMPD/rebase.env
  # duplicate revisions
  if test -n "$DP" ; then
    ( set -x
      jj duplicate -r "$CREVS$CH" $TO "$TARGET"
    ) || ERROR
  else # rebase revisions
    ( set -x
      jj rebase $II $FR "$CREVS" $TO "$TARGET"
    ) || ERROR
  fi
  # simplify-parents
  ! $SP ||
    ( set -x
      jj simplify-parents -r "$CREVS"
    ) || ERROR
)
MULTIKEYBINDINGS["Alt-R"]="rebase"			FIRSTS="$FIRSTS rebase"

# Restore File
DOC['restore-file']='Start a dialog to select a file to `jj restore` from the currently selected revision into the working copy.'
restore-file()
(
  R="$(xrev "${1:-@}")"
  temp_dir
  # find differing fileset
  mapfile -t MAPFILE < <(jj --no-pager --ignore-working-copy diff --name-only --from @ --to "$R")
  [[ ${#MAPFILE[@]} -ge 1 ]] ||
    return
  # file picker
  PREVIEW='read F <<<{} && test -n \"$F\" || exit '
  PREVIEW+='&& jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r "'"$R|@"'" && echo '
  PREVIEW+='&& jj --no-pager --ignore-working-copy diff --color=always -s --git --from @ --to "'"$R"'" -- cwd-file:"$F" '
  printf '%s\n' "${MAPFILE[@]}" |
    "${FZFPOPUP[@]}" \
      --border-label '-[ RESTORE-FILE ]-' --color=border:blue,label:blue \
      --preview "$PREVIEW" \
      --header "Restore File from $R into @" \
      > $TEMPD/restore-file.sel
  # restore file if any
  FILENAME="$(<$TEMPD/restore-file.sel)"
  test -z "$FILENAME" || (
    set -x
    jj restore --from "$R" -- cwd-file:"$FILENAME"
  ) || ERROR
)
KEYBINDINGS["Alt-S"]="restore-file"

# Tag Creation
DOC['tag']='EXPERIMENTAL: Enter a tag name to create a new unsigned, annotated tag at the selected revision with `git tag`.'
tag()
(
  R="$(xrev "${1:-@}")"
  C="$(rev_commitid "$R")"
  require_git_dir
  read -p 'Tag Name: ' B &&
    test -n "$B" ||
      return
  M="$(git log -1 --oneline "$C")"
  ( set -x
    git tag -a "$B" -m "$M" "$C"
  ) || ERROR
  #  jj git import --quiet
)
KEYBINDINGS["Alt-T"]="tag"

# Log single change
logrev()
(
  R="$(xrev_as_commit "${1:-@}")"
  (
    jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$R"
    jj_show_diff --color=always -T '"\n"' -r "$R"
  ) | $JJFZFPAGER
)
FUNCTIONS+=( 'logrev' )

# Log flat change history
DOC['log']='Use `jj log` to browse the history including patches, starting from the selected revision (or divergent commit).'
log()
{
  R="$(xrev_or_commit "${1:-@}")"
  jj log --ignore-working-copy --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -r "::$R" -s -p --ignore-space-change \
    | $JJFZFPAGER
}
KEYBINDINGS["Ctrl-L"]="log"

# vivifydivergent
DOC['vivifydivergent']='When a revision has more than one visible commit, it becomes a divergent revision. This command uses `jj new+squash …` to create a new *change_id* for the currently selected revision, effectively resolving the divergence.'
vivifydivergent()
(
  # fetch commit_id of a divergent revision
  COMMIT="$(xrev_as_commit "${1:-@}")" &&
    WCOPY="$(xrev_as_commit "@")" ||
      die 'no divergent revision'
  # leave working copy alone, unless it is $1
  test "$COMMIT" == "$WCOPY" && NOEDIT= || NOEDIT=--no-edit
  echo "# ${ABSPATHSCRIPT##*/} vivifydivergent $COMMIT" >&2
  jj --no-pager log --no-graph -T builtin_log_oneline -r "$COMMIT" # --ignore-working-copy
  export JJ_EDITOR='true' # Override ui.editor to implement --squash-with-no-description
  ( set -x
    jj new --insert-after "$COMMIT" $NOEDIT
    jj squash --from "$COMMIT" --into "$COMMIT+"
  ) || ERROR
)
KEYBINDINGS["Alt-V"]="vivifydivergent"	NEXTS="$NEXTS vivifydivergent"

# Gitk View
DOC['gitk']='DEPRECATED: Start `gitk` to browse the *Git* history of the repository.'
gitk()
(
  R="$(xrev "${1:-@}")"
  # jj git export --quiet
  COMMIT="$(rev_commitid "$R")"
  git update-index --refresh || :
  #test -e "$JJROOT/.jj/repo/store/git" && export GIT_DIR="$JJROOT/.jj/repo/store/git" || export GIT_DIR="$JJROOT/.git"
  # readarray -t HEADS < <( jj --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r ' heads(..) ' )
  # beware gitk is executable and sh function
  ( set -x
    exec gitk --branches --tags --remotes --select-commit=$COMMIT $COMMIT HEAD -- # "${HEADS[@]}"
  ) || ERROR
  # jj git import --quiet
)
KEYBINDINGS["Ctrl-V"]="gitk"

# Toggle between various diff formats
DOC['word-diff-toggle']='Toggle/cycle between various diff formats, including JJ, Git, word-diff, with and without whitespace.'
word-diff-toggle()
(
  currentmode=$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true)
  diffmodes=("jj-diff" "diff-b" "word-b")
  index=0
  for i in "${!diffmodes[@]}"; do
    [[ "${diffmodes[$i]}" == "$currentmode" ]] && { index=$i && break; }
  done
  next_index=$(( (index + 1) % ${#diffmodes[@]} ))
  jj --no-pager --ignore-working-copy config set --repo jj-fzf.diff-mode "${diffmodes[$next_index]}"
)
KEYBINDINGS["Ctrl-W"]="word-diff-toggle"	SILENTREFRESH="$SILENTREFRESH word-diff-toggle"

# Edit (New) Working Copy
DOC['edit']='Use `jj {edit|new}` to set the currently selected revision (or divergent commit) as the working-copy revision. Will create a new empty commit if the selected revision is immutable.'
edit()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
  ( set -x
    jj $CMD -r "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-E"]="edit"

# Swap Commits
DOC['swap-commits']='Use `jj rebase --insert-before` to quickly swap the currenly selected revision with the revision immediately before it.'
swap-commits()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj rebase -r "$R" --insert-before "$R-"
  ) || ERROR
)
KEYBINDINGS["Alt-X"]="swap-commits"		POSTCMDS["swap-commits"]="+down"

# Redo last JJ op
DOC['redo']='Use `jj redo` to redo the last undo operation performed by `jj undo`.'
redo()
(
  ( set -x
    jj redo
  ) || ERROR
)
MULTIKEYBINDINGS["Alt-Y"]="redo"

# Undo last JJ op
DOC['undo']='Use `jj undo` to undo the last operation performed by `jj` that was not previously undone.'
undo()
(
  ( set -x
    jj undo
  ) || ERROR
  # Known cases where the above multi-step undo logic breaks:
  # * Undo of an operation like "reconcile divergent operations" just gives "Error: Cannot undo a merge operation"
)
MULTIKEYBINDINGS["Alt-Z"]="undo"

# Minimal Markdown transformations for the terminal
sedmarkdown()
(
  B=$'\e[1m'              # Bold
  T=$'\e[32;1;4m'         # Title
  H=$'\e[1;4m'            # Heading
  C=$'\e[36m'             # Code
  I=$'\e[3m'              # Italic
  U=$'\e[4m'              # Underline
  Z=$'\e[0;24m'           # Reset
  W='[][<>{}A-Z| $@○◆a-z0-9/ ↑←↓→-⇿ :….()+-]'      # Word-like chars (english)
  SEDSCRIPT="
    s/\r\`\`\`+\w*(([^\`]*|\`[^\`])+)\r\`\`\`+/$C\1$Z\n/g       # Code block with backticks
    s/\r~~~+\w*(([^~]*|~[^~])+)\r~~~+/$C\1$Z\n/g                # Code block with tilde
    s/(^|\r)# ([^\r]+)[ #]*\r/\1$T\2$Z\r/g                      # Title Heading
    s/(^|\r)##+ ([^\r]+)[ #]*\r/\1$H\2$Z\r/g                    # Headings
    s/(\r\s?\s?)[-*] (\w+\b:)?/\1$B* \2$Z/g                     # List bullet
    s/(\s)\*\*($W+)\*\*/\1$B\2$Z/g                              # Bold
    s/(\s)\*($W+)\*([^*])/\1$I\2$Z\3/g                          # Italic
    s/(\s)_($W+)_([^_])/\1$U\2$Z\3/g                            # Underline
    s/(\s)\`($W+)\`([^\`])/\1$C\2$Z\3/g                         # Code
    s/\r?<!--([^-]|-[^-]|--[^>])*-->//g                         # Html Comments
    s,(\bhttps?://[^ ()\r]+),$U\1$Z,g                           # Link
  "
  tr \\n \\r |
    { $COLOR && sed -re "$SEDSCRIPT" || cat ; } |
    tr \\r \\n
)

# == MULTISELECT ==
if test -n "$MULTISELECT" ; then
  KEYBINDINGS=$(declare -p MULTIKEYBINDINGS) && declare -A KEYBINDINGS="${KEYBINDINGS#*=}"	# copy MULTIKEYBINDINGS -> KEYBINDINGS
else
  for key in "${!MULTIKEYBINDINGS[@]}"; do							# merge MULTIKEYBINDINGS -> KEYBINDINGS
    KEYBINDINGS["$key"]="${MULTIKEYBINDINGS[$key]}"
  done
fi

# == --help ==
HELPKEYS=$(declare -p KEYBINDINGS) && declare -A HELPKEYS="${HELPKEYS#*=}"	# copy KEYBINDINGS -> HELPKEYS
if test -n "$HELPKEYBINDINGS" ; then
  # Key bdingins only shown in long form help
  HELPKEYS[F11]='toggle-layout'
  DOC['toggle-layout']='Change between horizontal or vertical layout.'
  HELPKEYS[F5]='reload'
  DOC['reload']='Force a reload of the log view.'
  HELPKEYS[Shift-↑]='preview-up'
  HELPKEYS[Ctrl-↑]='preview-up'
  DOC['preview-up']='Scroll the preview window.'
  HELPKEYS[Shift-↓]='preview-down'
  HELPKEYS[Ctrl-↓]='preview-down'
  DOC['preview-down']='Scroll the preview window.'
  HELPKEYS[Ctrl-U]='clear-filter'
  DOC['clear-filter']='Discard the current *fzf* query string.'
  HELPKEYS[Alt-H]='toggle-show-keys'
  DOC['toggle-show-keys']='Display or hide the list of avilable key bindings, persist the setting in `jj-fzf.show-keys` of the `jj` user config.'
else
  test -n "$MULTISELECT" && {
    HELPKEYS[Alt-M]='multi-select-exit'
    HELPKEYS[TAB]='toggle-select'
  }
fi
DISPLAYKEYS="${!HELPKEYS[@]}"
DISPLAYKEYS=$(sort <<<"${DISPLAYKEYS// /$'\n'}" | grep -vF 'Ctrl-Alt-')
if test -n "$HELPKEYBINDINGS" ; then
  tty -s <&1 && COLOR=true || { COLOR=false; JJFZFPAGER=cat; }
  test -z "$COLORALWAYS" || COLOR=true
  ( :
    for k in $DISPLAYKEYS ; do
      NAME="${HELPKEYS[$k]}"
      echo && echo "## $k: _$NAME""_"
      D="${DOC[$NAME]:-}"
      test -z "$D" ||
	echo "$D" | fold -s -w78 | sed 's/^/  /'
    done
  )
  exit 0
fi

# == --key-bindings ==
list_key_bindings()
{
  LINES="${LINES:-$JJFZF_LINES}" COLUMNS="${COLUMNS:-$JJFZF_COLUMNS}" # unset by transform-header()
  test "$COLUMNS" -ge 218 && W=4 || {
      test "$COLUMNS" -ge 166 && W=3 || {
	  test "$COLUMNS" -ge 114 && W=2 || W=1; }; }
  [[ ${#DISPLAYKEYS} -gt $(($LINES * $W * 2)) ]] && {
    echo "Ctrl-H: help"		# no space left for jj-fzf.show-keys toggle
    exit 0
  }
  SHOW_KEYS="$(jj --ignore-working-copy config get 'jj-fzf.show-keys' 2>/dev/null || echo true)"
  [[ "$*" =~ --key-toggle ]] && {
    SHOW_KEYS="$(echo "$SHOW_KEYS" | sed 's/^false/x/; s/^true/false/; s/^x/true/')"
    jj --ignore-working-copy config set --user 'jj-fzf.show-keys' "$SHOW_KEYS"
  }
  $SHOW_KEYS || {
    echo "Ctrl-H: help  Alt-H: show-keys"
    exit 0
  }
  OUTPUT=""
  i=0; WHITE="                                                                                "
  for k in $DISPLAYKEYS ; do
    S="$k: ${HELPKEYS[$k]}"	# printf(1) cannot count UTF-8 continuation chars (0x80-0xBF)
    test ${#S} -lt 26 && S="$S${WHITE:0:$(( 26 - ${#S} ))}"	# so, format like %-26s
    OUTPUT="$OUTPUT$S" #$HIGH"
    i=$(($i+1))
    test 0 == $(($i % $W)) &&
      OUTPUT="$OUTPUT"$'\n' ||
	OUTPUT="$OUTPUT "
  done
  echo -n "$OUTPUT"
}
if test -n "$SHOWKEYBINDINGS" ; then
  list_key_bindings "$@"
  exit 0
fi

# == Function calling ==
if [[ "${1:-}" =~ ^[a-z0-9A-Z_+@-]+ ]] && [[ " ${KEYBINDINGS[*]} ${FUNCTIONS[*]} " =~ \ $1\  ]] ; then
  # Sync JJ working-copy before and after func, according to user config, but avoid paging
  ( set -e
    jj status --no-pager >/dev/null
    trap 'jj status --no-pager >/dev/null' 0 HUP INT QUIT TRAP USR1 PIPE TERM
    FUNC="$1" "$@"
  ) # preserves $FUNC exit status
  exit $?
fi

# == Sync ==
# Sync JJ before starting FZF, so user snapshot config and snapshot errors take effect
( set -x
  jj --no-pager status
) || exit $?

# === TEMPD ==
if test -z "${TEMPD:-}" ; then
  temp_dir
  export JJFZF_OUTER_TEMPD="$TEMPD" JJFZF_COLUMNS="$COLUMNS" JJFZF_LINES="$LINES"
fi
FZFEXTRAS=()
EXECKILLME=
$ONESHOT && {
  echo > "$TEMPD/killme.0"		# ignore first :focus:
  echo "$$" > "$TEMPD/killme.pid"	# then kill FZF
  FZFEXTRAS+=(
    --bind "start:execute( ps -o ppid= \$\$ > $TEMPD/killme.pid )"
    --bind "focus:execute-silent( test -e $TEMPD/killme.0 && rm -f $TEMPD/killme.0 || rm -f $TEMPD/killme.pid )"
  )
  EXECKILLME="+execute( test -e $TEMPD/killme.pid && kill -1 \$(<$TEMPD/killme.pid) )"
}

# == BIND COMMANDS ==
RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
BIND=()
for k in "${!KEYBINDINGS[@]}" ; do
  fun="${KEYBINDINGS[$k]}"
  postcmd=""
  [[ " $FIRSTS " == *" $fun "* ]] && postcmd="+first"
  [[ -v POSTCMDS["$fun"] ]] && postcmd="$postcmd${POSTCMDS[$fun]}"
  [[ " $SILENTREFRESH " == *" $fun "* ]] && EXECUTE=execute-silent || EXECUTE=execute
  [[ " $NEXTS " == *" $fun "* ]] && postcmd="+down"
  [[ " $SILENTREFRESH " == *" $fun "* ]] && REPAINT=refresh-preview || REPAINT="$RELOAD"
  [[ -v MULTIKEYBINDINGS["$k"] ]] && FUNARGS='{+}' || FUNARGS='{} {q}'
  [[ -n "$POSTCMDEXIT" ]] && postcmd=+abort
  BIND+=( --bind "${k,,}:$EXECUTE( $ABSPATHSCRIPT $MULTISELECT $fun $FUNARGS )$EXECKILLME+$REPAINT$postcmd" )
done

# == FZF ==
export FZF_DEFAULT_COMMAND="$ABSPATHSCRIPT fzflog"
fzflog 2>&1 |
  fzf \
  "${FZFSETTINGS[@]}" "${FZFEXTRAS[@]}" \
  --bind "ctrl-u:clear-query+clear-selection+clear-screen" \
  --bind "ctrl-z:execute( $JJSUBSHELL )+execute-silent( jj --no-pager status )+$RELOAD" \
  --bind "alt-m:abort" \
  --bind "f5:$RELOAD" \
  --bind "enter:execute( $ABSPATHSCRIPT logrev {} {q} )$EXECKILLME+$RELOAD" \
  "${BIND[@]}" \
  --bind "ctrl-r:transform-query( $ABSPATHSCRIPT revset-filter {q} )+become( exec $ABSPATHSCRIPT )" \
  --preview " exec ${ABSPATHSCRIPT%/*}/../libexec/jj-fzf/preview.sh preview_revision {} {q} " \
  --header "$(list_key_bindings)" --header-first \
  --bind "alt-h:transform-header:$ABSPATHSCRIPT $MULTISELECT --key-bindings --key-toggle" \
  --prompt "  $(fzflog --revsetname) ${MULTISELECT/-m/(MULTI)}> " \
  --sync \
  --no-tac --no-sort +m $MULTISELECT

# Notes:
# * Do not use 'exec' as last command, otherwise trap-handlers are skipped.
# * Ctrl-R: This must be rebound to run transform-query, ideally we would just transform-query+transform-prompt+reload
#   but that crashes fzf-0.44.1 when the cursor position is after the new revset length, so we use become().
# * Avoid needless $($ABSPATHSCRIPT...) invocations, these cause significant slowdowns during startup
