Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
8df8073
feat: add basic lua plugin load action
SirZenith Jun 5, 2026
a03927e
feat: add new bindings; add `create_cmd` API
SirZenith Jun 5, 2026
ce7a720
feat: add some method binding of basic types
SirZenith Jun 5, 2026
33cc795
feat: add Lua binding
SirZenith Jun 5, 2026
10e131f
feat: allow registering new sort method in Lua plugin
SirZenith Jun 5, 2026
a5edd71
feat: allow Lua plugin writes extra infomation to file object
SirZenith Jun 5, 2026
1628bbe
Merge branch 'register-sort-method' into lua
SirZenith Jun 5, 2026
7d70131
fix: move entries registered by Lua to standalone struct
SirZenith Jun 5, 2026
4568383
fix: apply Lua sort method in only one Lua function call
SirZenith Jun 6, 2026
bde8c59
feat: allow registering event hook from Lua plugin
SirZenith Jun 6, 2026
e5ca212
fix: move Lua registration API to lf module
SirZenith Jun 6, 2026
72392b1
feat: add UTF8 Lua helper module
SirZenith Jun 6, 2026
6ff5e9e
feat: add Lua state pool implementation as preparation for async Lua …
SirZenith Jun 6, 2026
bbcd40b
feat: distribute Lua function call to multiple Lua state
SirZenith Jun 6, 2026
ae27191
feat: add Lua previewer registration support
SirZenith Jun 7, 2026
2d13870
feat: add command for adjusting priority of Lua previewer
SirZenith Jun 7, 2026
616302c
feat: allow Lua command define completion method
SirZenith Jun 7, 2026
34b8521
rename module files
SirZenith Jun 7, 2026
76745ca
refactor: move compMatch binding to standalone file
SirZenith Jun 7, 2026
945fc83
refactor: adding documentation comment to Lua plugin API
SirZenith Jun 7, 2026
a23fd9b
refactor: simplify Lua message calling API
SirZenith Jun 7, 2026
8e64043
refactor: add `variant` field to luaMsgExpr
SirZenith Jun 7, 2026
8b3d246
refactor: merging binding file
SirZenith Jun 7, 2026
29c28bc
feat: add Lua binding
SirZenith Jun 7, 2026
d1ed384
fix: add registration for new Lua bindings
SirZenith Jun 7, 2026
5de1677
fix: fix improper Lua binding name
SirZenith Jun 7, 2026
388d1da
fix: fix return value of Lua binding
SirZenith Jun 8, 2026
df809a2
feat: add Lua API for calling command and running shell command
SirZenith Jun 8, 2026
c8c64c0
feat: add support for registering key map from Lua
SirZenith Jun 8, 2026
c32cfbc
feat: add Lua API for setting option
SirZenith Jun 8, 2026
eb459e4
feat: add support for converting slice/array and map to Lua value
SirZenith Jun 8, 2026
e8836cd
feat: allow Lua completion function returns nil as needed
SirZenith Jun 8, 2026
85365ff
feat: add Lua API for running call expression
SirZenith Jun 8, 2026
84936ab
feat: run Lua previewer on standalone goroutine; add binding for exec…
SirZenith Jun 9, 2026
2c25b57
fix: fix is_sync value for all Lua message registry
SirZenith Jun 9, 2026
9e296a5
feat: allow defining preview cleaner in Lua
SirZenith Jun 9, 2026
f25df72
fix: disallow getting Lua state from pool after shutdown
SirZenith Jun 9, 2026
f74c7c1
feat: close Lua previewer pipe when enough lines are read
SirZenith Jun 9, 2026
a5ec113
Merge branch 'master' into lua
SirZenith Jun 9, 2026
b95f9f8
feat: add command for reload Lua plugin
SirZenith Jun 9, 2026
15d9eb8
feat: add tags and marks read/write Lua binding
SirZenith Jun 9, 2026
9838f26
feat: allow calling plugin-reload command from Lua
SirZenith Jun 9, 2026
819090a
fix: force Lua echo/echomsg/echoerr/echoerrf call to be execute on ma…
SirZenith Jun 9, 2026
abef59b
feat: add tcell style struct Lua binding
SirZenith Jun 10, 2026
4bf6b1b
feat: allow specifying some of *fmt options in Lua
SirZenith Jun 10, 2026
3e5594c
feat: allow generating string result instead of using *fmt options wi…
SirZenith Jun 10, 2026
9312324
doc: add doc comment to Lua API
SirZenith Jun 11, 2026
a5748d2
refactor: change ordering of added commands
SirZenith Jun 11, 2026
6765e82
refactor: minimize usage of global variable
SirZenith Jun 11, 2026
18b6b59
refactor: change key name used in ui_formatter and ui_style
SirZenith Jun 11, 2026
83e26a7
refactor: adjust binding file structure
SirZenith Jun 11, 2026
c4d0958
refactor: rename functions
SirZenith Jun 11, 2026
b02ed26
refactor: unify is_sync key definition
SirZenith Jun 11, 2026
8bd3fc0
doc: add Lua development documentation
SirZenith Jun 11, 2026
4c9f2da
feat: allow loading Lua plugin from paths without `lfrc` around
SirZenith Jun 11, 2026
acec87c
Merge branch 'master' into lua
SirZenith Jun 11, 2026
42d1379
fix: add package import
SirZenith Jun 11, 2026
2bcd465
feat: when config path is passed from command line, use that for seac…
SirZenith Jun 11, 2026
29c03b7
feat: add Lua API for setting exec.Cmd stdout/stderr writer
SirZenith Jun 11, 2026
86a0d35
refactor: use lower case at the beginning of function name
SirZenith Jun 11, 2026
7fce442
feat: allow setting lf options and local options in Lua
SirZenith Jun 11, 2026
d757310
feat: allow deleting existing keymap from Lua
SirZenith Jun 11, 2026
3f4a6ce
fix: fix file entry Lua formatter calls
SirZenith Jun 11, 2026
426eac0
refactor: adjust Lua module function name prefix
SirZenith Jun 12, 2026
ba3d765
refactor: in Lua, load optioin and local option first
SirZenith Jun 12, 2026
7716f6e
fix: adjust Lua relative expr evaluation
SirZenith Jun 12, 2026
6d30734
feat: add Lua list, table extend function
SirZenith Jun 12, 2026
c02915b
fix: avoid dead lock in Lua option loading process
SirZenith Jun 12, 2026
0e48355
fix: store map in file struct instead of Lua table
SirZenith Jun 12, 2026
57dc160
fix: remove unsed import
SirZenith Jun 12, 2026
e567951
feat: use synchronous Lua state for execution by default
SirZenith Jun 12, 2026
d371a8f
feat: restrict some Lua API to synchronous mode
SirZenith Jun 12, 2026
7f3158a
doc: formalize variable names used in LUA_DEV
SirZenith Jun 12, 2026
5efeb7a
fix: fix dead lock caused by check sync mode in Lua API
SirZenith Jun 12, 2026
3fb35a1
fix: fix Lua cleaner existance check
SirZenith Jun 12, 2026
7ab5212
feat: add subprocess singaling Lua API
SirZenith Jun 12, 2026
a103e12
feat: allow defining shell with Lua
SirZenith Jun 12, 2026
547080f
feat: generalize shell registry type as misc for future extension
SirZenith Jun 12, 2026
481351b
feat: make Lua message log less verbose
SirZenith Jun 12, 2026
89d2c82
fix: fix merged indicator display
SirZenith Jun 12, 2026
93cc239
feat: add Lua binding for iconDef, iconMap
SirZenith Jun 12, 2026
9e7d5e1
fix: fix Lua binding definition
SirZenith Jun 13, 2026
5dfeb42
feat: add Lua UI formatter support for file entry
SirZenith Jun 13, 2026
fdd262e
feat: add API for drawing ruler with Lua
SirZenith Jun 13, 2026
ee1df6f
fix: fix go vet problems
SirZenith Jun 13, 2026
4aae94c
feat: register Lua UI messages without return value as ui_printer
SirZenith Jun 13, 2026
73886af
feat: add support for printing directory with Lua
SirZenith Jun 14, 2026
9f56db3
refactor: unify argument passing style of UI printers
SirZenith Jun 14, 2026
9dcd09c
feat: exposing plain directory printing function to Lua
SirZenith Jun 14, 2026
838a84a
feat: rewrite directory file iterating Lua API with iterator
SirZenith Jun 14, 2026
517decd
feat: allow get and set extra Lua data to dir object
SirZenith Jun 14, 2026
d75ecd5
fix: fix error handling in Lua extra data API
SirZenith Jun 14, 2026
3ec7ebc
fix: fix return type of luaNavCurrFile
SirZenith Jun 15, 2026
ec845d6
feat: add setter support to dir Lua binding
SirZenith Jun 15, 2026
140d86b
fix: add dir binding function to metatable
SirZenith Jun 15, 2026
8b79fba
fix: apply luamsglog option on Lua key map log
SirZenith Jun 15, 2026
98cea15
feat: support passing key count to Lua key map action
SirZenith Jun 15, 2026
a7d356e
fix: set registered luaKeyMapExpr count value to 1
SirZenith Jun 15, 2026
5bfe6b6
doc: add comment to Lua binding functions
SirZenith Jun 16, 2026
7edebca
feat: add Lua binding and doc comment
SirZenith Jun 16, 2026
7773a9f
feat: add Lua binding
SirZenith Jun 16, 2026
5bc8bd7
feat: add some missing Lua binding
SirZenith Jun 16, 2026
7769e06
refactor: simplify ruler printer Lua message call
SirZenith Jun 16, 2026
3d67ae6
feat: add Lua global variable for storing data between execution
SirZenith Jun 16, 2026
bbee662
feat: implement Lua sorting method as compare function
SirZenith Jun 17, 2026
0e3262b
refactor: remove call to `stat` Lua formatter
SirZenith Jun 17, 2026
128dcb7
refactor: simplify callLuaMsgOnState
SirZenith Jun 17, 2026
e200b5b
Merge branch 'master' into lua
SirZenith Jun 17, 2026
ebccfc4
feat: add Lua binding for completion matching functions
SirZenith Jun 18, 2026
63b3b00
feat: allow calling isPreviewable from sync Lua
SirZenith Jun 22, 2026
a41e018
doc: add comment about synchronous & asynchronous execution
SirZenith Jun 22, 2026
ea7c094
Merge branch 'master' into lua
SirZenith Jun 22, 2026
6db4edd
fix: remove platform specific singla Lua binding
SirZenith Jun 22, 2026
5e6e579
fix: remove unsed import
SirZenith Jun 22, 2026
54b337a
fix: fix linting issue
SirZenith Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 352 additions & 0 deletions LUA_DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
# Lua Plugin Support

