Skip to content

Lua Scripting in jjui

jjui supports lua scripting to extend functionality and create custom workflows. Scripts can access the Jujutsu command-line, manipulate the UI, query context, and interact with the user.

Add a Lua custom command to your config file (~/.config/jjui/config.toml):

[custom_commands.hello]
key = ["ctrl+h"]
lua = '''
flash("Hello from Lua!")
'''

Press Ctrl+h in jjui to see the message.

Lua scripts run asynchronously using coroutines. Functions that interact with the UI (like jj_async, choose, input) yield control until completion. This allows non-blocking operations.

Query the current selection and checked items.

FunctionDescriptionReturns
context.change_id()Returns the change ID of the currently selected item (works for revisions and files)string or nil
context.commit_id()Returns the commit ID of the currently selected item (works for revisions, files, and commits)string or nil
context.file()Returns the file path if a file is currently selected (details view)string or nil
context.operation_id()Returns the operation ID if viewing the operations logstring or nil
context.checked_files()Returns an array of checked file pathstable (array of strings)
context.checked_change_ids()Returns an array of checked change IDstable (array of strings)
context.checked_commit_ids()Returns an array of checked commit IDstable (array of strings)

Manipulate revisions and trigger UI actions.

FunctionDescriptionReturnsExample
revisions.current()Returns the change ID of the currently selected revisionstring or nil
revisions.checked()Returns an array of checked revision change IDstable (array of strings)
revisions.open_details()Open the details view for the selected revision
revisions.refresh(options)Refreshes the revision list. Options: keep_selections (bool), selected_revision (string)revisions.refresh({keep_selections = true})
revisions.navigate(options)Navigate the revision list. Options: by (int), page (bool), target (string: "parent", "child", "working_copy"), to (string), fallback (string), ensureView (bool), allowStream (bool)revisions.navigate({by = 5}) or revisions.navigate({target = "parent"})
revisions.start_squash(options)Initiate squash operation. Options: files (table of strings)revisions.start_squash({files = {"main.go"}})
revisions.start_rebase(options)Initiate rebase operation. Options: source (string: "revision", "branch", "descendants"), target (string: "destination", "after", "before", "insert")revisions.start_rebase({source = "branch", target = "after"})
revisions.start_inline_describe()Open inline editor to change the description. Yields until editor is closedbool (true if applied)local applied = revisions.start_inline_describe()

Manage the current revset filter.

FunctionDescriptionReturnsExample
revset.current()Returns the current revset expressionstring
revset.default()Returns the default revset expression from configstring
revset.set(expression)Set a new revset expressionrevset.set("root()")
revset.reset()Reset to the default revset

Execute Jujutsu commands in different modes.

FunctionModeReturnsUse ForExample
jj(...args)Synchronousoutput (string), error (string or nil)Quick queries that don’t require UI interactionlocal output, err = jj("log", "-r", "@", "-T", "change_id")
jj_async(...args)AsynchronousNothing (fire-and-forget)Commands that modify state but don’t need outputjj_async("bookmark", "create", "feature")
jj_interactive(...args)InteractiveNothingCommands requiring user input or editor (e.g., split, absorb)jj_interactive("split")

Arguments: All functions accept varargs or a table of strings: jj("log", "-r", "@") or jj({"log", "-r", "@"})

Details:

  • jj() blocks script execution until command completes, returns output
  • jj_async() dispatches command and immediately continues script execution
  • jj_interactive() opens command in terminal for user interaction
FunctionDescriptionReturnsExample
flash(message)Display a temporary message to the userflash("Done!")
choose(options) or choose({options = ..., title = ...})Show selection menu, wait for user choice. Accepts varargs, table, or options object with options (table) and title (string)string or nillocal choice = choose("Yes", "No") or choose({options = {"a", "b"}, title = "Pick"})
input(options)Show input prompt. Options: title (string), prompt (string)string or nillocal text = input({title = "Name", prompt = "Enter: "})
FunctionDescriptionReturnsExample
copy_to_clipboard(text)Copy text to system clipboardbool, error (string or nil)local ok, err = copy_to_clipboard("text")
split_lines(text, keep_empty)Split text into lines. By default, empty lines are removed. Args: text (string), keep_empty (bool, default: false)table (array of strings)local lines = split_lines(output) or split_lines(output, true)
exec_shell(command)Execute a shell command interactively. Unlike os.execute, this properly returns to jjui after the command exits.boolexec_shell("vim " .. file)
[custom_commands.copy_to_clipboard]
key = ["Y"]
lua = '''
local checked_files = context.checked_files()
if checked_files and #checked_files > 0 then
local file_names = table.concat(checked_files, " ")
copy_to_clipboard(file_names)
flash("Copied checked files: " .. file_names)
return
end
local selected_file = context.file()
if selected_file then
copy_to_clipboard(selected_file)
flash("Copied file: " .. selected_file)
return
end
local change_id = context.change_id()
if change_id then
copy_to_clipboard(change_id)
flash("Copied change ID: " .. change_id)
return
end
flash("No item selected to copy")
'''

Show menu of common revsets:

[custom_commands.quick_revset]
key = ["ctrl+r"]
lua = '''
local selected = choose({
options = {
"mine()",
"all()",
"trunk()..@",
revset.default()
},
title = "Quick Revset"
})
if selected then
revset.set(selected)
flash("Revset: " .. selected)
end
'''

jjui_custom_command

Open a file in editor in details view.

[custom_commands.open_file]
key = ["O"]
lua = '''
local file = context.file()
if not file then
flash("No file selected")
return
end
-- exec_shell returns to jjui after the editor exits
exec_shell("vim " .. file)
-- or your external editor
os.execute("code " .. file)
'''

Create new revision after a revision, like jj new --insert-after -r $ChangeID

[custom_commands.new_insert_after]
key = ["N"]
lua = '''
jj("new", "-A", context.change_id())
revisions.refresh()
local new_change_id = jj("log", "-r", "@", "-T", "change_id.shortest()", "--no-graph")
revisions.navigate{to=new_change_id}
'''
[custom_commands.push_with_name]
key = ["B"]
lua = '''
local branch_name = jjui.input({
title = "Create New Branch",
prompt = "Branch name: "
})
local rev = revisions.current()
if branch_name then
jj("git", "push", "--named", branch_name.."="..rev)
revisions.refresh()
end
'''
Contribute Community