Jujutsu UI

jjui is a terminal user interface for working with Jujutsu version control system. I have built it according to my own needs and will keep adding new features as I need them. I am open to feature requests and contributions.

Features

Core Features

  • Preview: View diffs and details of commits and files
  • Details: Explore files in a commit, with diff, split, and restore capabilities
  • Oplog: View the operation log of your repository
  • Command Execution: Execute shell and jj commands directly from jjui
  • Custom Commands: Define your own commands with custom keybindings
  • Fuzzy File Finder: Fuzzy find on all files and explore changes on them.
  • Ace Jump: Quickly move between revisions using fast key strokes.

Revision Operations

  • Rebase: Interactive rebase of commits
  • Absorb: Automatically absorb changes into commits
  • Abandon: Remove commits from your history
  • Duplicate: Copy revisions with interactive UI

Customization

  • Configuration: How to configure JJUI to your needs
  • Command Line Options: Customize behavior with command line flags
  • Leader Key: Create powerful mnemonic shortcuts with nested keymaps
  • Themes: Customize the appearance of the UI with detailed styling options

Preview

You can open the preview window by pressing p.

Preview window displays output of the jj show (or jj op show) command of the selected revision/file/operation.

You can specify commands to display the content of different item types in the preview window using the configuration options under the preview table in your configuration file.

The default commands are:

[preview]
revision_command = ["show", "--color", "always", "-r", "$change_id"]
oplog_command = ["op", "show", "$operation_id", "--color", "always"]
file_command = ["diff", "--color", "always", "-r", "$change_id", "$file"]

While the preview window is showing, you can press; ctrl+n to scroll one line down, ctrl+p to scroll one line up, ctrl+d to scroll half page down, ctrl+u to scroll half page up.

GIF

Configuration

By default preview window has the following configuration:

[keys.preview]
  mode = ["p"]
  scroll_up = ["ctrl+p"]
  scroll_down = ["ctrl+n"]
  half_page_down = ["ctrl+d"]
  half_page_up = ["ctrl+u"]
  expand = ["ctrl+h"]
  shrink = ["ctrl+l"]
[preview]
  show_at_start = false
  width_percentage = 50.0
  width_increment_percentage = 5.0
  revision_command = ["show", "--color", "always", "-r", "$change_id"]
  oplog_command = ["op", "show", "$operation_id", "--color", "always"]
  file_command = ["diff", "--color", "always", "-r", "$change_id", "$file"]

Details

Pressing l (as in going right into details of a revision) will open the details view of the revision you selected.

GIF

In this mode, you can:

  • Leave the details mode using h
  • Split selected files using s
  • Restore selected files using r
  • View diff of the highlighted file by pressing d
  • Change revset to files(name of the selected file) using *, effectively showing all revisions that touched that file.
  • Use J to jump to the parent revision
  • Use K to jump to the child revision

You can toggle file selection using m or space keys.

Showing diff

You can press d to show the diff of the selected file.

Splitting files in a revision

Pressing s splits the selected files into two revisions:

  • Selected files stay in the current revision.
  • Unselected files move to a new revision.

If no file is selected, the currently highlighted file will be split.

GIF

Restoring files in a revision

Pressing r restores the selected files to its previous state. (i.e. discard changes).

GIF

Show revisions that touched the file

Pressing * will change the revset to files(highligted file name); effectively showing all revisions that touched that file.

image

Op Log

You can switch to op log view by pressing o.

Op log view is limited to show last 200 operations by default. This can be changed by setting the oplog.limit configuration.

You can use the preview window (opened by pressing p) to display the contents of the selected operation.

Pressing r will restore the working directory to the state of that operation, essentially executing jj op restore <operationid>. You can always undo this operation by pressing u.

Pressing d will show the operation changes in full diff view.

GIF

Command Execution

You can execute shell and jj commands directly from jjui.

Execute jj commands

Press : to run interactive jj commands. jjui will suspend, and the command will run directly in your terminal (e.g., : restore -i). You will return to jjui when the command finishes.

Execute shell commands

Press $ to run any shell command without leaving jjui. (e.g., $ man jj or $ htop)

Context-aware placeholders