Lua plugin support implementation is now written in following files:

- `lua_plugin.go`, functions for handling Lua state management, calling Lua functions.
- `lua_module_*.go` provides functionalities exposed to Lua as module.
- `lua_binding_*.go` defines data types exposed to Lua as user data and meta table

Following variables are provided as Lua global variable:

- `app`, user data pointing to app object.
- `lf_type`, a table containing all metatable of exported types.

## Execution Structure

Lua state gets initialized right before user config files are sourced, so that
sorting methods, commands, etc. registered in Lua plugins can be used in user
config file.

When initializing, program will look for `plugins` ditectory under parent directory
of config file (if no plugin directory path or config file path is specified from
command line), all subdirectory under `plugins` directories that contains `init.lua`
in them will be considered a plugin. And those `init.lua` will be used as plugin
entrance.

Lua states, are single threaded Lua interpreter state machines. To execute Lua
script, two different groups of Lua state objects are used:

- A single Lua state object that runs all scripts requiring synchronous execution.
- A Lua state pool, makes more Lua states when asked by different goroutine,
makes running Lua code concurrently possible.

Different Lua states share no data, so if a function depends on a variable data
stored on Lua state, then one must run this funtion in synchronous mode to make
sure this function gets executed on the one and only synchronous Lua state.

Lua states are encapsulate in a global variable `gLuaPool` defined in `lua_plugin.go`.
Lua state used for code execution can be acquired from this object.

