Skip to content

Instantly share code, notes, and snippets.

@noahmayr
Forked from chooglen/fzf-jj.zsh
Last active April 7, 2024 14:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noahmayr/ed545a6bcd7c27d19dab1b629af0d144 to your computer and use it in GitHub Desktop.
Save noahmayr/ed545a6bcd7c27d19dab1b629af0d144 to your computer and use it in GitHub Desktop.
Interactive `jj` with `fzf`
#!/bin/sh
### CONFIG ###
changeIdColumn="2"
commitIdColumn="3"
nodeTypes="@○◆•"
delimiterPattern='((?:^[^ ] +)|[^[:alnum:]:.])+'
defaultLogArgs=""
copyStdinToClipboard="pbcopy"
### END CONFIG ###
# need to export this so all subshells created by fzf are also going to use sh and the correct PATH env var
export SHELL=/bin/sh
export PATH="$PATH"
command="jj --color=always"
ignoreWcCommand="$command --ignore-working-copy"
baseLogCommand="$ignoreWcCommand log"
reload="toggle-preview+reload($baseLogCommand)+toggle-preview"
loadObsLog="reload($ignoreWcCommand obslog -r {$changeIdColumn})"
previewCommand="$ignoreWcCommand show --color-words {$commitIdColumn}"
selectedOrFocusedRevset="echo {+$commitIdColumn} | tr '[:blank:]' '|'"
pipeToPager="2>&1 | less -KR > /dev/tty"
logCommand="if [ -z \"\$FZF_BORDER_LABEL\" ]; then $baseLogCommand $defaultLogArgs; else $baseLogCommand -r \"\$FZF_BORDER_LABEL\"; fi"
reloadLog="reload($logCommand)+first"
rebaseCommand="$selectedOrFocusedRevset | xargs -I% $command rebase"
help=$'### NORMAL MODE ###
[q] Quit
[i] Enter revset query mode
[o] Enter obslog mode for focused revision
[esc] Reload
[enter] Print selected change ids to stdout
[u] Undo
[s] Status
[e] Edit focused
[n] New with selected parents
[d] Describe focused
[a] Abandon selected
[U] Rebase selected on trunk
[f] Fetch
[S] Sync
[r] Set selected as parents of @
[p] Add selected as parents of @
[P] Remove selected as parents of @
[c] Copy change id
[C] Copy commit id
[space] Show change in detail
(i) "selected" falls back to whichever revision is focused if there is no selection
### REVSET QUERY MODE ###
[esc] Go back to normal mode
[enter] search for revset, invalid queries will result in empty log (pressing enter with empty query will restore default query)
### OBSLOG MODE ###
[esc] Go back to normal mode
[space] Show commit in detail
[c] Copy change id
[C] Copy commit id
[enter] Print selected commit ids to stdout
'
normal_header='Press ? for help, q to quit'
revset_header='Press ? for help, ctrl+c to quit'
obslog_header='Press ? for help, q to quit'
$baseLogCommand |
fzf --ansi --exact --reverse --no-sort --tiebreak=index --border --no-info --multi --phony --no-mouse --prompt "normal" \
--delimiter "$delimiterPattern" \
--header 'Press ? for help, ctrl+q to quit' \
--preview "$previewCommand" \
--height 100% --preview-window right \
--bind "esc:change-prompt(normal)+rebind(u,S,e,n,d,a,U,f,s,r,p,p,i,o,space,y,Y,q,?)+$reloadLog" \
--bind "change:transform:[[ \$FZF_PROMPT ~= 'revset: ' ]] && echo 'clear-query'" \
--bind "?:execute(echo '$help' $pipeToPager)" \
--bind "u:execute($command undo $pipeToPager)+$reloadLog" \
--bind "s:execute($command status $pipeToPager)+$reloadLog" \
--bind "e:execute($command edit {$commitIdColumn} $pipeToPager)+$reloadLog" \
--bind "n:execute($command new {+$commitIdColumn} $pipeToPager)+$reloadLog" \
--bind "d:execute($command describe {$commitIdColumn} > /dev/tty)+$reloadLog" \
--bind "a:execute($command abandon {$commitIdColumn} $pipeToPager)+$reloadLog" \
--bind "U:execute($rebaseCommand -b 'all:%' -d 'trunk()' $pipeToPager)+$reloadLog" \
--bind "f:execute($command git fetch $pipeToPager)+$reloadLog" \
--bind "S:execute($command git push $pipeToPager)+$reloadLog" \
--bind "r:execute($rebaseCommand -r @ -d 'all:%' $pipeToPager)+$reloadLog" \
--bind "p:execute($rebaseCommand -r @ -d 'all:@- | %' $pipeToPager)+$reloadLog" \
--bind "P:execute($rebaseCommand -r @ -d 'all:@- & ~%' $pipeToPager)+$reloadLog" \
--bind "y:execute-silent(echo {$changeIdColumn} | $copyStdinToClipboard)" \
--bind "Y:execute-silent(echo {$commitIdColumn} | $copyStdinToClipboard)" \
--bind "q:abort" \
--bind "space:execute($command show --ignore-working-copy {$commitIdColumn} $pipeToPager)" \
--bind "o:change-prompt(obslog)+unbind(u,S,e,n,d,a,U,f,s,r,p,p,i,o)+rebind(?,space,y,Y,q)+$loadObsLog+first" \
--bind "i:change-prompt(revset: )+unbind(u,S,e,n,d,a,U,f,s,r,p,p,i,o,space,y,Y,q)+rebind(?)+$reloadLog" \
--bind "enter:transform:if [ \"\$FZF_PROMPT\" = 'revset: ' ]; then echo 'transform-border-label(echo \"\$FZF_QUERY\")+clear-query+$reloadLog'; elif [ \$FZF_PROMPT = 'normal' ]; then echo 'become(echo {+$changeIdColumn})'; else echo 'become(echo {+$commitIdColumn})'; fi" \
--bind "focus:transform:echo {} | grep -q '[$nodeTypes]' && exit || test {fzf:action} = up && echo up || echo down"
@noahmayr
Copy link
Author