Both command types support context-aware placeholders that are replaced with values from your current selection:

  • $file: The currently selected file
  • $change_id: The ID of the selected revision
  • $operation_id: The ID of the selected operation
  • $revset: The current revset query

These placeholders make it easy to create powerful commands that operate on your current selection.

History files

History for exec :, $ commands is stored in the $XDG_CACHE_HOME/jjui/history/ directory.

A separate history file is used for jj and shell commands exec_jj and exec_sh respectively. For simplicity, each line of history files represent an item.

If needed you can use the following to import your shell history and make it available on jjui's shell prompt:

mkdir -p ~/.cache/jjui/history
history >> ~/.cache/jjui/history/exec_sh

Custom commands can be defined in the custom_commands section of the configuration:

[custom_commands]
"show diff" = { key = ["U"], args = ["diff", "-r", "$change_id", "--color", "always"], show = "diff" }
"show oplog diff" = { key = ["ctrl+o"], args = ["op", "show",  "$operation_id", "--color", "always"], show = "diff" }
"resolve vscode" = { key = ["R"], args = ["resolve", "--tool", "vscode"], show = "interactive" }
"new main" = { args = ["new", "main"] }
"tug" = { key = ["ctrl+t"], args = ["bookmark", "move", "--from", "closest_bookmark($change_id)", "--to", "closest_pushable($change_id)"] }

Custom commands can have placeholder arguments like $change_id, $operation_id, $file, and $revset.

Custom commands can also change the revset. For example, the following custom command will filter the view to only show descendants of the selected revision:

[custom_commands]
"show after revisions" = { key = ["M"], revset = "::$change_id" }

There is also a show argument which you can set it to be:

  • none (default); command will run as is and will only be displayed in the status bar.
  • diff: the output of the command will be displayed in the diff viewer.
  • interactive: the command run in interactive mode similar to diffedit, split, commit etc.

Custom commands menu can be opened by pressing x key. Each custom command can have a dedicated optional custom key binding which you can use to invoke it without having to open the custom commands menu.

Custom command window is context aware so it won't display the commands that have place holder but not applicable to the selected item.

image