Whenever a new Lua state is instanciated, plugin script evaluation will be run
on this Lua state to make sure it has the same initial state as others. And this
process is also required, since all data yielded during execution are local to
this Lua state.

## Plugin Function Registration

Every plugin entrance script must return a table for registering function to lf.
And each value in this table must also be a table. Let's call the value a registry
table, and key-value paris of registry tables are registry entries.

Not all, but many of supported registry tables provide messages, keys in those
registry tables would be used as message names.

The structure looks like this:

```lua
-- plugins/foo/init.lua
return {
registry_key = {
message_name = message_entry,
},
}
```

Basicaly, message entry takes one of three forms:

- plain value, which can be used directly by lf.
- function, gets called by lf, to provide extension to the program, let's call
this a message action function.
- table, when meta data is required, or there is more than one action associated
with one message name, a table is used to represent message entry.

Those data tables returned by plugin scripts will be stored in a global struct
called `gLuaRegistry`. Data tables are associated with the Lua state they belongs
to.

```go
var gLuaRegistry struct {
// first level indexing: pointer to data tables's owner state
// second level indexing: path to script that returns the table
stateDataMap map[*lua.LState]map[string]*lua.LTable

// ...
}
```

### Calling Message Action

The registry table structure makes distributing tasks amoung Lua states possible.

When calling a message, lf first acquires a Lua state for execution, and retrives
data table map of this state. Then, tries to locate a message action with 4 components:

- Source name, this is the path to plugin entrance script that provides this message.
- Registry key, used for fetching registry table from data table returned by
that plugin scripts.
- Message name.
- Variant name, when a message entry uses table as value, and contains multiple
action in it. lf will fetch action function from that table with variant name
as key.

Supported variant name varies depending on registry type.

There are serval functions provided for calling Lua message in `lua_plugin.go`,
such as `callLuaMsg`.

Lua state used for execution is not determined when calling `callLuaMsg`, but
creating Lua value arguments needed for message action call often requires access
to Lua state object. Hence a function with signature `func(L *lua.LState) []lua.LValue`
is passed in, so that message action arguments can be generated after Lua state
is acquired.

### Variant Handling

Some message entry support defining multiple actions. For example, a command entry
can have both an main action and a completion action.

A message action extractor is a function that takes a message entry and returns
a Lua function pointer as message action.

Each message variant has a extractor defined for it. Those extractors returns
action function found in message entry when it's defined as a table. And returns
default action function when message entry is defined as a non-table value or
such variant cannot be found.

### Asynchronous Message Action

For now, all message actions are synchronous by default, when message action
needs to be marked asynchronous, a message entry table with `is_async` set to true
is used.

For example:

```lua
return {
command = {
foo = {
action = function()
app:ui():echo("bar")
end,
is_async = true,
},
},
}
```

Asynchronous, does not necessary mean Lua tasks are executed in parallel, when
a message is synchronous, it has to wait until synchronous Lua state to become
free to get executed.

And there are binding APIs that calls another Lua message itself. If such binding
is used in synchronous message, and unluckly the message called by that binding is
also a synchronous message, then dead lock would occur.

There are two functions that can be used to force binding API to be called only
on synchronous/asynchronous Lua state:

- `tryRaiseNonSyncLuaStateError` raises error when `L` passed in is not synchronous
Lua state.
- `tryRaiseSyncLuaStateError` raises error whnen `L` passed in is synchronous Lua
state.

## Supported Registry Keys

> All entry types, returns types in this section are Lua types, some of them are
exposed from Go to Lua via binding.

Currently, following keys are supported:

- `command`, is a message table, adds new command to lf.

Entry type: `string`, `function`, `table`

Message variant:

- `action`, a function to run when this command gets called, can be a string or a function
- `completion`, a function that returns a list of `CompMatch` and matched string
- `event_hook`, is a message table, adds callback function for lf event like `on-init`, `on-load`, ...

Entry type: `function`, `table`

Message variant:

- `action`, a function to run when event happens.
- `key_map`, defines new key maps. It's registry table looks like this:

```lua
return {
key_map = {
n = {
["<c-f>"] = {
action = function()
app:ui:echo("hello")
end,
is_async = true,
},
}
}
}
```

Registry entry in `key_map` registry table uses key map mode as key (`n`, `v` and `c`),
and its value defines action of different keys under this mode.

Here, key map actions are defined just like message entries.

Key map entry type: `function`, `table`

Variant:

- `action`, a function to run when key map is triggered.
- `local_option`, each key in this registry table is a directory paths,
corresponding value is table containing options for this directory. All values
in option table are string, and allowed keys are option names allowed by `setlocal`
command.
- `misc`, is a message table, provide some extension to lf.

Entry type: `function`, `table`

Message variant provided by each message may vary depending on how lf use them,
but all of them has a `action` variant as main message action.

Currently supported messages are:

- `dupfile`, generates name for duplicated files during copy/move operation.
- `shell`, takes shell command and argument list, and makes an `exec.Cmd` from
them.
- `option`, is a table of string keys and string values. Keys are lf option names,
and its value will be set to corresponding lf option.
- `previewer`, is a message table, adds new preview action.

Entry type: `function`, `table`

Message variant:

- `action`, a function that takes a data writer and preview arguments, display
preview content by writing data to the writer.
- `clean`, cleaner function for this previewer.
- `condition`, a function that takes the path of target file, and returns a boolean
value for indicating whether this previewer is active for that file.
- `sorting_method`, is a message table, adds new sorting method to lf.

Entry type: `function`, `table`

Variant:

- `action`, a function takes a list of `File`, and returns sorted list of files.
- `ui_formatter`, is a message table, but allowed message names are predefined.

Provides formatter function for different UI element in place of `fmt` options
that with formatting verbs in them.

Messages in this table affect appearance of UI element by returning styled string.

Entry type: `function`, `table`

Variant:

- `action`, a function, its argument type differs by UI element type.

Currently supported keys are:

- `cursoractive`, formatting file entry under cursor in active directory window.
- `cursorparent`, formatting file entry under cursor in parent directory window.
- `cursorpreview`, formatting file entry under cursor in preview directory window.
- `error`, formatting error message.
- `numbercursor`, formatting directory line number under corsor.
- `number`, formatting directory line number.
- `tag`: formatting tags
- `ui_printer`, is a message table

Messages in this table will receive objects like `win`, `screen` as arguments,
and prints directly to screen to define how UI elements look.

Entry type: `function`, `table`

Variant:

- `action`

Currently supported keys are:

- `directory`, print content in window of a directory
- `dir_entry`, print a single entry in directory
- `ruler`, print ruler line
- `prompt`, print prompt line
- `ui_style`

This registry table provides style values for `fmt` options that do not allow
formatting verbs in their value. namely:

- borderfmt
- copyfmt
- cutfmt
- menufmt
- menuheaderfmt
- menuselectfmt
- selectfmt
- visual

Registry entry keys in this registry table are names of those options but without
that `fmt` suffix.

Entry type: `Style`, `fun(): Style`

## Type Binding

Some types are exposed to Lua via binding. Bindings are written in `lua_binding_*.go`,
types are grouped by the module they belong to.

Some of them are listed below:

- `lua_binding_bufio.go`, provides reader and writer for exchanging data between
lf, Lua and possible subprocess spawned in Lua script.
- `lua_binding_exec.go`, allows Lua to spawn subprocess.
- `lua_binding_main.go`, expose lf types like `app`, `nav`, `ui` ...
- `lua_binding_tcell.go`, expose `tcell.Style` type as a tool for writing
`ui_formatter` and setting `ui_stylel`.

One can build CSI styled string with builder style calls.

```lua
{
ui_formatter = {
tag = function(tag)
if tag == "-" then
return Style.new():foreground_name("yellow"):background_name("gray"):wrap(tag)
end
return Style:new():foreground_name("red"):wrap(tag)
end,
}
}
```

## Modules

Modules are written in `lua_module_*.go`, they are exposed to Lua as preload modules.

Modules can be accessed in Lua via `require` call:

```lua
local lf = require "lf"
```

- `lua_module_fs.go`: exposed as module `lf.fs` file and filepath operation.
- `lua_module_main.go`: exposed as module `lf`, provides API for accessing lf
functionalities, and miscellaneous helper functions.
- `lua_module_ui.go`: exposed as module `lf.ui` functions about drwaing UI.
- `lua_module_utf8.go`: exposed as module `lf.utf8`, helper function for dealing with UTF-8 strings.

Lua strings are plain byte blobs with no predefined structure. This module is
required if one wants to handle UTF-8 runes.
19 changes: 18 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ func (app *app) quit() {

onQuit(app)

gLuaPool.shutdown()

if gOpts.history {
if err := app.writeHistory(); err != nil {
log.Printf("writing history file: %s", err)
Expand Down Expand Up @@ -274,6 +276,10 @@ func (app *app) loop() {

go app.ui.readEvents()

// loads plugins before sourcing config files, so that functionalities
// provided by plugins can be used in config.
initializeLua(app)

if gConfigPath != "" {
if _, err := os.Stat(gConfigPath); !os.IsNotExist(err) {
app.readFile(gConfigPath)
Expand Down Expand Up @@ -591,7 +597,18 @@ func (app *app) runShell(s string, args []string, prefix string) {
gState.data["files"] = listFilesInCurrDir(app.nav)
gState.mutex.Unlock()

cmd := shellCommand(s, args)
luaCmdMaker := getLuaMiscMsg(luaMiscMsgShell)
var cmd *exec.Cmd
if luaCmdMaker != nil {
var makerErr error
cmd, makerErr = makeShellCmdWithLuaMsg(luaCmdMaker, s, args)
if makerErr != nil {
app.ui.echoerrf("running shell: failed to create command with Lua message, %s", makerErr)
return
}
} else {
cmd = shellCommand(s, args)
}

switch prefix {
case "$", "!":
Expand Down
Loading