Skip to content

Lua Scripting

jjui exposes a Lua API for custom workflows and UI automation.

For startup configuration wiring (setup(config), config.action, config.bind), see config.lua.

For LuaLS autocomplete and basic type information, run:

Terminal window
jjui --install-lua-types

This installs generated jjui Lua API metadata into your config directory. See config.lua for details.

Define a Lua action in config.toml and bind it:

[[actions]]
name = "hello-lua"
lua = '''
flash("Hello from Lua")
'''
[[bindings]]
action = "hello-lua"
key = "ctrl+h"
scope = "revisions"
desc = "hello"

Helper functions and the revisions, revset, and context tables are available both as top-level globals and under the jjui namespace — flash(...) and jjui.flash(...) are the same, and revisions.edit() and jjui.revisions.edit() are the same.

Other built-in action scopes (ui, git, diff, oplog, bookmarks, undo, redo, choose, input, command_history, file_search) are only available under the jjui namespace — e.g. jjui.git.push(), not git.push().

Built-in action wrappers are additionally available under jjui.builtin.*, which forces built-in resolution and ignores any user-defined overrides for those action names.


Read-only access to the currently highlighted or selected item.

FunctionReturnsNotes
context.change_id()stringAvailable for revisions and files
context.commit_id()stringAvailable for revisions, files, and commits
context.file()stringOnly when a file is selected in the details view
context.operation_id()stringOnly in the oplog
context.checked_files()string[]Files checked in the details view
context.checked_change_ids()string[]Checked revisions or files
context.checked_commit_ids()string[]Checked revisions, files, or commits

FunctionReturnsNotes
revisions.current()stringchange_id of the highlighted revision
revisions.checked()string[]change_id of every checked revision
revisions.refresh({ keep_selections?, selected_revision? })Yields until the revision list finishes loading
revisions.navigate({ by?, page?, target?, to?, fallback?, ensureView?, allowStream? })target: "parent", "child", or "working"

FunctionReturns
revset.current()Active revset expression string
revset.default()Default revset from config

FunctionReturnsNotes
jj(...args)(output, err)Runs synchronously; returns output string and error
jj_async(...args)Yields until the command completes
jj_interactive(...args)Opens an interactive terminal for the command

Args can be passed as individual strings or as a single table of strings.


FunctionReturnsNotes
flash(text)Show a short status message
flash({ text, error?, sticky? })error = true styles as error; sticky = true keeps it visible
choose(option1, option2, ...)string or nilShow a picker; returns the selected value or nil if cancelled
choose({ options, title?, filter?, ordered? })string or nilTable form
input({ title?, prompt? })string or nilShow a text input dialog; returns the entered text or nil
copy_to_clipboard(text)(true, nil) or (nil, err)Copy to system clipboard
split_lines(text, keep_empty?)string[]Split on newlines; strips \r; skips empty lines by default
exec_shell(command)Run a shell command line; yields until done

These pause the script until a matching UI event arrives.

FunctionReturnsNotes
wait_close()boolWaits for any modal or view to close; returns true if it was applied
wait_refresh()Waits for the revision list to finish refreshing

Every built-in action is exposed as a Lua function via jjui.<scope>.<action>(). These dispatch the action into the UI and yield until it is handled, so they compose naturally with other async steps.

In every scope that has a cancel() action, close() is available as an alias — e.g. jjui.git.close() is the same as jjui.git.cancel().

Actions that take a single required string arg accept it positionally:

revset.set("trunk()")
-- equivalent to:
revset.set({ value = "trunk()" })
FunctionNotes
revisions.absorb()Run jj absorb on the current revision
revisions.new()Create a new revision
revisions.commit()
revisions.describe()Open external editor to edit description
revisions.edit()
revisions.force_edit()
revisions.diff()
revisions.diff_edit()
revisions.split()
revisions.split_parallel()
revisions.apply({ force? })
revisions.force_apply()
revisions.toggle_select()
revisions.refresh()
revisions.move_up() / move_down()
revisions.page_up() / page_down()
revisions.jump_to_parent()
revisions.jump_to_children()
revisions.jump_to_working_copy()
revisions.ace_jump()
revisions.quick_search_next() / _prev() / _clear()
revisions.open_abandon()Enter abandon mode
revisions.open_details()Open the details view
revisions.open_duplicate()Enter duplicate mode
revisions.open_evolog()Open the evolution log
revisions.open_inline_describe()Start inline describe
revisions.open_rebase()Enter rebase mode
revisions.open_revert()Enter revert mode
revisions.open_set_bookmark()Open bookmark input
revisions.open_set_parents()Enter set-parents mode
revisions.open_squash()Enter squash mode
FunctionArgs
revisions.rebase.set_source({ source })"revision", "branch", or "source" (descendants)
revisions.rebase.set_target({ target })"onto", "after", "before", or "insert"
revisions.rebase.skip_emptied()Toggle
revisions.rebase.apply({ force? })
revisions.rebase.force_apply()
revisions.rebase.cancel()
revisions.rebase.target_picker()
revisions.rebase.ace_jump()
revisions.rebase.jump_to_working_copy()
FunctionArgs
revisions.squash.interactive()Toggle
revisions.squash.keep_emptied()Toggle
revisions.squash.use_destination_msg()Toggle
revisions.squash.apply({ force? })
revisions.squash.force_apply()
revisions.squash.cancel()
revisions.squash.target_picker()
revisions.squash.ace_jump()
revisions.squash.jump_to_working_copy()
FunctionArgs
revisions.duplicate.set_target({ target })"onto", "after", "before", or "insert"
revisions.duplicate.apply({ force? })
revisions.duplicate.force_apply()
revisions.duplicate.cancel()
revisions.duplicate.target_picker()
revisions.duplicate.ace_jump()
revisions.duplicate.jump_to_working_copy()
FunctionArgs
revisions.revert.set_target({ target })"onto", "after", "before", or "insert"
revisions.revert.apply({ force? })
revisions.revert.force_apply()
revisions.revert.cancel()
revisions.revert.target_picker()
Function
revisions.abandon.toggle_select()
revisions.abandon.select_descendants()
revisions.abandon.apply({ force? })
revisions.abandon.force_apply()
revisions.abandon.cancel()
revisions.abandon.ace_jump()
revisions.abandon.jump_to_working_copy()
Function
revisions.set_parents.toggle_select()
revisions.set_parents.apply()
revisions.set_parents.cancel()
revisions.set_parents.ace_jump()
revisions.set_parents.jump_to_working_copy()
FunctionNotes
revisions.inline_describe.accept({ force? })Apply the description
revisions.inline_describe.force_accept()Apply the description with force
revisions.inline_describe.new_line()Insert a line break
revisions.inline_describe.editor()Open external editor
revisions.inline_describe.cancel()Cancel without saving
FunctionArgs
revisions.details.select_file(file)Required string (positional)
revisions.details.toggle_select()
revisions.details.squash()
revisions.details.absorb()
revisions.details.split()
revisions.details.split_parallel()
revisions.details.restore()
revisions.details.diff()
revisions.details.refresh()
revisions.details.revisions_changing_file()
revisions.details.move_up() / move_down()
revisions.details.page_up() / page_down()
revisions.details.cancel() / revisions.details.quit()

Shown when a details operation (e.g. squash) affects multiple revisions and needs confirmation.

Function
revisions.details.confirmation.apply({ force? })
revisions.details.confirmation.force_apply()
revisions.details.confirmation.cancel()
revisions.details.confirmation.next()
revisions.details.confirmation.prev()
Function
revisions.evolog.restore()
revisions.evolog.apply({ force? })
revisions.evolog.diff()
revisions.evolog.move_up() / move_down()
revisions.evolog.page_up() / page_down()
revisions.evolog.cancel() / revisions.evolog.quit()
Function
revisions.ace_jump.apply()
revisions.ace_jump.cancel()

Shared target picker used by rebase, squash, duplicate, and revert.

Function
revisions.target_picker.move_up() / move_down()
revisions.target_picker.apply({ force? })
revisions.target_picker.force_apply()
revisions.target_picker.cancel()
revisions.target_picker.autocomplete() / autocomplete_back()
Function
revisions.set_bookmark.apply()
revisions.set_bookmark.cancel()
revisions.set_bookmark.autocomplete() / autocomplete_back()
FunctionArgs
revset.set(value)Required string (positional)
revset.reset()
revset.edit({ clear? })Open the revset editor
revset.apply()
revset.cancel()
revset.autocomplete() / autocomplete_back()Cycle completion suggestions
revset.move_up() / revset.move_down()Navigate completion list

Also available as a top-level global — ui.cancel() and jjui.ui.cancel() are the same.

FunctionNotes
jjui.ui.cancel()
jjui.ui.quit()
jjui.ui.suspend()
jjui.ui.open_bookmarks()
jjui.ui.open_git()
jjui.ui.open_oplog()
jjui.ui.open_undo()
jjui.ui.open_redo()
jjui.ui.open_revset()
jjui.ui.open_command_history()
jjui.ui.file_search_toggle()
jjui.ui.quick_search()
jjui.ui.expand_status()
jjui.ui.preview_toggle()
jjui.ui.preview_toggle_bottom()
jjui.ui.preview_expand() / jjui.ui.preview_shrink()
jjui.ui.preview_scroll_down() / jjui.ui.preview_scroll_up()
jjui.ui.preview_half_page_down() / jjui.ui.preview_half_page_up()
jjui.ui.preview.show(content)Required string (positional) — display content in the preview panel
jjui.ui.exec_jj()Open the jj command prompt
jjui.ui.exec_shell()Open the shell command prompt
Function
jjui.oplog.restore()
jjui.oplog.revert()
jjui.oplog.diff()
jjui.oplog.move_up() / jjui.oplog.move_down()
jjui.oplog.page_up() / jjui.oplog.page_down()
jjui.oplog.close() / jjui.oplog.quit()
Function
jjui.bookmarks.cycle_remotes() / jjui.bookmarks.cycle_remotes_back()
jjui.bookmarks.filter()
jjui.bookmarks.bookmark_delete()
jjui.bookmarks.bookmark_forget()
jjui.bookmarks.bookmark_move()
jjui.bookmarks.bookmark_track()
jjui.bookmarks.bookmark_untrack()
jjui.bookmarks.move_up() / jjui.bookmarks.move_down()
jjui.bookmarks.page_up() / jjui.bookmarks.page_down()
jjui.bookmarks.apply() / jjui.bookmarks.cancel() / jjui.bookmarks.quit()
Function
jjui.git.push()
jjui.git.fetch()
jjui.git.filter()
jjui.git.cycle_remotes() / jjui.git.cycle_remotes_back()
jjui.git.move_up() / jjui.git.move_down()
jjui.git.page_up() / jjui.git.page_down()
jjui.git.apply() / jjui.git.cancel() / jjui.git.quit()

Also available as a top-level global — diff.show(content) and jjui.diff.show(content) are the same.

Function
jjui.diff.scroll_up() / jjui.diff.scroll_down()
jjui.diff.page_up() / jjui.diff.page_down()
jjui.diff.half_page_up() / jjui.diff.half_page_down()
jjui.diff.move_top() / jjui.diff.move_bottom()
jjui.diff.left() / jjui.diff.right()
jjui.diff.show(content)
jjui.diff.toggle_wrap()
Function
jjui.undo.apply() / jjui.undo.cancel()
jjui.undo.prev() / jjui.undo.next()
jjui.redo.apply() / jjui.redo.cancel()
jjui.redo.prev() / jjui.redo.next()
Function
jjui.choose.apply()
jjui.choose.cancel()
jjui.choose.move_up() / jjui.choose.move_down()
Function
jjui.input.apply()
jjui.input.cancel()
Function
jjui.command_history.move_up() / jjui.command_history.move_down()
jjui.command_history.close()
jjui.command_history.delete_selected()
Function
jjui.file_search.move_up() / jjui.file_search.move_down()
jjui.file_search.page_up() / jjui.file_search.page_down()
jjui.file_search.preview_half_page_up() / jjui.file_search.preview_half_page_down()
jjui.file_search.toggle()
jjui.file_search.edit()
jjui.file_search.apply() / jjui.file_search.cancel()
Contribute Community