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
set_theme(name)Switch the active theme at runtime
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

set_theme(name) changes the currently active theme immediately. This is exposed to Lua for custom workflows and plugins, but it is not wired to the UI by default, so you need to call it from your own action, binding, or startup logic.


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({ value? })Open bookmark input, optionally prefilled
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