noahmayr commented Apr 6, 2024

notes:

  • this was developed on macos (and qwertz keyboard, in case of shortcuts like alt-c/alt-p which is also ç/π on my keyboard)
  • only requires fzf and jj (pbcopy won't work on other systems though, replace with another copy to clipboard cli or just remove the copy commands)
  • it's important that the commit id and change id consistently stay in the same position, I've had to modify builtin_change_id_with_hidden_and_divergent_info to achieve this. Play around with the columns in the config until the correct one is extracted
  • the node symbols you use have to be configured, this is necessary to detect "real" lines when using multiline log
  • do not use any of the commands in the obslog view, I have yet to add :transform to all of them to prevent them from working in that mode

@glencbz
Copy link

glencbz commented Apr 7, 2024

The base of primitives here is excellent. Thanks so much!

I took most of the primitives here, kept my original idea of eval()-ing the query, and made the bindings more vimmish (i learned you could do this using change:clear-query + binding to single keys). The result is a huge QoL improvement for me, because I get to keep all of the Emacs-style keybindings to edit the query + I like single key bindings anyway.

#... config and variables
becomeNormal="change-prompt(NORMAL MODE: )+rebind(change,i,j,k,n,E,A,d,U,O,y,Y,space)+unbind(enter)"
becomeInsert="change-prompt(INSERT MODE: )+unbind(change,i,j,k,n,E,A,d,U,O,y,Y,space)+rebind(enter)"

eval $baseLogCommand |
    fzf --ansi --exact --reverse --no-sort --no-info --phony --prompt "NORMAL MODE: " \
        --delimiter "$delimiterPattern" \
        --header 'Press ? for help, ctrl+q to quit' \
        --preview "$command --ignore-working-copy show --color-words {$commitIdColumn}" \
        --height 100% --preview-window right \
        --bind "change:clear-query" \
        --bind "j:down" \
        --bind "k:up" \
        --bind "n:execute-silent($command new {+$commitIdColumn})+$reloadLog" \
        --bind "E:execute-silent($command edit {+$commitIdColumn})+$reloadLog" \
        --bind "A:execute-silent($command abandon {+$commitIdColumn})+$reloadLog" \
        --bind "d:execute-silent($command describe {+$commitIdColumn})+$reloadLog" \
        --bind "U:execute-silent($command undo)+$reloadLog" \
        --bind "space:execute($command show --ignore-working-copy {$commitIdColumn} | $pipeToPager)" \
        --bind "O:change-prompt(OBSLOG MODE)+$loadObsLog+first" \
        --bind "y:execute-silent(echo -n {$changeIdColumn} | $copyStdinToClipboard)" \
        --bind "Y:execute-silent(echo -n {$commitIdColumn} | $copyStdinToClipboard)" \
        --bind "enter:execute(eval {q})+$becomeNormal+$reload" \
        --bind "esc:$becomeNormal+$reload" \
        --bind "i:$becomeInsert" \
        --bind "start:$becomeNormal"

I'll probably post something somewhere sometime.

@noahmayr
Copy link
Author

noahmayr commented Apr 7, 2024

Updated this one to be more vimlike as well, you can enter revset mode by pressing i, entering a revset and submitting it with enter, the revset is then persisted in the FZF_BORDER_LABEL and can be cleared again by pressing enter with an empty query in revset mode.
obslog is also a separate mode with only a subset of keybinds supported.
I'm thinking about also adding a command mode for your eval needs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment