Skip to content

Instantly share code, notes, and snippets.

@ravicious
Last active May 11, 2026 12:36
Show Gist options
  • Select an option

  • Save ravicious/e6b9c69d9a72b4a02c030f6e45c59f5b to your computer and use it in GitHub Desktop.

Select an option

Save ravicious/e6b9c69d9a72b4a02c030f6e45c59f5b to your computer and use it in GitHub Desktop.
jj prompt for oh-my-zsh

jj-aware oh-my-zsh prompt

oh-my-zsh snippet that shows the closest Jujutsu bookmark when you're inside a jj repo, falling back to the git branch otherwise. Prefers mutable bookmarks (your working branches) over immutable ones (main, tags). Runs in oh-my-zsh's async-prompt background process so it doesn't block prompt redraw.

It uses git's dirty marker because jj doesn't understand submodules yet. Because of how jj log works, bookmarks that are out of sync with origin will have * appended to their names, which I think is a nice bonus.

Themes

Works with any oh-my-zsh theme that uses the standard git_prompt_info function and ZSH_THEME_GIT_PROMPT_* variables — robbyrussell, garyblessington, fwalch, and most simpler themes. Formatting (parens, colors, dirty marker) is inherited from the theme.

Does not work as-is with themes that roll their own VCS rendering: agnoster, powerlevel10k, spaceship. Those would need a similar override at their own entry point.

# .config/jj/config.toml
[revset-aliases]
"closest_bookmark(to)" = 'heads(::to & bookmarks())'
# Show the closest jj bookmark in the prompt when inside a jj repo, falling
# back to the standard git branch otherwise. Hooked into oh-my-zsh's async
# prompt machinery so the jj/git subprocess calls don't block prompt redraw:
# _omz_register_handler schedules our worker on every precmd to run in a
# background process; its stdout is captured into
# $_OMZ_ASYNC_OUTPUT[_omz_jj_or_git_prompt_info], and the theme's
# $(git_prompt_info) just reads that cached value. When the value changes,
# the machinery triggers `zle reset-prompt` to redraw.
function _omz_jj_or_git_prompt_info() {
# One jj invocation returns all closest ancestor bookmarks (mutable +
# immutable), each line tagged "m:" or "i:" via the template's
# if(self.immutable(), ...). Prefer mutable bookmarks; fall back to
# immutable; fall back to git if no bookmarks at all or jj failed (not a
# jj repo). The per-commit "\n" separator also prevents the multi-commit
# concatenation bug where sibling bookmarks would get glued together.
local output
output=$(jj log -r "closest_bookmark(@)" --ignore-working-copy --no-graph --color never \
--template 'if(self.immutable(), "i", "m") ++ ":" ++ self.bookmarks() ++ "\n"' 2>/dev/null) \
|| { _omz_git_prompt_info; return }
local mutable="" immutable=""
for line in ${(f)output}; do
case $line in
m:*) mutable+="${line#m:} ";;
i:*) immutable+="${line#i:} ";;
esac
done
local bookmark="${${mutable:-$immutable}% }"
if [[ -n "$bookmark" ]]; then
echo -n "${ZSH_THEME_GIT_PROMPT_PREFIX}${bookmark}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
else
_omz_git_prompt_info
fi
}
function git_prompt_info() {
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_jj_or_git_prompt_info]}"
}
_omz_register_handler _omz_jj_or_git_prompt_info
# oh-my-zsh's lib/git.zsh auto-registers _omz_git_prompt_info as an async
# handler at load time because $PROMPT still contains $(git_prompt_info).
# Our override no longer reads its cached output, so drop it from the async
# schedule. The function itself stays defined and is still invoked from
# inside _omz_jj_or_git_prompt_info as the fallback.
_omz_async_functions=(${_omz_async_functions:#_omz_git_prompt_info})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment