-
Notifications
You must be signed in to change notification settings - Fork 8
LUA_PLUGINS
Midnight Commander can load optional Lua 5.3+ support. Lua is not linked into
the mc executable: mc-lua.so is a runtime extension loaded through
dlopen only when it is available and enabled. That extension discovers and
runs Lua scripts.
See also the generated Lua API reference for the complete list of public methods and the Lua domain model for the ownership, lifecycle, and ABI boundaries behind those methods.
Build it with:
./configure --enable-lua-plugin=yes --with-lua=PREFIX
makeReplace PREFIX with the Lua installation prefix, for example /usr.
--enable-lua-plugin=auto is the default. In that mode the runtime is not
built when a compatible Lua development package is unavailable.
Lua is enabled by default in a Lua-enabled build. Put this in
~/.config/mc/ini to disable it persistently:
[Lua]
enabled=false
user_scripts_dir=user_scripts_dir may override the user script directory, but must be an
absolute path. mc --no-lua and MC_NO_LUA=1 mc prevent Lua code from being
loaded for one run.
Manage Plugins shows one native runtime, runtime / lua / Lua engine.
Clearing it disables all Lua code on the next start. Enter or F4 on that row
opens the global and user scripts from the mcedit workspace. In the script
list, F4 opens the selected package's declared entry file in the internal
editor; Run lists and invokes only actions registered by the selected
package. Event-only packages have no runnable action. Individual script
choices are stored as lua/<id>=true in
[DisabledPlugins] of ~/.config/mc/plugins.ini. No Lua script is hot-reloaded
during a session.
System scripts live in ${datadir}/mc/lua/scripts/. The primary user script
directory is ${XDG_DATA_HOME:-~/.local/share}/mc/lua/scripts/. For backward
compatibility MC also scans
${XDG_CONFIG_HOME:-~/.config}/mc/lua/scripts/, unless user_scripts_dir
explicitly overrides the user location. A script belongs to a fixed workspace,
determined by the directory immediately below scripts/:
scripts/editor/base64-decode/lua.ini
scripts/editor/base64-decode/lib/format.lua # optional
| Workspace | Directory below scripts/
|
|---|---|
mc |
mc/ |
mcedit |
editor/ |
mcview |
viewer/ |
mcterm |
terminal/ |
mcdiff |
diff/ |
The workspace is not a lua.ini property. A top-level directory such as
scripts/my-script/ is not discovered; place it under one of the directories
above instead.
The script manifest is named lua.ini. It must contain:
[Lua]
id=my-script
api_version=1
name=My script
entry=init.lua
provides=eventsThe ID is limited to 64 characters from A-Z, a-z, 0-9, _, . and
-, and must match its directory name. IDs are global across all workspaces.
Scripts load in lexical ID order. A user script with the same ID replaces the
whole system script.
provides describes the entry points declared by the script. It is an
optional comma-separated list for compatibility with earlier scripts:
events, macros, or both. The Lua scripts list displays this value, so
the user can distinguish a script that reacts to events from one that exposes
an action. A script using mc.macro() must declare provides=macros.
Each script has its own Lua state. require("a.b") searches the script's
lib/a/b.lua and then the shared library directory from the same origin. C
modules are disabled for these lookups.
The shared directories are ${datadir}/mc/lua/lib/ for system scripts and
${XDG_DATA_HOME:-~/.local/share}/mc/lua/lib/ for user scripts. The legacy
${XDG_CONFIG_HOME:-~/.config}/mc/lua/lib/ directory is also supported for
compatibility. A system script never searches a user shared directory.
The native runtime is loaded once, while every enabled package gets an isolated Lua state. Registration calls made by the entry file describe future callbacks; they do not give the package a permanently active MC context.
sequenceDiagram
participant Core as MC core
participant Loader as Runtime loader
participant Lua as mc-lua
participant Package as Lua package
Core->>Loader: Initialize runtime plugins
Loader->>Lua: dlopen and init(host API, context)
Lua->>Lua: Discover and validate lua.ini files
loop Each enabled package
Lua->>Package: Create isolated Lua state
Lua->>Package: Execute entry file
Package-->>Lua: Register callbacks, providers and source definitions
end
Core->>Lua: Publish startup snapshot
Lua->>Package: startup callback, when subscribed
Note over Core,Package: Normal callbacks and API calls
Core->>Lua: Publish shutdown snapshot
Lua->>Package: shutdown callback, when subscribed
Core->>Lua: shutdown()
Lua->>Package: Unsubscribe, clear owned UI, close state
Lua never calls an editor, panel or viewer object directly. mc-lua opens a
short-lived callback context, translates API calls into host operations, and
returns copied snapshots or opaque handles. The context ends when the callback
returns.
sequenceDiagram
participant Core as MC core
participant Host as Host services
participant Adapter as mc-lua adapter
participant Package as Lua package
Core->>Adapter: Event snapshot and active context
Adapter->>Package: callback(ev)
Package->>Adapter: mc.editor.current()
Adapter->>Host: Resolve current editor
Host->>Core: Query live editor
Core-->>Host: Opaque handle
Host-->>Adapter: Opaque handle
Adapter-->>Package: editor userdata
Package->>Adapter: editor:replace(range, text)
Adapter->>Adapter: Check context and capability
Adapter->>Host: Replace(handle, revision, range, text)
Host->>Core: Validate handle and revision, then apply edit
Core-->>Host: Result or stable error code
Host-->>Adapter: Result
Adapter-->>Package: true or nil, error
Package-->>Adapter: Callback result
Adapter-->>Core: PASS or CONSUME for editor.key, then end context
Calls outside the callback fail with no active MC context. A closed handle
or stale revision fails at the boundary instead of dereferencing an obsolete
MC object or changing the wrong buffer.
A Lua macro is an action registered while its script is loaded, but executed only when its key is pressed in the declared area. This is distinct from an event handler: loading the script registers the macro; it does not run its action.
For now the supported area is editor. The script must be in
scripts/editor/ (the mcedit workspace) and declare provides=macros:
mc.macro {
id = "decode-base64",
area = "editor",
key = "F11",
description = "Decode Base64 selection",
priority = 50, -- optional; 0 through 100
menu = { -- optional direct menu entry
path = "Tools",
label = "Decode Base64",
position = 100,
},
action = function (ev)
-- ev is an editor.key-style event snapshot
return mc.CONSUME
end,
}Macro IDs are unique inside their script. Key names are case-insensitive and
use the same spelling as ev.key.name, for example F11 or Ctrl-S.
The matching macro with the greatest priority runs; equal priorities retain
script load and registration order. A macro consumes its key by default.
Return false or mc.PASS to allow normal editor processing to continue.
After three action errors, only that macro is disabled for the session.
menu optionally places the same action directly in an editor menu. path
is the stable, untranslated top-level menu name. Existing names such as
Command append to that menu; any other name creates a top-level menu. The
optional label defaults to description. position ranges from -100000 to
100000 and orders runtime-provided entries within the target menu; lower
values appear first. Entries with the same position retain script load and
registration order. Menu placement is independent of listed, which only
controls the Run action list.
mc.process.run() synchronously runs a shell command through the core runtime
host and captures its output:
local result, err = mc.process.run {
command = "git status --short",
max_output = 8 * 1024 * 1024, -- optional, 1 byte through 64 MiB
}On success, result contains binary-safe stdout and stderr strings,
exit_code (or nil if terminated by a signal), signal, and the booleans
stdout_truncated and stderr_truncated. A non-zero command exit status is a
successful process invocation and must be handled by the script. The call is
allowed during an active callback context, except in notification-only phases
such as panel.file_open, and blocks the UI until the command exits. Commands
are intentionally interpreted by /bin/sh; scripts must not concatenate
untrusted text into command.
Register callbacks with mc.on() and remove them with mc.off():
local token = mc.on("panel.chdir", function (ev)
mc.ui.status("Directory: " .. ev.new_path)
end, { priority = 10 })
mc.on("shutdown", function ()
mc.off(token)
end)Priorities range from -100 to 100; higher callbacks run first and equal
priorities keep registration order. mc.off() is idempotent. For
editor.key, returning true or mc.CONSUME stops normal editor processing;
all other event callbacks are notifications. Prefer mc.macro() for a
user-visible key action; editor.key remains the low-level notification and
interception event.
Available event names are:
-
startup,shutdown -
panel.chdir,panel.selection_changed,panel.file_open -
editor.open,editor.save,editor.key viewer.open
Every callback receives a fresh, copied snapshot. The event-specific fields are:
| Event | Fields in ev
|
|---|---|
startup |
run_mode, config_dir, data_dir
|
shutdown |
reason |
panel.chdir |
panel, old_path, new_path, cause
|
panel.selection_changed |
panel, current, selected, selected_count, selected_truncated
|
panel.file_open |
panel, path, open_mode, is_dir
|
editor.open |
editor, path, readonly, line, column
|
editor.save |
editor, path, previous_path, save_as
|
editor.key |
editor, key (name, code, optional text, modifiers) |
viewer.open |
viewer, path, source_kind, start_line
|
current and every element of selected are mc.File snapshots with
name, path, is_dir, size, mtime, and marked. selected contains
at most 4096 items; selected_count remains the full count.
Panel, editor, and viewer references are opaque userdata, never C pointers.
They are valid only while their MC window is alive; a later call returns
nil, "closed" if it has gone away.
| Object | Creation and methods |
|---|---|
| Panel |
mc.panel.active(), mc.panel.passive(); :cwd(), :current(), :selected(), :refresh(), :chdir(path)
|
| Editor |
mc.editor.current(); :info(), :selection(), :text([range]), :replace(range, text), :replace_selection(text), :edit(spec), plus the legacy cursor, text, insert, path, readonly, and save methods |
| Viewer |
mc.viewer.current(); :path(), :position(), :mode(), :goto(offset)
|
cursor() and set_cursor() use one-based line and column numbers.
Their columns are editor display columns and therefore expand tabs using the
current editor:tab_width(). The tab width query returns the configured
positive tab-stop width; scripts that align text must use it rather than
assuming eight columns. When the editor permits the cursor beyond the end of
a line, cursor() includes that virtual-space distance in the returned
column; inserting at that position requires materializing the gap as spaces.
get_text(from, to) uses one-based inclusive byte positions. Lua reserves
the word goto, so call the viewer method as
viewer["goto"](viewer, offset).
selected_text() returns the current ordinary text selection, or
nil, "no_selection". Column selections return
nil, "column_selection_not_supported" rather than silently decoding a
different range.
New buffer operations use zero-based, half-open byte ranges. A stored range should carry the revision that produced it:
local info = assert(editor:info())
local bytes = assert(editor:text {
from = 0, to = info.byte_length, revision = info.revision,
})
assert(editor:replace({ from = 0, to = 3, revision = info.revision }, "new"))editor:edit { revision = n, changes = {...}, cursor = { offset = n } }
validates every range before changing the buffer, applies non-overlapping
changes atomically, and creates one undo entry. A changed document returns
nil, "stale_revision". mc.ui.text_width(text) returns MC's terminal display
width for valid UTF-8 text and is intended for alignment and drawing scripts.
Object and UI methods require an active MC event callback. Outside one they
return nil, "no active MC context"; unavailable application modes return
nil, "not_ready". State-changing calls are rejected with
nil, "forbidden_in_phase" in panel.file_open. That hook is a notification
only, not a way to intercept the file open operation.
mc.ui.status(text) updates the file-manager hint line and returns true.
mc.ui.message(title, text) displays a modal message. Both return
nil, "not_ready" when no compatible UI is active; message() is also a
state-changing operation in panel.file_open.
mc.ui.indicator { id, area = "editor", text, priority = 0 } installs or
updates a persistent status-line indicator and mc.ui.indicator_clear(id)
removes it. IDs are scoped to the owning package, so scripts cannot replace
one another's indicators. Higher-priority indicators are placed first and
lower-priority ones are omitted when the status line is too narrow. All
indicators owned by a package are removed automatically when it is unloaded.
The initial implementation renders the editor area; the area field keeps
the API extensible to other MC workspaces without editor-specific methods.
mc.ui.indicator {
id = "mode", area = "editor", text = "[╔═╗]", priority = 100,
}
-- later:
mc.ui.indicator_clear("mode")An input control in mc.ui.dialog() accepts an optional history name and
an optional completion array. complete_on_tab = true makes Tab invoke
completion while that input has focus; Shift-Tab still moves to the previous
control. Completion providers are files, hosts, commands, variables,
users, cd, and shell; they use MC's existing input completion engine and
may be combined. For example:
{
id = "command", type = "input", value = "",
history = "my-command-history",
complete_on_tab = true,
completion = { "commands", "files", "variables", "shell" },
}mc.log.debug/info/warn/error(text) writes a message tagged with the Lua
script ID.
An mc workspace package can register a virtual panel namespace with
mc.panel_provider.register(spec). The specification declares the provider
ID, title, path prefix, optional actions and package-local help, together with
callbacks for opening and closing instances, listing revisioned views,
navigating, entering entries, reloading, invoking actions, viewing or reading
an entry, and managing saved connections. Registration is declarative and
does not expose native panel structures to Lua.
Every list(instance) result is a snapshot with a non-zero revision and
stable entry IDs. Requests concerning an entry or selection carry that
revision, allowing the provider to reject stale operations after its contents
change. A view may supply its own title, columns, actions and help_node, so
different virtual levels can represent fixed folders or provider-specific
entities rather than only filesystem directories.
open_read(instance, entry) supplies content for another panel plugin. In
the current bridge it must return an mc.source.process description; MC adapts
that process to its input-stream interface, which lets consumers such as an
archive panel read a remote entry without first exposing a local filename.
The source is declarative: returning mc.source.process(...) does not hand a
Lua stream to arcmc. The bridge starts the process when the consumer opens the
stream, wraps its stdout in the native input-stream contract, and owns process
cleanup.
sequenceDiagram
participant Consumer as Native consumer
participant Bridge as Panel bridge
participant Lua as mc-lua
participant Provider as Lua provider
Consumer->>Bridge: Request entry stream (ID, revision)
Bridge->>Lua: OPEN_READ request
Lua->>Provider: open_read(instance, entry ID)
Provider-->>Lua: mc.source.process(spec)
Lua-->>Bridge: Typed source descriptor
Bridge-->>Consumer: mc_pp_input_stream
Consumer->>Bridge: open()
Bridge->>Bridge: Start process and connect stdout
Bridge-->>Consumer: Stream handle
loop Until EOF
Consumer->>Bridge: read(size)
Bridge-->>Consumer: bytes
end
Consumer->>Bridge: close stream
Bridge->>Bridge: Close pipe and reap process
The constructors mc.source.bytes(data), mc.source.file(spec),
mc.source.process(spec) and mc.source.pipeline(stages) describe where data
comes from without running a command or opening a viewer at construction time.
They are typed descriptions, not open file descriptors or Lua stream objects.
The API reference lists the fields accepted by each constructor and the
contexts in which each source kind is supported.
mc.viewer_source.define(spec) defines a reusable controller with open,
prepare and close callbacks, optional initial_params and options
callbacks, and help metadata. definition:create(argument, params) creates a single-use
controller; mc.ui.open_viewer { controller = controller } transfers its
ownership to MC and opens the native viewer. MC then owns the controller until
the viewer closes, including cleanup after an error.
The package owns a newly created controller until open_viewer() succeeds.
For a fixed source the adapter prepares the initial specification before the
transfer; with resize=rebuild, the viewer requests it after its viewport is
known. After a successful transfer, the viewer drives option changes, rebuilds
and final cleanup through the adapter.
sequenceDiagram
participant Package as Lua package
participant Adapter as mc-lua adapter
participant Viewer as Native viewer
Package->>Adapter: definition:create(argument, params)
Adapter->>Package: open(argument)
Package-->>Adapter: Session state
opt initial_params is defined
Adapter->>Package: initial_params(session, params)
Package-->>Adapter: Initial parameters
end
Adapter-->>Package: Single-use controller
Package->>Adapter: mc.ui.open_viewer(controller)
alt resize is none
Adapter->>Package: prepare(session, params)
Package-->>Adapter: Initial ViewerSpec and typed Source
Adapter->>Viewer: Open with initial specification
else resize is rebuild
Adapter->>Viewer: Open controller
Viewer->>Adapter: Request specification for viewport
Adapter->>Package: prepare(session, params, viewport)
Package-->>Adapter: Initial ViewerSpec and typed Source
Adapter-->>Viewer: Prepared specification
end
Note over Adapter,Viewer: On success the viewer owns the controller
loop Options change or resize rebuild
Viewer->>Adapter: Request another specification
Adapter->>Package: options() and/or prepare()
Package-->>Adapter: Parameters and ViewerSpec
Adapter-->>Viewer: Replace prepared source
end
Viewer->>Adapter: Close
Adapter->>Package: close(session)
Adapter->>Adapter: Release controller and source state
For a direct two-way comparison,
mc.ui.open_diff { left, right, left_label, right_label } opens native
mcdiff over two binary-safe in-memory strings. It must be called from an
active callback context.
The installed notify-editor-save and base64-decode Lua scripts are small
working examples. Copy a script into the corresponding user workspace
directory before adapting it, so system updates do not overwrite local changes.
Lua scripts run with the permissions of the current MC process. Install only scripts you trust. MC rejects a script or loaded Lua module if its directory tree is symbolic-linked, owned by neither the current user nor root, or is group/world writable. A Lua callback error is isolated to that callback; after three errors in one session the callback is disabled.