Loading jj aliases as custom_commands. (idea from #211)

You can use the following to import all your aliases as custom commands.

echo "[custom_commands]" >> $JJUI_CONFIG_DIR/config.toml
jj config list aliases -T '"\"" ++ name ++ "\" = { args = [ \"" ++ name ++ "\" ] }\n"' --no-pager |\
    sed -e 's/aliases.//g' |\
    tee -a $JJUI_CONFIG_DIR/config.toml

jjui features a fuzzy file finder that can be activated using ctrl+t shortcut by default.

Selecting a file

When activated, you will see the list of all files that are present on the current revision. Press esc to cancel the file search.

When you select a file (via enter) the current revset will be changed to show all revisions that have touched the selected file.

You can navigate the candidates list using up/down and ctrl+n/ctrl+p as you'd expect on shell prompts.

Using tab will accept the selected file, updating the revset, but will not close the file search like enter does.

You can filter by typing parts of the path name. Space ( ) is used to refine search, that is, to search again but only on the currently matching elements.

We use the excellent sahilm/fuzzy library to perform search, which is also used in some charmbracelet UI widgets.

Refined search is useful particularly for finding files, because sahilm/fuzzy ranks results higher if they have a better match closer to the beginning of the string. However when finding files, you most likely remember the file name (being the furthest part of the whole path), so if you type the file name first and then space you can then filter by directory path.

Live mode

Upon entering fuzzy file search with ctrl+t, if you press ctrl+t again, live preview will be activated.

Live mode will show the revset for the file as you type it and also the show the revision preview.

When live mode is active, up and down are used to move on the revision list, allowing you to see the diff of the selected revision for the file being explored. This is useful when exploring the changes made to a file and quickly moving to explore other files if needed.

Also, during live mode, ctrl+n and ctrl+p are used to scroll the diff preview.

Ace Jump is a feature in various coding environments, like Emacs and IntelliJ, that allows users to quickly navigate to specific locations in their code with minimal keystrokes. It enhances efficiency by enabling fast cursor movement across the editor.

On jjui, you can enable Ace Jump mode by using f (mnemonic: find) also used as a motion in many vim-like environments.

Once enabled you will see change_ids and commit_ids get highlighted with a key you can press to go directly to that revision.

Jump is performed as soon as the shortest non-ambiguous prefix has been entered. This typically involves 1-3 keystrokes in most cases.

Rebase

You can rebase a revision, branch or source onto/before/after another revision in the revision tree.

GIF

Pressing r enter the rebase mode and by default source is set to source and target is set to onto.

During rebase, there are source and target options that you can set to choose what type of rebase you want. Source defines which revisions you want to be rebased and target defines where and how you want to be rebased.

Source

revision

Can be set by pressing r. Only the selected revision will be moved to the target.

branch

Can be set by pressing B. All revisions in the branch of the selected revision will be moved to the target.

source

Can be set by pressing s. Selected revision and its descendants will be moved to the target.

Target

onto

Can be set by pressing d. Source revisions will be branched off the target revision.

image when applied: image

after

Can be set by pressing a. Source revision(s) will be placed after the target revision. image when applied: image

before

Can be set by pressing b. Source revision(s) will be placed before the target revision.

image

when applied:

image

insert

The revision that's selected by pressing i is set to be the --insert-after argument and the one that you have selected by pressing enter is going to be the --insert-before argument. Effectively running the following command:

jj rebase -r <the revision when r was pressed> --insert-after <the revision when i was pressed> --insert-before <revision when enter was pressed>

before:

image

after:

image

You can press A to run jj absorb on the highlighted revision.

Revisions that "absorbed" the changes will show (affected by last operation) hint line next to them.

image

You can press a for running jj abandon against the highlighted or selected revisions. You can select multiple revisions by pressing space.

image

Duplicate

You can now duplicate one or more revisions directly from the UI.

Select the revision(s) you want to copy and press y (the default key) to enter "duplicate mode".

How it works

  1. Select Source: First, select the revision(s) you want to duplicate
  2. Enter Duplicate Mode: Press y to start the duplicate operation
  3. Choose Destination: Navigate to the target revision where you want to place the copy
  4. Fine-tune Placement: Use these sub-keys to control the exact placement:
    • a: Place the copy after the target revision
    • b: Place the copy before the target revision
    • d: Place the copy onto the target revision (i.e., --destination)
  5. Confirm: Press Enter to execute the command

The UI provides a live preview of the operation to help you understand exactly what will happen before you confirm.

JJUI loads configuration on start up from the system's config directory.

  • MacOS: ~/Library/Application Support/jjui/config.toml
  • Linux: ~/.config/jjui/config.toml
  • Windows: %AppData%/jjui/config.toml (Might have to manually create folder).
  • Custom: $JJUI_CONFIG_DIR/config.toml

You can edit the configuration in your $EDITOR by passing the --config argument:

jjui --config

Key bindings

Key binding support is limited by the key handling capabilities of the terminal emulator you are using.

Note: Order of the modifiers matter; for example ctrl+alt+up is read as alt+ctrl+up, so alt should come before ctrl.

Note: shift combined with letters is not supported by the underlying library that jjui is using for rendering and key handling. So, ctrl+shift+f is read as ctrl+f. However, shift+f can be defined as F, and it should work.

Colours and Themes

UI appearance is configured through the theming system. You can customize the colors and styles of various UI elements in your config.toml file or by creating custom theme files.

For example, to customize the appearance of selected items:

[ui.colors]
"selected" = { bg = "your colour" }

For more detailed customization options, see the Themes page.

Default configuration

You can find the default configuration in the repo here: https://github.com/idursun/jjui/blob/main/internal/config/default/config.toml

Command Line Options

jjui provides several command line options to customize its behavior. These options can be passed when starting the application.

Usage

jjui [flags] [location]

Where [location] is an optional path to a jj repository. If not provided, the current directory is used.

Available Flags

FlagAliasDescriptionDefault
--revset=<revset>-rSet default revsetEmpty (uses jj config)
--period=<seconds>-pOverride auto-refresh interval in secondsFrom config (set to 0 to disable)
--limit=<n>-nNumber of revisions to show0 (no limit)
--versionShow version information
--configOpen configuration file in $EDITOR
--helpShow help information

Examples

# Open jjui in the current directory
jjui

# Open jjui in a specific directory
jjui /path/to/repo

# Show only 10 revisions
jjui --limit 10

# Use a custom revset
jjui --revset "remote() & trunk()"

# Disable auto-refresh
jjui --period 0

# Open with a 5-second refresh interval
jjui --period 5

Configuration

jjui uses a configuration file for additional settings. You can edit this file using jjui --config.

For more information on other aspects of jjui:

Leader key is inspired by vim's Leader, and emacs' hydra/which-key/transient.

Leader is a prefix key that allows navigating a tree of keymaps where leafs are actions to be performed in the UI as if the user typed them directly.

When Leader is activated (default keybinding is backslash: \), the user can navigate a keymap tree using single letter keystrokes. Actions are leafs on this tree and represent key sequences sent to the UI.

Leader keymaps represent mnemonic shortcuts that can do anything that is possible via jjui key-bindings, and allow user defined workflows that fit each person's mental model.

Leader configuration (on your config.toml)

Leader keymaps are configured via the leader table.

[!IMPORTANT] Each entry is identified by an alphanumeric key-sequence.

And it can have the following optional attributes:

context: An array of context placeholders required to enable this entry.

help: Human message to show for keybinding.

send: An array of keys to send into the UI.

Each element of the send array is either a tea.keyName like enter, ctrl+s, down, etc. or a custom string sent directly into the UI.

Examples

The most basic example could be a Leader key h that sends ? into the main UI.

From the main UI, use the following key sequence to invoke it: \ h

[leader.h]
help = "Help"
send = ["?"]

More interesting are nested keymaps:

[leader.n]
context = ["$change_id"]
help = "New change"

[leader.na]
help = "After"
send = [ ":", "new -A $change_id", "enter" ]

[leader.nb]
help = "Before"
send = [ ":", "new -B $change_id", "enter" ]

In this example, \n does not have a send sequence, it is only used to set a help message for the n keymap. But it does have a context requirement, meaning that the n key and its nested keymaps are only visible when $change_id context value is available. That is, when a revision is selected.

From the main UI, entering the following key sequence: \na will send the configured keys into jjui's event loop as if typed by the user:

  • : Opens "exec jj" interactive command.
  • types new -A $change_id
  • executes the command by sending enter key.

Contribute Leader keys that might be useful to others.

Leader keys are intended to be user defined, so they fit your workflow and help you easily do repetitive tasks.

You are free to adapt these examples as you like.

If you want to share some keymaps that might be valuable or serve as inspiration for others, this is the place :).

Edit a file from revision detail. (idea from #184)

The following key \E is enabled only when the cursor is over a file in details view. It will open $file after making $change_id current.

[leader.e]
context = [ "$file", "$change_id" ]
help = "Edit file in @"
send = [ "$", "$EDITOR $file", "enter" ]

[leader.E]
context = [ "$file", "$change_id" ]
help = "Edit file in change"
send = [ "$", "jj edit $change_id && $EDITOR $file", "enter" ]

Save the current revset under a new alias. (idea from #169)

NOTE: uses gum to prompt for the revset alias.

[leader.R]
context = [ "$revset" ]
help = "Save revset"
send = [ "$", "jj config set --repo revset-aliases.$(gum input --placeholder 'Revset Alias') $revset", "enter" ]

Create a bookmark on current change (idea from #67)

[leader.bn]
help = "Set new bookmark"
send = [ "$", "jj bookmark set -r $change_id $(gum input --placeholder \"Name of the new bookmark\")", "enter" ]

Themes allow for detailed control over the application's appearance.

The following configuration loads a base theme from ~/.config/jjui/themes/my-theme.toml.

# ~/.config/jjui/config.toml
[ui]
theme = "my-theme"
# ~/.config/jjui/themes/my-theme.toml
"selected" = { bg = "red", bold = true }

Available themes.

Overriding theme colors

You can still override certain parts of theme by defining the overrides inside ui.colors section. For example the following configuration will load the theme from my-theme file but will set the selected style's background colour to red.

[ui]
theme = "my-theme"

[ui.colors]
"selected" = { bg = "red" }

[!NOTE] Theme support is actively being developed. The information on this page is subject to change as the application evolves.

Style Format

Themes are defined as a series of key-value pairs in a TOML file. Each entry consists of a selector (the key) that targets a UI element, and a style table (the value) that defines its appearance.

Example:

"selected" = { fg = "#FF8C00", bg = "#2B2B2B", bold = true }
"border" = { fg = "bright black" }

Style Properties

The style table can contain any of the following properties:

PropertyTypeDescription
fgColorSets the foreground (text) color.
bgColorSets the background color.
boldboolIf true, makes the text bold.
underlineboolIf true, adds an underline to the text.
strikethroughboolIf true, adds a strikethrough line.
italicboolIf true, makes the text italic.

Color Formats

Colors can be specified in one of three formats:

  • TrueColor (Hex): A string representing a hex color code (e.g., "#FF4500").
  • Base16 Names: A string for standard terminal colors (e.g., red, bright green, white).
  • ANSI256 Codes: An integer from 0 to 255.

Selector Inheritance

Style resolution uses a fallback system to apply styles. This allows you to define general styles and override them with more specific ones, reducing repetition.

You can use broad styles like "selected" or "border" to apply globally. Then you can define component-level styles like "revisions" to style an entire section. Then, you can use more specific selectors like "revisions selected" to override the appearance for a specific state within that component.

Resolution Example for "revisions details selected" selector:

  1. The engine looks for a "revisions details selected" style.
  2. It then inherits from "revisions details".
  3. It then inherits from "revisions"
  4. It then inherits from "details selected".
  5. It then inherits from "details".
  6. Finally, it inherits from the base "selected" style.

UI Elements & Selectors

Global Styles

These are base elements that apply throughout the application unless overridden by a more specific selector.

  • text: The default style for all text.
  • dimmed: Less important text, such as hints, descriptions, and inactive elements.
  • selected: The style for a currently highlighted or active item in a list or menu.
  • border: The style for borders around windows, panes, and pop-ups.
  • title: The style for titles in windows, panes, and menus.
  • shortcut: The style for keyboard shortcuts (e.g., [Enter], [q]).
  • matched: The style for the part of the text that matches user input, typically in a completion or filter.

Operation-Specific Styles

These styles appear during interactive operations like rebase, squash or duplicate.

  • source_marker: The marker for the revision being moved or acted upon.
  • target_marker: The marker for the destination of the operation.

Application Sections

Revset (Top Bar)

The input bar at the top of the screen.

  • revset title: The "Revset:" label.
  • revset text: The user input area. It's recommended to make this bold.
  • Completions Dropdown:
    • revset completion selected: The highlighted item in the completions list.
    • revset completion matched: The part of a completion that matches the input.
    • revset completion dimmed: The auto-suggested part of a completion.

Revisions / Oplog (Main List View)

The central list of commits or operations.

  • revisions: The base style for the entire list area.
  • revisions selected: The currently highlighted line.
  • revisions dimmed: Hint text shown during interactive operations.

Status (Bottom Bar)

The bar at the bottom showing the current mode and available actions.

  • status: The base style for the entire bar. A distinct bg is recommended.
  • status title: The current mode indicator (e.g., NORMAL). A contrasting bg helps it stand out.
  • The actions also uses shortcut and dimmed styles.

Evolog (Sub-List View)

The pop-up list showing the evolution history for a revision.

  • evolog: Base style for the view.
  • evolog selected: The highlighted item. Can be styled differently from revisions selected to show which pane is active.

Pop-up menus for primary actions.

  • menu: Base style for menus. Should have a border.
  • Selected item is styled with menu selected
  • Filter is styled with menu matched.
  • Items use menu shortcut, menu title, and menu dimmed styles.

Help Window

The pop-up window displaying key-bindings and help text.

  • help: The base style for the window. To avoid a "patchy" look, define a bg color here. This color will serve as the background for the entire content area.
  • The window uses border and title styles.

Preview (Side Pane)

The pane on the right that shows diffs or other details.

  • preview: The base style for the pane.
  • Uses preview border style for its frame.

Confirmation Dialog

The small inline dialog for confirmations (e.g., "Abandon all?").

  • confirmation: Base style for the dialog. Should have a border.
  • The message uses the global text style.
  • Options use selected (for the highlighted choice) and dimmed (for other choices) styles.

Example "Fire" theme

fire theme screenshot
"text" = { fg = "#F0E6D2", bg = "#1C1C1C" }
"dimmed" = { fg = "#888888" }
"selected" = { bg = "#4B2401", fg = "#FFD700" }
"border" = { fg = "#3A3A3A" }
"title" = { fg = "#FF8C00", bold = true }
"shortcut" = { fg = "#FFA500" }
"matched" = { fg = "#FFD700", underline = true }
"source_marker" = { bg = "#6B2A00", fg = "#FFFFFF" }
"target_marker" = { bg = "#800000", fg = "#FFFFFF" }
"revisions rebase source_marker" = { bold = true }
"revisions rebase target_marker" = { bold = true }
"status" = { bg = "#1A1A1A" }
"status title" = { fg = "#000000", bg = "#FF4500", bold = true }
"status shortcut" = { fg = "#FFA500" }
"status dimmed" = { fg = "#888888" }
"revset text" = { bold = true }
"revset completion selected" = { bg = "#4B2401", fg = "#FFD700" }
"revset completion matched" = { bold = true }
"revset completion dimmed" = { fg = "#505050" }
"revisions selected" = { bold = true }
"oplog selected" = { bold = true }
"evolog selected" = { bg = "#403010", fg = "#FFD700", bold = true }
"help" = { bg = "#2B2B2B" }
"help title" = { fg = "#FF8C00", bold = true, underline = true }
"help border" = { fg = "#3A3A3A" }
"menu" = { bg = "#2B2B2B" }
"menu title" = { fg = "#FF8C00", bold = true }
"menu shortcut" = { fg = "#FFA500" }
"menu dimmed" = { fg = "#888888" }
"menu border" = { fg = "#3A3A3A" }
"menu selected" = { bg = "#4B2401", fg = "#FFD700" }
"confirmation" = { bg = "#2B2B2B" }
"confirmation text" = { fg = "#F0E6D2" }
"confirmation selected" = { bg = "#4B2401", fg = "#FFD700" }
"confirmation dimmed" = { fg = "#888888" }
"confirmation border" = { fg = "#FF4500" }
"undo" = { bg = "#2B2B2B" }
"undo confirmation dimmed" = { fg = "#888888" }
"undo confirmation selected" = { bg = "#4B2401", fg = "#FFD700" }
"preview" = { fg = "#F0E6D2" }

How do I .... ?

Welcome to our How do I ...? questions and answers program.

The intention of this page is to document some use-cases that are totally possible
but maybe are not (as of today) directly implemented as key-bindings on jjui.

It would be very difficult to have key-bindings for every imaginable workflow,
so instead, we have tried for jjui to be flexible enough to allow you do stuff
even if they are not directly implemented as jjui features.

We are of course, open to implementing features that are valuable to most of jjui users,
so if something on this page seems like can be made into jjui's codebase, please open a
Feature-Request, an Implementation-Proposal or Pull-Request.
Feel free to open an Q&A discussion for anything not covered here.

Also, remember that jjui's UI is scriptable, and anything that is possible via the UI
can be assigned key-bindings using Leader-Key or Custom-Commands

How do I edit files with conflicts?

Move to the revision marked as having conflicts,

  • Use d to enter the details view, you will be presented with a list of files changed
    in that revision and will see the files containing conflicts.
  • Use (space) to check (✓) the files you want to edit.
  • Use $ vim $checked_files to edit all of them in vim.

How do I create a new change directly upon the current revision?

  • Use : new -A $commit_id

How do I create a mega merge?

  • Use (space) to check (✓) the revisions you want to merge.
  • Use : new all:$checked_commit_ids to create a merge commit having multiple parents.

How do I squash together specific files from multiple revisions?

  • Use (space) to check (✓) the revisions you want to squash from.
  • Open d (details) on one of these revisions and
    use (space) to check (✓) the files from details view.
  • Use : squash --from $checked_commit_ids --into @ $checked_files

This will squash the content of checked files from the checked revisions into the working copy.